lsst.meas.astrom  14.0-8-g1eca518+4
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.geom as afwGeom
11 import lsst.afw.table as afwTable
12 import lsst.pex.config as pexConfig
13 import lsst.pipe.base as pipeBase
14 from .setMatchDistance import setMatchDistance
15 from .sip import makeCreateWcsWithSip
16 
17 
18 class FitTanSipWcsConfig(pexConfig.Config):
19  order = pexConfig.RangeField(
20  doc="order of SIP polynomial",
21  dtype=int,
22  default=4,
23  min=0,
24  )
25  numIter = pexConfig.RangeField(
26  doc="number of iterations of fitter (which fits X and Y separately, and so benefits from " +
27  "a few iterations",
28  dtype=int,
29  default=3,
30  min=1,
31  )
32  numRejIter = pexConfig.RangeField(
33  doc="number of rejection iterations",
34  dtype=int,
35  default=1,
36  min=0,
37  )
38  rejSigma = pexConfig.RangeField(
39  doc="Number of standard deviations for clipping level",
40  dtype=float,
41  default=3.0,
42  min=0.0,
43  )
44  maxScatterArcsec = pexConfig.RangeField(
45  doc="maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " +
46  "be generous, as this is only intended to catch catastrophic failures",
47  dtype=float,
48  default=10,
49  min=0,
50  )
51 
52 # The following block adds links to this task from the Task Documentation page.
53 
59 
60 
61 class FitTanSipWcsTask(pipeBase.Task):
62  """!Fit a TAN-SIP WCS given a list of reference object/source matches
63 
64  @anchor FitTanSipWcsTask_
65 
66  @section meas_astrom_fitTanSipWcs_Contents Contents
67 
68  - @ref meas_astrom_fitTanSipWcs_Purpose
69  - @ref meas_astrom_fitTanSipWcs_Initialize
70  - @ref meas_astrom_fitTanSipWcs_IO
71  - @ref meas_astrom_fitTanSipWcs_Schema
72  - @ref meas_astrom_fitTanSipWcs_Config
73  - @ref meas_astrom_fitTanSipWcs_Example
74  - @ref meas_astrom_fitTanSipWcs_Debug
75 
76  @section meas_astrom_fitTanSipWcs_Purpose Description
77 
78  Fit a TAN-SIP WCS given a list of reference object/source matches.
79  See CreateWithSip.h for information about the fitting algorithm.
80 
81  @section meas_astrom_fitTanSipWcs_Initialize Task initialisation
82 
83  @copydoc \_\_init\_\_
84 
85  @section meas_astrom_fitTanSipWcs_IO Invoking the Task
86 
87  @copydoc fitWcs
88 
89  @section meas_astrom_fitTanSipWcs_Config Configuration parameters
90 
91  See @ref FitTanSipWcsConfig
92 
93  @section meas_astrom_fitTanSipWcs_Example A complete example of using FitTanSipWcsTask
94 
95  FitTanSipWcsTask is a subtask of AstrometryTask, which is called by PhotoCalTask.
96  See \ref pipe_tasks_photocal_Example.
97 
98  @section meas_astrom_fitTanSipWcs_Debug Debug variables
99 
100  FitTanSipWcsTask does not support any debug variables.
101  """
102  ConfigClass = FitTanSipWcsConfig
103  _DefaultName = "fitWcs"
104 
105  @pipeBase.timeMethod
106  def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
107  """!Fit a TAN-SIP WCS from a list of reference object/source matches
108 
109  @param[in,out] matches a list of lsst::afw::table::ReferenceMatch
110  The following fields are read:
111  - match.first (reference object) coord
112  - match.second (source) centroid
113  The following fields are written:
114  - match.first (reference object) centroid,
115  - match.second (source) centroid
116  - match.distance (on sky separation, in radians)
117  @param[in] initWcs initial WCS
118  @param[in] bbox the region over which the WCS will be valid (an lsst:afw::geom::Box2I);
119  if None or an empty box then computed from matches
120  @param[in,out] refCat reference object catalog, or None.
121  If provided then all centroids are updated with the new WCS,
122  otherwise only the centroids for ref objects in matches are updated.
123  Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec".
124  @param[in,out] sourceCat source catalog, or None.
125  If provided then coords are updated with the new WCS;
126  otherwise only the coords for sources in matches are updated.
127  Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec".
128  @param[in] exposure Ignored; present for consistency with FitSipDistortionTask.
129 
130  @return an lsst.pipe.base.Struct with the following fields:
131  - wcs the fit WCS as an lsst.afw.geom.Wcs
132  - scatterOnSky median on-sky separation between reference objects and sources in "matches",
133  as an lsst.afw.geom.Angle
134  """
135  if bbox is None:
136  bbox = afwGeom.Box2I()
137 
138  import lsstDebug
139  debug = lsstDebug.Info(__name__)
140 
141  wcs = self.initialWcs(matches, initWcs)
142  rejected = np.zeros(len(matches), dtype=bool)
143  for rej in range(self.config.numRejIter):
144  sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs)
145  wcs = sipObject.getNewWcs()
146  rejected = self.rejectMatches(matches, wcs, rejected)
147  if rejected.sum() == len(rejected):
148  raise RuntimeError("All matches rejected in iteration %d" % (rej + 1,))
149  self.log.debug(
150  "Iteration {0} of astrometry fitting: rejected {1} outliers, "
151  "out of {2} total matches.".format(
152  rej, rejected.sum(), len(rejected)
153  )
154  )
155  if debug.plot:
156  print("Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
157  self.plotFit(matches, wcs, rejected)
158  # Final fit after rejection
159  sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs)
160  wcs = sipObject.getNewWcs()
161  if debug.plot:
162  print("Plotting final fit")
163  self.plotFit(matches, wcs, rejected)
164 
165  if refCat is not None:
166  self.log.debug("Updating centroids in refCat")
167  afwTable.updateRefCentroids(wcs, refList=refCat)
168  else:
169  self.log.warn("Updating reference object centroids in match list; refCat is None")
170  afwTable.updateRefCentroids(wcs, refList=[match.first for match in matches])
171 
172  if sourceCat is not None:
173  self.log.debug("Updating coords in sourceCat")
174  afwTable.updateSourceCoords(wcs, sourceList=sourceCat)
175  else:
176  self.log.warn("Updating source coords in match list; sourceCat is None")
177  afwTable.updateSourceCoords(wcs, sourceList=[match.second for match in matches])
178 
179  self.log.debug("Updating distance in match list")
180  setMatchDistance(matches)
181 
182  scatterOnSky = sipObject.getScatterOnSky()
183 
184  if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
185  raise pipeBase.TaskError(
186  "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
187  (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
188 
189  return pipeBase.Struct(
190  wcs=wcs,
191  scatterOnSky=scatterOnSky,
192  )
193 
194  def initialWcs(self, matches, wcs):
195  """Generate a guess Wcs from the astrometric matches
196 
197  We create a Wcs anchored at the center of the matches, with the scale
198  of the input Wcs. This is necessary because matching returns only
199  matches with no estimated Wcs, and the input Wcs is a wild guess.
200  We're using the best of each: positions from the matches, and scale
201  from the input Wcs.
202  """
203  crpix = afwGeom.Extent2D(0, 0)
204  crval = afwGeom.Extent3D(0, 0, 0)
205  for mm in matches:
206  crpix += afwGeom.Extent2D(mm.second.getCentroid())
207  crval += afwGeom.Extent3D(mm.first.getCoord().getVector())
208  crpix /= len(matches)
209  crval /= len(matches)
210  newWcs = afwGeom.makeSkyWcs(crpix=afwGeom.Point2D(crpix),
211  crval=afwGeom.SpherePoint(afwGeom.Point3D(crval)),
212  cdMatrix=wcs.getCdMatrix())
213  return newWcs
214 
215  def _fitWcs(self, matches, wcs):
216  """Fit a Wcs based on the matches and a guess Wcs"""
217  for i in range(self.config.numIter):
218  sipObject = makeCreateWcsWithSip(matches, wcs, self.config.order)
219  wcs = sipObject.getNewWcs()
220  return sipObject
221 
222  def rejectMatches(self, matches, wcs, rejected):
223  """Flag deviant matches
224 
225  We return a boolean numpy array indicating whether the corresponding
226  match should be rejected. The previous list of rejections is used
227  so we can calculate uncontaminated statistics.
228  """
229  fit = [wcs.skyToPixel(m.first.getCoord()) for m in matches]
230  dx = np.array([ff.getX() - mm.second.getCentroid().getX() for ff, mm in zip(fit, matches)])
231  dy = np.array([ff.getY() - mm.second.getCentroid().getY() for ff, mm in zip(fit, matches)])
232  good = np.logical_not(rejected)
233  return (dx > self.config.rejSigma*dx[good].std()) | (dy > self.config.rejSigma*dy[good].std())
234 
235  def plotFit(self, matches, wcs, rejected):
236  """Plot the fit
237 
238  We create four plots, for all combinations of (dx, dy) against
239  (x, y). Good points are black, while rejected points are red.
240  """
241  try:
242  import matplotlib.pyplot as plt
243  except ImportError as e:
244  self.log.warn("Unable to import matplotlib: %s", e)
245  return
246 
247  fit = [wcs.skyToPixel(m.first.getCoord()) for m in matches]
248  x1 = np.array([ff.getX() for ff in fit])
249  y1 = np.array([ff.getY() for ff in fit])
250  x2 = np.array([m.second.getCentroid().getX() for m in matches])
251  y2 = np.array([m.second.getCentroid().getY() for m in matches])
252 
253  dx = x1 - x2
254  dy = y1 - y2
255 
256  good = np.logical_not(rejected)
257 
258  figure = plt.figure()
259  axes = figure.add_subplot(2, 2, 1)
260  axes.plot(x2[good], dx[good], 'ko')
261  axes.plot(x2[rejected], dx[rejected], 'ro')
262  axes.set_xlabel("x")
263  axes.set_ylabel("dx")
264 
265  axes = figure.add_subplot(2, 2, 2)
266  axes.plot(x2[good], dy[good], 'ko')
267  axes.plot(x2[rejected], dy[rejected], 'ro')
268  axes.set_xlabel("x")
269  axes.set_ylabel("dy")
270 
271  axes = figure.add_subplot(2, 2, 3)
272  axes.plot(y2[good], dx[good], 'ko')
273  axes.plot(y2[rejected], dx[rejected], 'ro')
274  axes.set_xlabel("y")
275  axes.set_ylabel("dx")
276 
277  axes = figure.add_subplot(2, 2, 4)
278  axes.plot(y2[good], dy[good], 'ko')
279  axes.plot(y2[rejected], dy[rejected], 'ro')
280  axes.set_xlabel("y")
281  axes.set_ylabel("dy")
282 
283  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.
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
def rejectMatches(self, matches, wcs, rejected)
std::shared_ptr< SkyWcs > makeSkyWcs(Point2D const &crpix, SpherePoint const &crval, Eigen::Matrix2d const &cdMatrix, std::string const &projection="TAN")
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:61