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 fitWcs
Fit a TAN-SIP WCS from a list of reference object/source matches.
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.
Fit a TAN-SIP WCS given a list of reference object/source matches.