1 from __future__
import absolute_import, division, print_function
3 __all__ = [
"FitTanSipWcsTask",
"FitTanSipWcsConfig"]
5 from builtins
import zip
6 from builtins
import range
14 import lsst.pipe.base
as pipeBase
15 from .setMatchDistance
import setMatchDistance
16 from .sip
import makeCreateWcsWithSip
20 order = pexConfig.RangeField(
21 doc=
"order of SIP polynomial",
26 numIter = pexConfig.RangeField(
27 doc=
"number of iterations of fitter (which fits X and Y separately, and so benefits from " +
33 numRejIter = pexConfig.RangeField(
34 doc=
"number of rejection iterations",
39 rejSigma = pexConfig.RangeField(
40 doc=
"Number of standard deviations for clipping level",
45 maxScatterArcsec = pexConfig.RangeField(
46 doc=
"maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " +
47 "be generous, as this is only intended to catch catastrophic failures",
63 """!Fit a TAN-SIP WCS given a list of reference object/source matches 65 @anchor FitTanSipWcsTask_ 67 @section meas_astrom_fitTanSipWcs_Contents Contents 69 - @ref meas_astrom_fitTanSipWcs_Purpose 70 - @ref meas_astrom_fitTanSipWcs_Initialize 71 - @ref meas_astrom_fitTanSipWcs_IO 72 - @ref meas_astrom_fitTanSipWcs_Schema 73 - @ref meas_astrom_fitTanSipWcs_Config 74 - @ref meas_astrom_fitTanSipWcs_Example 75 - @ref meas_astrom_fitTanSipWcs_Debug 77 @section meas_astrom_fitTanSipWcs_Purpose Description 79 Fit a TAN-SIP WCS given a list of reference object/source matches. 80 See CreateWithSip.h for information about the fitting algorithm. 82 @section meas_astrom_fitTanSipWcs_Initialize Task initialisation 86 @section meas_astrom_fitTanSipWcs_IO Invoking the Task 90 @section meas_astrom_fitTanSipWcs_Config Configuration parameters 92 See @ref FitTanSipWcsConfig 94 @section meas_astrom_fitTanSipWcs_Example A complete example of using FitTanSipWcsTask 96 FitTanSipWcsTask is a subtask of AstrometryTask, which is called by PhotoCalTask. 97 See \ref pipe_tasks_photocal_Example. 99 @section meas_astrom_fitTanSipWcs_Debug Debug variables 101 FitTanSipWcsTask does not support any debug variables. 103 ConfigClass = FitTanSipWcsConfig
104 _DefaultName =
"fitWcs" 107 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
108 """!Fit a TAN-SIP WCS from a list of reference object/source matches 110 @param[in,out] matches a list of lsst::afw::table::ReferenceMatch 111 The following fields are read: 112 - match.first (reference object) coord 113 - match.second (source) centroid 114 The following fields are written: 115 - match.first (reference object) centroid, 116 - match.second (source) centroid 117 - match.distance (on sky separation, in radians) 118 @param[in] initWcs initial WCS 119 @param[in] bbox the region over which the WCS will be valid (an lsst:afw::geom::Box2I); 120 if None or an empty box then computed from matches 121 @param[in,out] refCat reference object catalog, or None. 122 If provided then all centroids are updated with the new WCS, 123 otherwise only the centroids for ref objects in matches are updated. 124 Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". 125 @param[in,out] sourceCat source catalog, or None. 126 If provided then coords are updated with the new WCS; 127 otherwise only the coords for sources in matches are updated. 128 Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec". 129 @param[in] exposure Ignored; present for consistency with FitSipDistortionTask. 131 @return an lsst.pipe.base.Struct with the following fields: 132 - wcs the fit WCS as an lsst.afw.geom.Wcs 133 - scatterOnSky median on-sky separation between reference objects and sources in "matches", 134 as an lsst.afw.geom.Angle 143 rejected = np.zeros(len(matches), dtype=bool)
144 for rej
in range(self.config.numRejIter):
145 sipObject = self.
_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
146 wcs = sipObject.getNewWcs()
148 if rejected.sum() == len(rejected):
149 raise RuntimeError(
"All matches rejected in iteration %d" % (rej + 1,))
151 "Iteration {0} of astrometry fitting: rejected {1} outliers, " 152 "out of {2} total matches.".format(
153 rej, rejected.sum(), len(rejected)
157 print(
"Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
158 self.
plotFit(matches, wcs, rejected)
160 sipObject = self.
_fitWcs([mm
for i, mm
in enumerate(matches)
if not rejected[i]], wcs)
161 wcs = sipObject.getNewWcs()
163 print(
"Plotting final fit")
164 self.
plotFit(matches, wcs, rejected)
166 if refCat
is not None:
167 self.log.debug(
"Updating centroids in refCat")
170 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
173 if sourceCat
is not None:
174 self.log.debug(
"Updating coords in sourceCat")
177 self.log.warn(
"Updating source coords in match list; sourceCat is None")
180 self.log.debug(
"Updating distance in match list")
183 scatterOnSky = sipObject.getScatterOnSky()
185 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
186 raise pipeBase.TaskError(
187 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
188 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
190 return pipeBase.Struct(
192 scatterOnSky=scatterOnSky,
196 """Generate a guess Wcs from the astrometric matches 198 We create a Wcs anchored at the center of the matches, with the scale 199 of the input Wcs. This is necessary because matching returns only 200 matches with no estimated Wcs, and the input Wcs is a wild guess. 201 We're using the best of each: positions from the matches, and scale 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""" 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)
CreateWcsWithSip< MatchT > makeCreateWcsWithSip(std::vector< MatchT > const &matches, afw::geom::SkyWcs const &linearWcs, int const order, afw::geom::Box2I const &bbox=afw::geom::Box2I(), int const ngrid=0)
Factory function for CreateWcsWithSip.
std::shared_ptr< SkyWcs > makeSkyWcs(Point2D const &crpix, coord::IcrsCoord const &crval, Eigen::Matrix2d const &cdMatrix, std::string const &projection="TAN")
def initialWcs(self, matches, wcs)
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
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.