22__all__ = [
"FitTanSipWcsTask",
"FitTanSipWcsConfig"]
33from .setMatchDistance
import setMatchDistance
34from .sip
import makeCreateWcsWithSip
38 """Config for FitTanSipWcsTask."""
39 order = pexConfig.RangeField(
40 doc=
"order of SIP polynomial",
45 numIter = pexConfig.RangeField(
46 doc=
"number of iterations of fitter (which fits X and Y separately, and so benefits from "
52 numRejIter = pexConfig.RangeField(
53 doc=
"number of rejection iterations",
58 rejSigma = pexConfig.RangeField(
59 doc=
"Number of standard deviations for clipping level",
64 maxScatterArcsec = pexConfig.RangeField(
65 doc=
"maximum median scatter of a WCS fit beyond which the fit fails (arcsec); "
66 "be generous, as this is only intended to catch catastrophic failures",
74 """Fit a TAN-SIP WCS given a list of reference object/source matches.
76 ConfigClass = FitTanSipWcsConfig
77 _DefaultName = "fitWcs"
80 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
81 """Fit a TAN-SIP WCS from a list of reference object/source matches
86 The following fields are read:
88 - match.first (reference object) coord
89 - match.second (source) centroid
91 The following fields are written:
93 - match.first (reference object) centroid,
94 - match.second (source) centroid
95 - match.distance (on sky separation, in radians)
100 the region over which the WCS will be valid (an lsst:afw::geom::Box2I);
101 if None or an empty box then computed
from matches
103 reference object catalog,
or None.
104 If provided then all centroids are updated
with the new WCS,
105 otherwise only the centroids
for ref objects
in matches are updated.
106 Required fields are
"centroid_x",
"centroid_y",
"coord_ra",
and "coord_dec".
108 source catalog,
or None.
109 If provided then coords are updated
with the new WCS;
110 otherwise only the coords
for sources
in matches are updated.
111 Required fields are
"slot_Centroid_x",
"slot_Centroid_y",
and "coord_ra",
and "coord_dec".
113 Ignored; present
for consistency
with FitSipDistortionTask.
117 result : `lsst.pipe.base.Struct`
118 with the following fields:
121 - ``scatterOnSky`` : median on-sky separation between reference
122 objects
and sources
in "matches" (`lsst.afw.geom.Angle`)
131 rejected = np.zeros(len(matches), dtype=bool)
132 for rej
in range(self.config.numRejIter):
133 sipObject = self.
_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
134 wcs = sipObject.getNewWcs()
136 if rejected.sum() == len(rejected):
137 raise RuntimeError(
"All matches rejected in iteration %d" % (rej + 1,))
139 "Iteration {0} of astrometry fitting: rejected {1} outliers, "
140 "out of {2} total matches.".format(
141 rej, rejected.sum(), len(rejected)
145 print(
"Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
146 self.
plotFit(matches, wcs, rejected)
148 sipObject = self.
_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
149 wcs = sipObject.getNewWcs()
151 print(
"Plotting final fit")
152 self.
plotFit(matches, wcs, rejected)
154 if refCat
is not None:
155 self.log.debug(
"Updating centroids in refCat")
158 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
161 if sourceCat
is not None:
162 self.log.debug(
"Updating coords in sourceCat")
165 self.log.warn(
"Updating source coords in match list; sourceCat is None")
168 self.log.debug(
"Updating distance in match list")
169 setMatchDistance(matches)
171 scatterOnSky = sipObject.getScatterOnSky()
173 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
174 raise pipeBase.TaskError(
175 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
176 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
178 return pipeBase.Struct(
180 scatterOnSky=scatterOnSky,
184 """Generate a guess Wcs from the astrometric matches
186 We create a Wcs anchored at the center of the matches, with the scale
187 of the input Wcs. This
is necessary because matching returns only
188 matches
with no estimated Wcs,
and the input Wcs
is a wild guess.
189 We
're using the best of each: positions from the matches, and scale
195 List of sources matched to references.
202 Initial WCS guess
from estimated crpix
and crval.
208 crval += mm.first.getCoord().getVector()
209 crpix /= len(matches)
210 crval /= len(matches)
213 cdMatrix=wcs.getCdMatrix())
216 def _fitWcs(self, matches, wcs):
217 """Fit a Wcs based on the matches and a guess Wcs.
222 List of sources matched to references.
231 for i
in range(self.config.numIter):
232 sipObject = makeCreateWcsWithSip(matches, wcs, self.config.order)
233 wcs = sipObject.getNewWcs()
237 """Flag deviant matches
239 We return a boolean numpy array indicating whether the corresponding
240 match should be rejected. The previous list of rejections
is used
241 so we can calculate uncontaminated statistics.
246 List of sources matched to references.
249 rejected : array-like of `bool`
250 Array of matches rejected
from the fit. Unused.
254 rejectedMatches : `ndarray` of type `bool`
255 Matched objects found to be outside of tolerance.
257 fit = [wcs.skyToPixel(m.first.getCoord()) for m
in matches]
258 dx = np.array([ff.getX() - mm.second.getCentroid().getX()
for ff, mm
in zip(fit, matches)])
259 dy = np.array([ff.getY() - mm.second.getCentroid().getY()
for ff, mm
in zip(fit, matches)])
260 good = np.logical_not(rejected)
261 return (dx > self.config.rejSigma*dx[good].
std()) | (dy > self.config.rejSigma*dy[good].
std())
266 We create four plots, for all combinations of (dx, dy) against
267 (x, y). Good points are black,
while rejected points are red.
272 List of sources matched to references.
275 rejected : array-like of `bool`
276 Array of matches rejected
from the fit.
279 import matplotlib.pyplot
as plt
280 except ImportError
as e:
281 self.log.warn(
"Unable to import matplotlib: %s", e)
284 fit = [wcs.skyToPixel(m.first.getCoord())
for m
in matches]
285 x1 = np.array([ff.getX()
for ff
in fit])
286 y1 = np.array([ff.getY()
for ff
in fit])
287 x2 = np.array([m.second.getCentroid().getX()
for m
in matches])
288 y2 = np.array([m.second.getCentroid().getY()
for m
in matches])
293 good = np.logical_not(rejected)
295 figure = plt.figure()
296 axes = figure.add_subplot(2, 2, 1)
297 axes.plot(x2[good], dx[good],
'ko')
298 axes.plot(x2[rejected], dx[rejected],
'ro')
300 axes.set_ylabel(
"dx")
302 axes = figure.add_subplot(2, 2, 2)
303 axes.plot(x2[good], dy[good],
'ko')
304 axes.plot(x2[rejected], dy[rejected],
'ro')
306 axes.set_ylabel(
"dy")
308 axes = figure.add_subplot(2, 2, 3)
309 axes.plot(y2[good], dx[good],
'ko')
310 axes.plot(y2[rejected], dx[rejected],
'ro')
312 axes.set_ylabel(
"dx")
314 axes = figure.add_subplot(2, 2, 4)
315 axes.plot(y2[good], dy[good],
'ko')
316 axes.plot(y2[rejected], dy[rejected],
'ro')
318 axes.set_ylabel(
"dy")
def _fitWcs(self, matches, wcs)
def initialWcs(self, matches, wcs)
def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None)
def plotFit(self, matches, wcs, rejected)
def rejectMatches(self, matches, wcs, rejected)
Measure the distortions in an image plane and express them a SIP polynomials.
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)