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