LSSTApplications  10.0+286,10.0+36,10.0+46,10.0-2-g4f67435,10.1+152,10.1+37,11.0,11.0+1,11.0-1-g47edd16,11.0-1-g60db491,11.0-1-g7418c06,11.0-2-g04d2804,11.0-2-g68503cd,11.0-2-g818369d,11.0-2-gb8b8ce7
LSSTDataManagementBasePackage
astrometry.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 from lsst.daf.base import PropertyList
4 from lsst.afw.image.utils import getDistortedWcs
5 import lsst.afw.geom as afwGeom
6 import lsst.afw.math as afwMath
7 import lsst.pex.config as pexConfig
8 import lsst.pipe.base as pipeBase
9 from .loadAstrometryNetObjects import LoadAstrometryNetObjectsTask
10 from .matchOptimisticB import MatchOptimisticBTask
11 from .fitTanSipWcs import FitTanSipWcsTask
12 from .display import displayAstrometry
13 from .astromLib import makeMatchStatistics
14 
15 class AstrometryConfig(pexConfig.Config):
16  refObjLoader = pexConfig.ConfigurableField(
17  target = LoadAstrometryNetObjectsTask,
18  doc = "reference object loader",
19  )
20  matcher = pexConfig.ConfigurableField(
21  target = MatchOptimisticBTask,
22  doc = "reference object/source matcher",
23  )
24  wcsFitter = pexConfig.ConfigurableField(
25  target = FitTanSipWcsTask,
26  doc = "WCS fitter",
27  )
28  forceKnownWcs = pexConfig.Field(
29  dtype = bool,
30  doc= "If True then load reference objects and match sources but do not fit a WCS; " +
31  " this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
32  default = False,
33  )
34  maxIter = pexConfig.RangeField(
35  doc = "maximum number of iterations of match sources and fit WCS" +
36  "ignored if not fitting a WCS",
37  dtype = int,
38  default = 3,
39  min = 1,
40  )
41  matchDistanceSigma = pexConfig.RangeField(
42  doc = "the maximum match distance is set to "
43  " mean_match_distance + matchDistanceSigma*std_dev_match_distance; " +
44  "ignored if not fitting a WCS",
45  dtype = float,
46  default = 2,
47  min = 0,
48  )
49  minMatchDistanceArcSec = pexConfig.RangeField(
50  doc = "the match distance below which further iteration is pointless (arcsec); "
51  "ignored if not fitting a WCS",
52  dtype = float,
53  default = 0.001,
54  min = 0,
55  )
56 
57 # The following block adds links to this task from the Task Documentation page.
58 ## \addtogroup LSST_task_documentation
59 ## \{
60 ## \page measAstrom_astrometryTask
61 ## \ref AstrometryTask_ "AstrometryTask"
62 ## Match an input source catalog with objects from a reference catalog and solve for the WCS
63 ## \}
64 
65 class AstrometryTask(pipeBase.Task):
66  """!Match an input source catalog with objects from a reference catalog and solve for the WCS
67 
68  @anchor AstrometryTask_
69 
70  @section meas_astrom_astrometry_Contents Contents
71 
72  - @ref meas_astrom_astrometry_Purpose
73  - @ref meas_astrom_astrometry_Initialize
74  - @ref meas_astrom_astrometry_IO
75  - @ref meas_astrom_astrometry_Config
76  - @ref meas_astrom_astrometry_Example
77  - @ref meas_astrom_astrometry_Debug
78 
79  @section meas_astrom_astrometry_Purpose Description
80 
81  Match input sourceCat with a reference catalog and solve for the Wcs
82 
83  There are three steps, each performed by different subtasks:
84  - Find position reference stars that overlap the exposure
85  - Match sourceCat to position reference stars
86  - Fit a WCS based on the matches
87 
88  @section meas_astrom_astrometry_Initialize Task initialisation
89 
90  @copydoc \_\_init\_\_
91 
92  @section meas_astrom_astrometry_IO Invoking the Task
93 
94  @copydoc run
95 
96  @copydoc loadAndMatch
97 
98  @section meas_astrom_astrometry_Config Configuration parameters
99 
100  See @ref AstrometryConfig
101 
102  @section meas_astrom_astrometry_Example A complete example of using AstrometryTask
103 
104  See \ref meas_photocal_photocal_Example.
105 
106  @section meas_astrom_astrometry_Debug Debug variables
107 
108  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
109  flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
110  @b debug.py files.
111 
112  The available variables in AstrometryTask are:
113  <DL>
114  <DT> @c display (bool)
115  <DD> If True display information at three stages: after finding reference objects,
116  after matching sources to reference objects, and after fitting the WCS; defaults to False
117  <DT> @c frame (int)
118  <DD> ds9 frame to use to display the reference objects; the next two frames are used
119  to display the match list and the results of the final WCS; defaults to 0
120  </DL>
121 
122  To investigate the @ref meas_astrom_astrometry_Debug, put something like
123  @code{.py}
124  import lsstDebug
125  def DebugInfo(name):
126  debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
127  if name == "lsst.meas.astrom.astrometry":
128  debug.display = True
129 
130  return debug
131 
132  lsstDebug.Info = DebugInfo
133  @endcode
134  into your debug.py file and run this task with the @c --debug flag.
135  """
136  ConfigClass = AstrometryConfig
137  _DefaultName = "astrometricSolver"
138 
139  def __init__(self, schema=None, **kwargs):
140  """!Construct an AstrometryTask
141 
142  @param[in] schema ignored; available for compatibility with an older astrometry task
143  @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_
144  """
145  pipeBase.Task.__init__(self, **kwargs)
146  self.makeSubtask("refObjLoader")
147  self.makeSubtask("matcher")
148  self.makeSubtask("wcsFitter")
149 
150  @pipeBase.timeMethod
151  def run(self, exposure, sourceCat):
152  """!Load reference objects, match sources and optionally fit a WCS
153 
154  This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs
155 
156  @param[in,out] exposure exposure whose WCS is to be fit
157  The following are read only:
158  - bbox
159  - calib (may be absent)
160  - filter (may be unset)
161  - detector (if wcs is pure tangent; may be absent)
162  The following are updated:
163  - wcs (the initial value is used as an initial guess, and is required)
164  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
165  @return an lsst.pipe.base.Struct with these fields:
166  - refCat reference object catalog of objects that overlap the exposure (with some margin)
167  (an lsst::afw::table::SimpleCatalog)
168  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
169  - scatterOnSky median on-sky separation between reference objects and sources in "matches"
170  (an lsst.afw.geom.Angle), or None if config.forceKnownWcs True
171  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
172  """
173  if self.config.forceKnownWcs:
174  res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
175  res.scatterOnSky = None
176  else:
177  res = self.solve(exposure=exposure, sourceCat=sourceCat)
178  return res
179 
180  @pipeBase.timeMethod
181  def loadAndMatch(self, exposure, sourceCat):
182  """!Load reference objects overlapping an exposure and match to sources detected on that exposure
183 
184  @param[in] exposure exposure whose WCS is to be fit
185  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
186 
187  @return an lsst.pipe.base.Struct with these fields:
188  - refCat reference object catalog of objects that overlap the exposure (with some margin)
189  (an lsst::afw::table::SimpleCatalog)
190  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
191  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
192 
193  @note ignores config.forceKnownWcs, config.maxIter, config.matchDistanceSigma
194  and config.minMatchDistanceArcSec
195  """
196  import lsstDebug
197  debug = lsstDebug.Info(__name__)
198 
199  expMd = self._getExposureMetadata(exposure)
200 
201  loadRes = self.refObjLoader.loadPixelBox(
202  bbox = expMd.bbox,
203  wcs = expMd.wcs,
204  filterName = expMd.filterName,
205  calib = expMd.calib,
206  )
207 
208  matchRes = self.matcher.matchObjectsToSources(
209  refCat = loadRes.refCat,
210  sourceCat = sourceCat,
211  wcs = expMd.wcs,
212  refFluxField = loadRes.fluxField,
213  maxMatchDist = None,
214  )
215 
216  distStats = self._computeMatchStatsOnSky(matchRes.matches)
217  self.log.info(
218  "Found %d matches with scatter = %0.3f +- %0.3f arcsec; " %
219  (len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds())
220  )
221 
222  if debug.display:
223  frame = int(debug.frame)
225  refCat = loadRes.refCat,
226  sourceCat = sourceCat,
227  matches = matchRes.matches,
228  exposure = exposure,
229  bbox = expMd.bbox,
230  frame = frame,
231  title="Matches",
232  )
233 
234  return pipeBase.Struct(
235  refCat = loadRes.refCat,
236  matches = matchRes.matches,
237  matchMeta = self._createMatchMetadata(bbox=expMd.bbox, wcs=expMd.wcs, filterName=expMd.filterName),
238  )
239 
240  @pipeBase.timeMethod
241  def solve(self, exposure, sourceCat):
242  """!Load reference objects overlapping an exposure, match to sources and fit a WCS
243 
244  @return an lsst.pipe.base.Struct with these fields:
245  - refCat reference object catalog of objects that overlap the exposure (with some margin)
246  (an lsst::afw::table::SimpleCatalog)
247  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
248  - scatterOnSky median on-sky separation between reference objects and sources in "matches"
249  (an lsst.afw.geom.Angle)
250  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
251 
252  @note ignores config.forceKnownWcs
253  """
254  import lsstDebug
255  debug = lsstDebug.Info(__name__)
256 
257  expMd = self._getExposureMetadata(exposure)
258 
259  loadRes = self.refObjLoader.loadPixelBox(
260  bbox = expMd.bbox,
261  wcs = expMd.wcs,
262  filterName = expMd.filterName,
263  calib = expMd.calib,
264  )
265  if debug.display:
266  frame = int(debug.frame)
268  refCat = loadRes.refCat,
269  sourceCat = sourceCat,
270  exposure = exposure,
271  bbox = expMd.bbox,
272  frame = frame,
273  title="Reference catalog",
274  )
275 
276  res = None
277  wcs = expMd.wcs
278  maxMatchDist = None
279  for i in range(self.config.maxIter):
280  iterNum = i + 1
281  try:
282  tryRes = self._matchAndFitWcs( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None
283  refCat = loadRes.refCat,
284  sourceCat = sourceCat,
285  refFluxField = loadRes.fluxField,
286  bbox = expMd.bbox,
287  wcs = wcs,
288  exposure = exposure,
289  maxMatchDist = maxMatchDist,
290  )
291  except Exception as e:
292  # if we have had a succeessful iteration then use that; otherwise fail
293  if i > 0:
294  self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
295  iterNum -= 1
296  break
297  else:
298  raise
299 
300  tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
301  self.log.logdebug(
302  "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
303  "max match distance = %0.3f arcsec" %
304  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
305  tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds()))
306  if maxMatchDist is not None:
307  if tryMatchDist.maxMatchDist >= maxMatchDist:
308  self.log.logdebug(
309  "Iteration %d had no better maxMatchDist; using previous iteration" % (iterNum,))
310  iterNum -= 1
311  break
312 
313  maxMatchDist = tryMatchDist.maxMatchDist
314  res = tryRes
315  wcs = res.wcs
316  if tryMatchDist.maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
317  self.log.logdebug(
318  "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
319  "that's good enough" %
320  (tryMatchDist.maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec))
321  break
322 
323  self.log.info(
324  "Matched and fit WCS in %d iterations; "
325  "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
326  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
327  tryMatchDist.distStdDev.asArcseconds()))
328 
329  exposure.setWcs(res.wcs)
330 
331  return pipeBase.Struct(
332  refCat = loadRes.refCat,
333  matches = res.matches,
334  scatterOnSky = res.scatterOnSky,
335  matchMeta = self._createMatchMetadata(bbox=expMd.bbox, wcs=res.wcs, filterName=expMd.filterName)
336  )
337 
338  def _computeMatchStatsOnSky(self, matchList):
339  """Compute on-sky radial distance statistics for a match list
340 
341  @param[in] matchList list of matches between reference object and sources;
342  the distance field is the only field read and it must be set to distance in radians
343 
344  @return a pipe_base Struct containing these fields:
345  - distMean clipped mean of on-sky radial separation
346  - distStdDev clipped standard deviation of on-sky radial separation
347  - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev
348  """
349  distStatsInRadians = makeMatchStatistics(matchList, afwMath.MEANCLIP | afwMath.STDEVCLIP)
350  distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*afwGeom.radians
351  distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*afwGeom.radians
352  return pipeBase.Struct(
353  distMean = distMean,
354  distStdDev = distStdDev,
355  maxMatchDist = distMean + self.config.matchDistanceSigma*distStdDev,
356  )
357  def _getExposureMetadata(self, exposure):
358  """!Extract metadata from an exposure
359 
360  @return an lsst.pipe.base.Struct containing the following exposure metadata:
361  - bbox: parent bounding box
362  - wcs: WCS (an lsst.afw.image.Wcs)
363  - calib calibration (an lsst.afw.image.Calib), or None if unknown
364  - filterName: name of filter, or None if unknown
365  """
366  exposureInfo = exposure.getInfo()
367  filterName = exposureInfo.getFilter().getName() or None
368  if filterName == "_unknown_":
369  filterName = None
370  return pipeBase.Struct(
371  bbox = exposure.getBBox(),
372  wcs = getDistortedWcs(exposureInfo, log=self.log),
373  calib = exposureInfo.getCalib() if exposureInfo.hasCalib() else None,
374  filterName = filterName,
375  )
376 
377  @pipeBase.timeMethod
378  def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, maxMatchDist=None,
379  exposure=None):
380  """!Match sources to reference objects and fit a WCS
381 
382  @param[in] refCat catalog of reference objects
383  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
384  @param[in] refFluxField field of refCat to use for flux
385  @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
386  @param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs)
387  @param[in] maxMatchDist maximum on-sky distance between reference objects and sources
388  (an lsst.afw.geom.Angle); if None then use the matcher's default
389  @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
390 
391  @return an lsst.pipe.base.Struct with these fields:
392  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
393  - wcs the fit WCS (an lsst.afw.image.Wcs)
394  - scatterOnSky median on-sky separation between reference objects and sources in "matches"
395  (an lsst.afw.geom.Angle)
396  """
397  import lsstDebug
398  debug = lsstDebug.Info(__name__)
399  matchRes = self.matcher.matchObjectsToSources(
400  refCat = refCat,
401  sourceCat = sourceCat,
402  wcs = wcs,
403  refFluxField = refFluxField,
404  maxMatchDist = maxMatchDist,
405  )
406  self.log.logdebug("Found %s matches" % (len(matchRes.matches),))
407  if debug.display:
408  frame = int(debug.frame)
410  refCat = refCat,
411  sourceCat = matchRes.usableSourceCat,
412  matches = matchRes.matches,
413  exposure = exposure,
414  bbox = bbox,
415  frame = frame + 1,
416  title="Initial WCS",
417  )
418 
419  self.log.logdebug("Fitting WCS")
420  fitRes = self.wcsFitter.fitWcs(
421  matches = matchRes.matches,
422  initWcs = wcs,
423  bbox = bbox,
424  refCat = refCat,
425  sourceCat = sourceCat,
426  )
427  fitWcs = fitRes.wcs
428  scatterOnSky = fitRes.scatterOnSky
429  if debug.display:
430  frame = int(debug.frame)
432  refCat = refCat,
433  sourceCat = matchRes.usableSourceCat,
434  matches = matchRes.matches,
435  exposure = exposure,
436  bbox = bbox,
437  frame = frame + 2,
438  title="Fit TAN-SIP WCS",
439  )
440 
441  return pipeBase.Struct(
442  matches = matchRes.matches,
443  wcs = fitWcs,
444  scatterOnSky = scatterOnSky,
445  )
446 
447  @staticmethod
448  def _createMatchMetadata(bbox, wcs, filterName):
449  """Create matchMeta metadata required for regenerating the catalog
450 
451  This is copied from Astrom and I'm not sure why it is needed.
452 
453  @param bbox bounding box of exposure (an lsst.afw.geom.Box2I or Box2D)
454  @param wcs WCS of exposure
455  @param filterName Name of filter, used for magnitudes
456  @return metadata about the field (a daf_base PropertyList)
457  """
458  matchMeta = PropertyList()
459  bboxd = afwGeom.Box2D(bbox)
460  ctrPos = bboxd.getCenter()
461  ctrCoord = wcs.pixelToSky(ctrPos).toIcrs()
462  llCoord = wcs.pixelToSky(bboxd.getMin())
463  approxRadius = ctrCoord.angularSeparation(llCoord)
464  matchMeta.add('RA', ctrCoord.getRa().asDegrees(), 'field center in degrees')
465  matchMeta.add('DEC', ctrCoord.getDec().asDegrees(), 'field center in degrees')
466  matchMeta.add('RADIUS', approxRadius.asDegrees(), 'field radius in degrees, approximate')
467  matchMeta.add('SMATCHV', 1, 'SourceMatchVector version number')
468  if filterName is not None:
469  matchMeta.add('FILTER', filterName, 'filter name for tagalong data')
470  return matchMeta
afw::math::Statistics makeMatchStatistics(std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
def _matchAndFitWcs
Match sources to reference objects and fit a WCS.
Definition: astrometry.py:379
Class for storing ordered metadata with comments.
Definition: PropertyList.h:81
def _getExposureMetadata
Extract metadata from an exposure.
Definition: astrometry.py:357
def __init__
Construct an AstrometryTask.
Definition: astrometry.py:139
def getDistortedWcs
Get a WCS from an exposureInfo, with distortion terms if possible.
Definition: utils.py:46
def run
Load reference objects, match sources and optionally fit a WCS.
Definition: astrometry.py:151
def solve
Load reference objects overlapping an exposure, match to sources and fit a WCS.
Definition: astrometry.py:241
A floating-point coordinate rectangle geometry.
Definition: Box.h:271
Match an input source catalog with objects from a reference catalog and solve for the WCS...
Definition: astrometry.py:65
def loadAndMatch
Load reference objects overlapping an exposure and match to sources detected on that exposure...
Definition: astrometry.py:181