lsst.meas.astrom  14.0-7-g0d69b06+3
fitTanSipWcs.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 __all__ = ["FitTanSipWcsTask", "FitTanSipWcsConfig"]
4 
5 from builtins import zip
6 from builtins import range
7 
8 import numpy as np
9 
10 import lsst.afw.coord as afwCoord
11 import lsst.afw.geom as afwGeom
12 import lsst.afw.table as afwTable
13 import lsst.pex.config as pexConfig
14 import lsst.pipe.base as pipeBase
15 from .setMatchDistance import setMatchDistance
16 from .sip import makeCreateWcsWithSip
17 
18 
19 class FitTanSipWcsConfig(pexConfig.Config):
20  order = pexConfig.RangeField(
21  doc="order of SIP polynomial",
22  dtype=int,
23  default=4,
24  min=0,
25  )
26  numIter = pexConfig.RangeField(
27  doc="number of iterations of fitter (which fits X and Y separately, and so benefits from " +
28  "a few iterations",
29  dtype=int,
30  default=3,
31  min=1,
32  )
33  numRejIter = pexConfig.RangeField(
34  doc="number of rejection iterations",
35  dtype=int,
36  default=1,
37  min=0,
38  )
39  rejSigma = pexConfig.RangeField(
40  doc="Number of standard deviations for clipping level",
41  dtype=float,
42  default=3.0,
43  min=0.0,
44  )
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",
48  dtype=float,
49  default=10,
50  min=0,
51  )
52 
53 # The following block adds links to this task from the Task Documentation page.
54 
60 
61 
62 class FitTanSipWcsTask(pipeBase.Task):
63  """!Fit a TAN-SIP WCS given a list of reference object/source matches
64 
65  @anchor FitTanSipWcsTask_
66 
67  @section meas_astrom_fitTanSipWcs_Contents Contents
68 
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
76 
77  @section meas_astrom_fitTanSipWcs_Purpose Description
78 
79  Fit a TAN-SIP WCS given a list of reference object/source matches.
80  See CreateWithSip.h for information about the fitting algorithm.
81 
82  @section meas_astrom_fitTanSipWcs_Initialize Task initialisation
83 
84  @copydoc \_\_init\_\_
85 
86  @section meas_astrom_fitTanSipWcs_IO Invoking the Task
87 
88  @copydoc fitWcs
89 
90  @section meas_astrom_fitTanSipWcs_Config Configuration parameters
91 
92  See @ref FitTanSipWcsConfig
93 
94  @section meas_astrom_fitTanSipWcs_Example A complete example of using FitTanSipWcsTask
95 
96  FitTanSipWcsTask is a subtask of AstrometryTask, which is called by PhotoCalTask.
97  See \ref pipe_tasks_photocal_Example.
98 
99  @section meas_astrom_fitTanSipWcs_Debug Debug variables
100 
101  FitTanSipWcsTask does not support any debug variables.
102  """
103  ConfigClass = FitTanSipWcsConfig
104  _DefaultName = "fitWcs"
105 
106  @pipeBase.timeMethod
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
109 
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.
130 
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
135  """
136  if bbox is None:
137  bbox = afwGeom.Box2I()
138 
139  import lsstDebug
140  debug = lsstDebug.Info(__name__)
141 
142  wcs = self.initialWcs(matches, initWcs)
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()
147  rejected = self.rejectMatches(matches, wcs, rejected)
148  if rejected.sum() == len(rejected):
149  raise RuntimeError("All matches rejected in iteration %d" % (rej + 1,))
150  self.log.debug(
151  "Iteration {0} of astrometry fitting: rejected {1} outliers, "
152  "out of {2} total matches.".format(
153  rej, rejected.sum(), len(rejected)
154  )
155  )
156  if debug.plot:
157  print("Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
158  self.plotFit(matches, wcs, rejected)
159  # Final fit after rejection
160  sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs)
161  wcs = sipObject.getNewWcs()
162  if debug.plot:
163  print("Plotting final fit")
164  self.plotFit(matches, wcs, rejected)
165 
166  if refCat is not None:
167  self.log.debug("Updating centroids in refCat")
168  afwTable.updateRefCentroids(wcs, refList=refCat)
169  else:
170  self.log.warn("Updating reference object centroids in match list; refCat is None")
171  afwTable.updateRefCentroids(wcs, refList=[match.first for match in matches])
172 
173  if sourceCat is not None:
174  self.log.debug("Updating coords in sourceCat")
175  afwTable.updateSourceCoords(wcs, sourceList=sourceCat)
176  else:
177  self.log.warn("Updating source coords in match list; sourceCat is None")
178  afwTable.updateSourceCoords(wcs, sourceList=[match.second for match in matches])
179 
180  self.log.debug("Updating distance in match list")
181  setMatchDistance(matches)
182 
183  scatterOnSky = sipObject.getScatterOnSky()
184 
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))
189 
190  return pipeBase.Struct(
191  wcs=wcs,
192  scatterOnSky=scatterOnSky,
193  )
194 
195  def initialWcs(self, matches, wcs):
196  """Generate a guess Wcs from the astrometric matches
197 
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
202  from the input Wcs.
203  """
204  crpix = afwGeom.Extent2D(0, 0)
205  crval = afwGeom.Extent3D(0, 0, 0)
206  for mm in matches:
207  crpix += afwGeom.Extent2D(mm.second.getCentroid())
208  crval += afwGeom.Extent3D(mm.first.getCoord().toIcrs().getVector())
209  crpix /= len(matches)
210  crval /= len(matches)
211  newWcs = afwGeom.makeSkyWcs(crpix=afwGeom.Point2D(crpix),
212  crval=afwCoord.IcrsCoord(afwGeom.Point3D(crval)),
213  cdMatrix=wcs.getCdMatrix())
214  return newWcs
215 
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):
219  sipObject = makeCreateWcsWithSip(matches, wcs, self.config.order)
220  wcs = sipObject.getNewWcs()
221  return sipObject
222 
223  def rejectMatches(self, matches, wcs, rejected):
224  """Flag deviant matches
225 
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.
229  """
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())
235 
236  def plotFit(self, matches, wcs, rejected):
237  """Plot the fit
238 
239  We create four plots, for all combinations of (dx, dy) against
240  (x, y). Good points are black, while rejected points are red.
241  """
242  try:
243  import matplotlib.pyplot as plt
244  except ImportError as e:
245  self.log.warn("Unable to import matplotlib: %s", e)
246  return
247 
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])
253 
254  dx = x1 - x2
255  dy = y1 - y2
256 
257  good = np.logical_not(rejected)
258 
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')
263  axes.set_xlabel("x")
264  axes.set_ylabel("dx")
265 
266  axes = figure.add_subplot(2, 2, 2)
267  axes.plot(x2[good], dy[good], 'ko')
268  axes.plot(x2[rejected], dy[rejected], 'ro')
269  axes.set_xlabel("x")
270  axes.set_ylabel("dy")
271 
272  axes = figure.add_subplot(2, 2, 3)
273  axes.plot(y2[good], dx[good], 'ko')
274  axes.plot(y2[rejected], dx[rejected], 'ro')
275  axes.set_xlabel("y")
276  axes.set_ylabel("dx")
277 
278  axes = figure.add_subplot(2, 2, 4)
279  axes.plot(y2[good], dy[good], 'ko')
280  axes.plot(y2[rejected], dy[rejected], 'ro')
281  axes.set_xlabel("y")
282  axes.set_ylabel("dy")
283 
284  plt.show()
def plotFit(self, matches, wcs, rejected)
STL namespace.
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")
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, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None)
Fit a TAN-SIP WCS from a list of reference object/source matches.
Fit a TAN-SIP WCS given a list of reference object/source matches.
Definition: fitTanSipWcs.py:62