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