1 from __future__
import absolute_import, division, print_function
3 __all__ = [
"FitTanSipWcsTask",
"FitTanSipWcsConfig"]
5 from builtins
import zip
6 from builtins
import range
12 import lsst.afw.geom
as afwGeom
13 import lsst.afw.table
as afwTable
14 import lsst.pex.config
as pexConfig
15 import lsst.pipe.base
as pipeBase
16 from .setMatchDistance
import setMatchDistance
17 from .sip
import makeCreateWcsWithSip
21 order = pexConfig.RangeField(
22 doc=
"order of SIP polynomial",
27 numIter = pexConfig.RangeField(
28 doc=
"number of iterations of fitter (which fits X and Y separately, and so benefits from " +
34 numRejIter = pexConfig.RangeField(
35 doc=
"number of rejection iterations",
40 rejSigma = pexConfig.RangeField(
41 doc=
"Number of standard deviations for clipping level",
46 maxScatterArcsec = pexConfig.RangeField(
47 doc=
"maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " +
48 "be generous, as this is only intended to catch catastrophic failures",
64 """!Fit a TAN-SIP WCS given a list of reference object/source matches 66 @anchor FitTanSipWcsTask_ 68 @section meas_astrom_fitTanSipWcs_Contents Contents 70 - @ref meas_astrom_fitTanSipWcs_Purpose 71 - @ref meas_astrom_fitTanSipWcs_Initialize 72 - @ref meas_astrom_fitTanSipWcs_IO 73 - @ref meas_astrom_fitTanSipWcs_Schema 74 - @ref meas_astrom_fitTanSipWcs_Config 75 - @ref meas_astrom_fitTanSipWcs_Example 76 - @ref meas_astrom_fitTanSipWcs_Debug 78 @section meas_astrom_fitTanSipWcs_Purpose Description 80 Fit a TAN-SIP WCS given a list of reference object/source matches. 81 See CreateWithSip.h for information about the fitting algorithm. 83 @section meas_astrom_fitTanSipWcs_Initialize Task initialisation 87 @section meas_astrom_fitTanSipWcs_IO Invoking the Task 91 @section meas_astrom_fitTanSipWcs_Config Configuration parameters 93 See @ref FitTanSipWcsConfig 95 @section meas_astrom_fitTanSipWcs_Example A complete example of using FitTanSipWcsTask 97 FitTanSipWcsTask is a subtask of AstrometryTask, which is called by PhotoCalTask. 98 See \ref pipe_tasks_photocal_Example. 100 @section meas_astrom_fitTanSipWcs_Debug Debug variables 102 FitTanSipWcsTask does not support any debug variables. 104 ConfigClass = FitTanSipWcsConfig
105 _DefaultName =
"fitWcs" 108 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
109 """!Fit a TAN-SIP WCS from a list of reference object/source matches 111 @param[in,out] matches a list of lsst::afw::table::ReferenceMatch 112 The following fields are read: 113 - match.first (reference object) coord 114 - match.second (source) centroid 115 The following fields are written: 116 - match.first (reference object) centroid, 117 - match.second (source) centroid 118 - match.distance (on sky separation, in radians) 119 @param[in] initWcs initial WCS 120 @param[in] bbox the region over which the WCS will be valid (an lsst:afw::geom::Box2I); 121 if None or an empty box then computed from matches 122 @param[in,out] refCat reference object catalog, or None. 123 If provided then all centroids are updated with the new WCS, 124 otherwise only the centroids for ref objects in matches are updated. 125 Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". 126 @param[in,out] sourceCat source catalog, or None. 127 If provided then coords are updated with the new WCS; 128 otherwise only the coords for sources in matches are updated. 129 Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec". 130 @param[in] exposure Ignored; present for consistency with FitSipDistortionTask. 132 @return an lsst.pipe.base.Struct with the following fields: 133 - wcs the fit WCS as an lsst.afw.image.Wcs 134 - scatterOnSky median on-sky separation between reference objects and sources in "matches", 135 as an lsst.afw.geom.Angle 138 bbox = afwGeom.Box2I()
141 debug = lsstDebug.Info(__name__)
144 rejected = np.zeros(len(matches), dtype=bool)
145 for rej
in range(self.config.numRejIter):
146 sipObject = self.
_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
147 wcs = sipObject.getNewWcs()
149 if rejected.sum() == len(rejected):
150 raise RuntimeError(
"All matches rejected in iteration %d" % (rej + 1,))
152 "Iteration {0} of astrometry fitting: rejected {1} outliers, " 153 "out of {2} total matches.".format(
154 rej, rejected.sum(), len(rejected)
158 print(
"Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
159 self.
plotFit(matches, wcs, rejected)
161 sipObject = self.
_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
162 wcs = sipObject.getNewWcs()
164 print(
"Plotting final fit")
165 self.
plotFit(matches, wcs, rejected)
167 if refCat
is not None:
168 self.log.debug(
"Updating centroids in refCat")
169 afwTable.updateRefCentroids(wcs, refList=refCat)
171 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
172 afwTable.updateRefCentroids(wcs, refList=[match.first
for match
in matches])
174 if sourceCat
is not None:
175 self.log.debug(
"Updating coords in sourceCat")
176 afwTable.updateSourceCoords(wcs, sourceList=sourceCat)
178 self.log.warn(
"Updating source coords in match list; sourceCat is None")
179 afwTable.updateSourceCoords(wcs, sourceList=[match.second
for match
in matches])
181 self.log.debug(
"Updating distance in match list")
184 scatterOnSky = sipObject.getScatterOnSky()
186 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
187 raise pipeBase.TaskError(
188 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
189 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
191 return pipeBase.Struct(
193 scatterOnSky=scatterOnSky,
197 """Generate a guess Wcs from the astrometric matches 199 We create a Wcs anchored at the center of the matches, with the scale 200 of the input Wcs. This is necessary because matching returns only 201 matches with no estimated Wcs, and the input Wcs is a wild guess. 202 We're using the best of each: positions from the matches, and scale 205 crpix = afwGeom.Extent2D(0, 0)
206 crval = afwGeom.Extent3D(0, 0, 0)
208 crpix += afwGeom.Extent2D(mm.second.getCentroid())
209 crval += afwGeom.Extent3D(mm.first.getCoord().toIcrs().getVector())
210 crpix /= len(matches)
211 crval /= len(matches)
212 newWcs = afwImage.Wcs(afwCoord.IcrsCoord(afwGeom.Point3D(crval)).getPosition(),
213 afwGeom.Point2D(crpix), wcs.getCDMatrix())
216 def _fitWcs(self, matches, wcs):
217 """Fit a Wcs based on the matches and a guess Wcs""" 218 for i
in range(self.config.numIter):
220 wcs = sipObject.getNewWcs()
224 """Flag deviant matches 226 We return a boolean numpy array indicating whether the corresponding 227 match should be rejected. The previous list of rejections is used 228 so we can calculate uncontaminated statistics. 230 fit = [wcs.skyToPixel(m.first.getCoord())
for m
in matches]
231 dx = np.array([ff.getX() - mm.second.getCentroid().getX()
for ff, mm
in zip(fit, matches)])
232 dy = np.array([ff.getY() - mm.second.getCentroid().getY()
for ff, mm
in zip(fit, matches)])
233 good = np.logical_not(rejected)
234 return (dx > self.config.rejSigma*dx[good].
std()) | (dy > self.config.rejSigma*dy[good].
std())
239 We create four plots, for all combinations of (dx, dy) against 240 (x, y). Good points are black, while rejected points are red. 243 import matplotlib.pyplot
as plt
244 except ImportError
as e:
245 self.log.warn(
"Unable to import matplotlib: %s", e)
248 fit = [wcs.skyToPixel(m.first.getCoord())
for m
in matches]
249 x1 = np.array([ff.getX()
for ff
in fit])
250 y1 = np.array([ff.getY()
for ff
in fit])
251 x2 = np.array([m.second.getCentroid().getX()
for m
in matches])
252 y2 = np.array([m.second.getCentroid().getY()
for m
in matches])
257 good = np.logical_not(rejected)
259 figure = plt.figure()
260 axes = figure.add_subplot(2, 2, 1)
261 axes.plot(x2[good], dx[good],
'ko')
262 axes.plot(x2[rejected], dx[rejected],
'ro')
264 axes.set_ylabel(
"dx")
266 axes = figure.add_subplot(2, 2, 2)
267 axes.plot(x2[good], dy[good],
'ko')
268 axes.plot(x2[rejected], dy[rejected],
'ro')
270 axes.set_ylabel(
"dy")
272 axes = figure.add_subplot(2, 2, 3)
273 axes.plot(y2[good], dx[good],
'ko')
274 axes.plot(y2[rejected], dx[rejected],
'ro')
276 axes.set_ylabel(
"dx")
278 axes = figure.add_subplot(2, 2, 4)
279 axes.plot(y2[good], dy[good],
'ko')
280 axes.plot(y2[rejected], dy[rejected],
'ro')
282 axes.set_ylabel(
"dy")
def plotFit(self, matches, wcs, rejected)
def initialWcs(self, matches, wcs)
CreateWcsWithSip< MatchT > makeCreateWcsWithSip(std::vector< MatchT > const &matches, afw::image::Wcs const &linearWcs, int const order, afw::geom::Box2I const &bbox=afw::geom::Box2I(), int const ngrid=0)
Factory function for CreateWcsWithSip.
def rejectMatches(self, matches, wcs, rejected)
def _fitWcs(self, matches, wcs)
def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None)
Fit a TAN-SIP WCS from a list of reference object/source matches.
def setMatchDistance(matches)
Fit a TAN-SIP WCS given a list of reference object/source matches.