LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
LSSTDataManagementBasePackage
fitTanSipWcs.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 from builtins import zip
3 from builtins import range
4 
5 import numpy as np
6 
7 import lsst.afw.image as afwImage
8 import lsst.afw.coord as afwCoord
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 __all__ = ["FitTanSipWcsTask", "FitTanSipWcsConfig"]
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 ## \addtogroup LSST_task_documentation
55 ## \{
56 ## \page measAstrom_fitTanSipWcsTask
57 ## \ref FitTanSipWcsTask "FitTanSipWcsTask"
58 ## Fit a TAN-SIP WCS given a list of reference object/source matches
59 ## \}
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 meas_photocal_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):
108  """!Fit a TAN-SIP WCS from a list of reference object/source matches
109 
110  @param[in,out] matches a list of reference object/source matches
111  (an lsst::afw::table::ReferenceMatchVector)
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 
131  @return an lsst.pipe.base.Struct with the following fields:
132  - wcs the fit WCS as an lsst.afw.image.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  if debug.plot:
151  print("Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
152  self.plotFit(matches, wcs, rejected)
153  # Final fit after rejection
154  sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs)
155  wcs = sipObject.getNewWcs()
156  if debug.plot:
157  print("Plotting final fit")
158  self.plotFit(matches, wcs, rejected)
159 
160  if refCat is not None:
161  self.log.debug("Updating centroids in refCat")
162  afwTable.updateRefCentroids(wcs, refList=refCat)
163  else:
164  self.log.warn("Updating reference object centroids in match list; refCat is None")
165  afwTable.updateRefCentroids(wcs, refList=[match.first for match in matches])
166 
167  if sourceCat is not None:
168  self.log.debug("Updating coords in sourceCat")
169  afwTable.updateSourceCoords(wcs, sourceList=sourceCat)
170  else:
171  self.log.warn("Updating source coords in match list; sourceCat is None")
172  afwTable.updateSourceCoords(wcs, sourceList=[match.second for match in matches])
173 
174  self.log.debug("Updating distance in match list")
175  setMatchDistance(matches)
176 
177  scatterOnSky = sipObject.getScatterOnSky()
178 
179  if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
180  raise pipeBase.TaskError(
181  "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
182  (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
183 
184  return pipeBase.Struct(
185  wcs=wcs,
186  scatterOnSky=scatterOnSky,
187  )
188 
189  def initialWcs(self, matches, wcs):
190  """Generate a guess Wcs from the astrometric matches
191 
192  We create a Wcs anchored at the center of the matches, with the scale
193  of the input Wcs. This is necessary because matching returns only
194  matches with no estimated Wcs, and the input Wcs is a wild guess.
195  We're using the best of each: positions from the matches, and scale
196  from the input Wcs.
197  """
198  crpix = afwGeom.Extent2D(0, 0)
199  crval = afwGeom.Extent3D(0, 0, 0)
200  for mm in matches:
201  crpix += afwGeom.Extent2D(mm.second.getCentroid())
202  crval += afwGeom.Extent3D(mm.first.getCoord().toIcrs().getVector())
203  crpix /= len(matches)
204  crval /= len(matches)
205  newWcs = afwImage.Wcs(afwCoord.IcrsCoord(afwGeom.Point3D(crval)).getPosition(),
206  afwGeom.Point2D(crpix), wcs.getCDMatrix())
207  return newWcs
208 
209  def _fitWcs(self, matches, wcs):
210  """Fit a Wcs based on the matches and a guess Wcs"""
211  for i in range(self.config.numIter):
212  sipObject = makeCreateWcsWithSip(matches, wcs, self.config.order)
213  wcs = sipObject.getNewWcs()
214  return sipObject
215 
216  def rejectMatches(self, matches, wcs, rejected):
217  """Flag deviant matches
218 
219  We return a boolean numpy array indicating whether the corresponding
220  match should be rejected. The previous list of rejections is used
221  so we can calculate uncontaminated statistics.
222  """
223  fit = [wcs.skyToPixel(m.first.getCoord()) for m in matches]
224  dx = np.array([ff.getX() - mm.second.getCentroid().getX() for ff, mm in zip(fit, matches)])
225  dy = np.array([ff.getY() - mm.second.getCentroid().getY() for ff, mm in zip(fit, matches)])
226  good = np.logical_not(rejected)
227  return (dx > self.config.rejSigma*dx[good].std()) | (dy > self.config.rejSigma*dy[good].std())
228 
229  def plotFit(self, matches, wcs, rejected):
230  """Plot the fit
231 
232  We create four plots, for all combinations of (dx, dy) against
233  (x, y). Good points are black, while rejected points are red.
234  """
235  import matplotlib.pyplot as plt
236 
237  fit = [wcs.skyToPixel(m.first.getCoord()) for m in matches]
238  x1 = np.array([ff.getX() for ff in fit])
239  y1 = np.array([ff.getY() for ff in fit])
240  x2 = np.array([m.second.getCentroid().getX() for m in matches])
241  y2 = np.array([m.second.getCentroid().getY() for m in matches])
242 
243  dx = x1 - x2
244  dy = y1 - y2
245 
246  good = np.logical_not(rejected)
247 
248  figure = plt.figure()
249  axes = figure.add_subplot(2, 2, 1)
250  axes.plot(x2[good], dx[good], 'ko')
251  axes.plot(x2[rejected], dx[rejected], 'ro')
252  axes.set_xlabel("x")
253  axes.set_ylabel("dx")
254 
255  axes = figure.add_subplot(2, 2, 2)
256  axes.plot(x2[good], dy[good], 'ko')
257  axes.plot(x2[rejected], dy[rejected], 'ro')
258  axes.set_xlabel("x")
259  axes.set_ylabel("dy")
260 
261  axes = figure.add_subplot(2, 2, 3)
262  axes.plot(y2[good], dx[good], 'ko')
263  axes.plot(y2[rejected], dx[rejected], 'ro')
264  axes.set_xlabel("y")
265  axes.set_ylabel("dx")
266 
267  axes = figure.add_subplot(2, 2, 4)
268  axes.plot(y2[good], dy[good], 'ko')
269  axes.plot(y2[rejected], dy[rejected], 'ro')
270  axes.set_xlabel("y")
271  axes.set_ylabel("dy")
272 
273  plt.show()
def fitWcs
Fit a TAN-SIP WCS from a list of reference object/source matches.
STL namespace.
Implementation of the WCS standard for a any projection.
Definition: Wcs.h:107
A coordinate class intended to represent absolute positions.
Definition: PSF.h:39
An integer coordinate rectangle.
Definition: Box.h:53
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:62
A class to handle Icrs coordinates (inherits from Coord)
Definition: Coord.h:156