lsst.meas.astrom  14.0
 All Classes Namespaces Files Functions Variables Typedefs Friends Macros Groups Pages
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.image as afwImage
11 import lsst.afw.coord as afwCoord
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
18 
19 
20 class FitTanSipWcsConfig(pexConfig.Config):
21  order = pexConfig.RangeField(
22  doc="order of SIP polynomial",
23  dtype=int,
24  default=4,
25  min=0,
26  )
27  numIter = pexConfig.RangeField(
28  doc="number of iterations of fitter (which fits X and Y separately, and so benefits from " +
29  "a few iterations",
30  dtype=int,
31  default=3,
32  min=1,
33  )
34  numRejIter = pexConfig.RangeField(
35  doc="number of rejection iterations",
36  dtype=int,
37  default=1,
38  min=0,
39  )
40  rejSigma = pexConfig.RangeField(
41  doc="Number of standard deviations for clipping level",
42  dtype=float,
43  default=3.0,
44  min=0.0,
45  )
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",
49  dtype=float,
50  default=10,
51  min=0,
52  )
53 
54 # The following block adds links to this task from the Task Documentation page.
55 ## \addtogroup LSST_task_documentation
56 ## \{
57 ## \page measAstrom_fitTanSipWcsTask
58 ## \ref FitTanSipWcsTask "FitTanSipWcsTask"
59 ## Fit a TAN-SIP WCS given a list of reference object/source matches
60 ## \}
61 
62 
63 class FitTanSipWcsTask(pipeBase.Task):
64  """!Fit a TAN-SIP WCS given a list of reference object/source matches
65 
66  @anchor FitTanSipWcsTask_
67 
68  @section meas_astrom_fitTanSipWcs_Contents Contents
69 
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
77 
78  @section meas_astrom_fitTanSipWcs_Purpose Description
79 
80  Fit a TAN-SIP WCS given a list of reference object/source matches.
81  See CreateWithSip.h for information about the fitting algorithm.
82 
83  @section meas_astrom_fitTanSipWcs_Initialize Task initialisation
84 
85  @copydoc \_\_init\_\_
86 
87  @section meas_astrom_fitTanSipWcs_IO Invoking the Task
88 
89  @copydoc fitWcs
90 
91  @section meas_astrom_fitTanSipWcs_Config Configuration parameters
92 
93  See @ref FitTanSipWcsConfig
94 
95  @section meas_astrom_fitTanSipWcs_Example A complete example of using FitTanSipWcsTask
96 
97  FitTanSipWcsTask is a subtask of AstrometryTask, which is called by PhotoCalTask.
98  See \ref pipe_tasks_photocal_Example.
99 
100  @section meas_astrom_fitTanSipWcs_Debug Debug variables
101 
102  FitTanSipWcsTask does not support any debug variables.
103  """
104  ConfigClass = FitTanSipWcsConfig
105  _DefaultName = "fitWcs"
106 
107  @pipeBase.timeMethod
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
110 
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.
131 
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
136  """
137  if bbox is None:
138  bbox = afwGeom.Box2I()
139 
140  import lsstDebug
141  debug = lsstDebug.Info(__name__)
142 
143  wcs = self.initialWcs(matches, initWcs)
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()
148  rejected = self.rejectMatches(matches, wcs, rejected)
149  if rejected.sum() == len(rejected):
150  raise RuntimeError("All matches rejected in iteration %d" % (rej + 1,))
151  self.log.debug(
152  "Iteration {0} of astrometry fitting: rejected {1} outliers, "
153  "out of {2} total matches.".format(
154  rej, rejected.sum(), len(rejected)
155  )
156  )
157  if debug.plot:
158  print("Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
159  self.plotFit(matches, wcs, rejected)
160  # Final fit after rejection
161  sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs)
162  wcs = sipObject.getNewWcs()
163  if debug.plot:
164  print("Plotting final fit")
165  self.plotFit(matches, wcs, rejected)
166 
167  if refCat is not None:
168  self.log.debug("Updating centroids in refCat")
169  afwTable.updateRefCentroids(wcs, refList=refCat)
170  else:
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])
173 
174  if sourceCat is not None:
175  self.log.debug("Updating coords in sourceCat")
176  afwTable.updateSourceCoords(wcs, sourceList=sourceCat)
177  else:
178  self.log.warn("Updating source coords in match list; sourceCat is None")
179  afwTable.updateSourceCoords(wcs, sourceList=[match.second for match in matches])
180 
181  self.log.debug("Updating distance in match list")
182  setMatchDistance(matches)
183 
184  scatterOnSky = sipObject.getScatterOnSky()
185 
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))
190 
191  return pipeBase.Struct(
192  wcs=wcs,
193  scatterOnSky=scatterOnSky,
194  )
195 
196  def initialWcs(self, matches, wcs):
197  """Generate a guess Wcs from the astrometric matches
198 
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
203  from the input Wcs.
204  """
205  crpix = afwGeom.Extent2D(0, 0)
206  crval = afwGeom.Extent3D(0, 0, 0)
207  for mm in matches:
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())
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 fitWcs
Fit a TAN-SIP WCS from a list of reference object/source matches.
STL namespace.
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.
Definition: fitTanSipWcs.py:63