22 __all__ = [
"FitTanSipWcsTask",
"FitTanSipWcsConfig"]
33 from lsst.utils.timer
import timeMethod
34 from .setMatchDistance
import setMatchDistance
35 from .sip
import makeCreateWcsWithSip
39 """Config for FitTanSipWcsTask."""
40 order = pexConfig.RangeField(
41 doc=
"order of SIP polynomial",
46 numIter = pexConfig.RangeField(
47 doc=
"number of iterations of fitter (which fits X and Y separately, and so benefits from "
53 numRejIter = pexConfig.RangeField(
54 doc=
"number of rejection iterations",
59 rejSigma = pexConfig.RangeField(
60 doc=
"Number of standard deviations for clipping level",
65 maxScatterArcsec = pexConfig.RangeField(
66 doc=
"maximum median scatter of a WCS fit beyond which the fit fails (arcsec); "
67 "be generous, as this is only intended to catch catastrophic failures",
75 """Fit a TAN-SIP WCS given a list of reference object/source matches.
77 ConfigClass = FitTanSipWcsConfig
78 _DefaultName =
"fitWcs"
81 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
82 """Fit a TAN-SIP WCS from a list of reference object/source matches
86 matches : `list` of `lsst.afw.table.ReferenceMatch`
87 The following fields are read:
89 - match.first (reference object) coord
90 - match.second (source) centroid
92 The following fields are written:
94 - match.first (reference object) centroid,
95 - match.second (source) centroid
96 - match.distance (on sky separation, in radians)
98 initWcs : `lsst.afw.geom.SkyWcs`
100 bbox : `lsst.geom.Box2I`
101 the region over which the WCS will be valid (an lsst:afw::geom::Box2I);
102 if None or an empty box then computed from matches
103 refCat : `lsst.afw.table.SimpleCatalog`
104 reference object catalog, or None.
105 If provided then all centroids are updated with the new WCS,
106 otherwise only the centroids for ref objects in matches are updated.
107 Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec".
108 sourceCat : `lsst.afw.table.SourceCatalog`
109 source catalog, or None.
110 If provided then coords are updated with the new WCS;
111 otherwise only the coords for sources in matches are updated.
112 Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec".
113 exposure : `lsst.afw.image.Exposure`
114 Ignored; present for consistency with FitSipDistortionTask.
118 result : `lsst.pipe.base.Struct`
119 with the following fields:
121 - ``wcs`` : the fit WCS (`lsst.afw.geom.SkyWcs`)
122 - ``scatterOnSky`` : median on-sky separation between reference
123 objects and sources in "matches" (`lsst.afw.geom.Angle`)
131 wcs = self.
initialWcsinitialWcs(matches, initWcs)
132 rejected = np.zeros(len(matches), dtype=bool)
133 for rej
in range(self.config.numRejIter):
134 sipObject = self.
_fitWcs_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
135 wcs = sipObject.getNewWcs()
136 rejected = self.
rejectMatchesrejectMatches(matches, wcs, rejected)
137 if rejected.sum() == len(rejected):
138 raise RuntimeError(
"All matches rejected in iteration %d" % (rej + 1,))
140 "Iteration {0} of astrometry fitting: rejected {1} outliers, "
141 "out of {2} total matches.".format(
142 rej, rejected.sum(), len(rejected)
146 print(
"Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
147 self.
plotFitplotFit(matches, wcs, rejected)
149 sipObject = self.
_fitWcs_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
150 wcs = sipObject.getNewWcs()
152 print(
"Plotting final fit")
153 self.
plotFitplotFit(matches, wcs, rejected)
155 if refCat
is not None:
156 self.log.debug(
"Updating centroids in refCat")
159 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
162 if sourceCat
is not None:
163 self.log.debug(
"Updating coords in sourceCat")
166 self.log.warn(
"Updating source coords in match list; sourceCat is None")
169 self.log.debug(
"Updating distance in match list")
172 scatterOnSky = sipObject.getScatterOnSky()
174 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
175 raise pipeBase.TaskError(
176 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
177 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
179 return pipeBase.Struct(
181 scatterOnSky=scatterOnSky,
185 """Generate a guess Wcs from the astrometric matches
187 We create a Wcs anchored at the center of the matches, with the scale
188 of the input Wcs. This is necessary because matching returns only
189 matches with no estimated Wcs, and the input Wcs is a wild guess.
190 We're using the best of each: positions from the matches, and scale
195 matches : `list` of `lsst.afw.table.ReferenceMatch`
196 List of sources matched to references.
197 wcs : `lsst.afw.geom.SkyWcs`
202 newWcs : `lsst.afw.geom.SkyWcs`
203 Initial WCS guess from estimated crpix and crval.
209 crval += mm.first.getCoord().getVector()
210 crpix /= len(matches)
211 crval /= len(matches)
214 cdMatrix=wcs.getCdMatrix())
217 def _fitWcs(self, matches, wcs):
218 """Fit a Wcs based on the matches and a guess Wcs.
222 matches : `list` of `lsst.afw.table.ReferenceMatch`
223 List of sources matched to references.
224 wcs : `lsst.afw.geom.SkyWcs`
229 sipObject : `lsst.meas.astrom.sip.CreateWcsWithSip`
232 for i
in range(self.config.numIter):
234 wcs = sipObject.getNewWcs()
238 """Flag deviant matches
240 We return a boolean numpy array indicating whether the corresponding
241 match should be rejected. The previous list of rejections is used
242 so we can calculate uncontaminated statistics.
246 matches : `list` of `lsst.afw.table.ReferenceMatch`
247 List of sources matched to references.
248 wcs : `lsst.afw.geom.SkyWcs`
250 rejected : array-like of `bool`
251 Array of matches rejected from the fit. Unused.
255 rejectedMatches : `ndarray` of type `bool`
256 Matched objects found to be outside of tolerance.
258 fit = [wcs.skyToPixel(m.first.getCoord())
for m
in matches]
259 dx = np.array([ff.getX() - mm.second.getCentroid().getX()
for ff, mm
in zip(fit, matches)])
260 dy = np.array([ff.getY() - mm.second.getCentroid().getY()
for ff, mm
in zip(fit, matches)])
261 good = np.logical_not(rejected)
262 return (dx > self.config.rejSigma*dx[good].
std()) | (dy > self.config.rejSigma*dy[good].
std())
267 We create four plots, for all combinations of (dx, dy) against
268 (x, y). Good points are black, while rejected points are red.
272 matches : `list` of `lsst.afw.table.ReferenceMatch`
273 List of sources matched to references.
274 wcs : `lsst.afw.geom.SkyWcs`
276 rejected : array-like of `bool`
277 Array of matches rejected from the fit.
280 import matplotlib.pyplot
as plt
281 except ImportError
as e:
282 self.log.warn(
"Unable to import matplotlib: %s", e)
285 fit = [wcs.skyToPixel(m.first.getCoord())
for m
in matches]
286 x1 = np.array([ff.getX()
for ff
in fit])
287 y1 = np.array([ff.getY()
for ff
in fit])
288 x2 = np.array([m.second.getCentroid().getX()
for m
in matches])
289 y2 = np.array([m.second.getCentroid().getY()
for m
in matches])
294 good = np.logical_not(rejected)
296 figure = plt.figure()
297 axes = figure.add_subplot(2, 2, 1)
298 axes.plot(x2[good], dx[good],
'ko')
299 axes.plot(x2[rejected], dx[rejected],
'ro')
301 axes.set_ylabel(
"dx")
303 axes = figure.add_subplot(2, 2, 2)
304 axes.plot(x2[good], dy[good],
'ko')
305 axes.plot(x2[rejected], dy[rejected],
'ro')
307 axes.set_ylabel(
"dy")
309 axes = figure.add_subplot(2, 2, 3)
310 axes.plot(y2[good], dx[good],
'ko')
311 axes.plot(y2[rejected], dx[rejected],
'ro')
313 axes.set_ylabel(
"dx")
315 axes = figure.add_subplot(2, 2, 4)
316 axes.plot(y2[good], dy[good],
'ko')
317 axes.plot(y2[rejected], dy[rejected],
'ro')
319 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)
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
def setMatchDistance(matches)
CreateWcsWithSip< MatchT > makeCreateWcsWithSip(std::vector< MatchT > const &matches, afw::geom::SkyWcs const &linearWcs, int const order, geom::Box2I const &bbox=geom::Box2I(), int const ngrid=0)
Factory function for CreateWcsWithSip.