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