lsst.meas.astrom  14.0-7-g0d69b06+3
astrometry.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 from __future__ import absolute_import, division, print_function
23 
24 __all__ = ["AstrometryConfig", "AstrometryTask"]
25 
26 from builtins import range
27 
28 import lsst.pex.config as pexConfig
29 import lsst.pipe.base as pipeBase
30 from .ref_match import RefMatchTask, RefMatchConfig
31 from .fitTanSipWcs import FitTanSipWcsTask
32 from .display import displayAstrometry
33 
34 
36  wcsFitter = pexConfig.ConfigurableField(
37  target=FitTanSipWcsTask,
38  doc="WCS fitter",
39  )
40  forceKnownWcs = pexConfig.Field(
41  dtype=bool,
42  doc="If True then load reference objects and match sources but do not fit a WCS; " +
43  " this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
44  default=False,
45  )
46  maxIter = pexConfig.RangeField(
47  doc="maximum number of iterations of match sources and fit WCS" +
48  "ignored if not fitting a WCS",
49  dtype=int,
50  default=3,
51  min=1,
52  )
53  minMatchDistanceArcSec = pexConfig.RangeField(
54  doc="the match distance below which further iteration is pointless (arcsec); "
55  "ignored if not fitting a WCS",
56  dtype=float,
57  default=0.001,
58  min=0,
59  )
60 
61 # The following block adds links to this task from the Task Documentation page.
62 
68 
69 
71  """!Match an input source catalog with objects from a reference catalog and solve for the WCS
72 
73  @anchor AstrometryTask_
74 
75  @section meas_astrom_astrometry_Contents Contents
76 
77  - @ref meas_astrom_astrometry_Purpose
78  - @ref meas_astrom_astrometry_Initialize
79  - @ref meas_astrom_astrometry_IO
80  - @ref meas_astrom_astrometry_Config
81  - @ref meas_astrom_astrometry_Example
82  - @ref meas_astrom_astrometry_Debug
83 
84  @section meas_astrom_astrometry_Purpose Description
85 
86  Match input sourceCat with a reference catalog and solve for the Wcs
87 
88  There are three steps, each performed by different subtasks:
89  - Find position reference stars that overlap the exposure
90  - Match sourceCat to position reference stars
91  - Fit a WCS based on the matches
92 
93  @section meas_astrom_astrometry_Initialize Task initialisation
94 
95  @copydoc \_\_init\_\_
96 
97  @section meas_astrom_astrometry_IO Invoking the Task
98 
99  @copydoc run
100 
101  @copydoc loadAndMatch
102 
103  @section meas_astrom_astrometry_Config Configuration parameters
104 
105  See @ref AstrometryConfig
106 
107  @section meas_astrom_astrometry_Example A complete example of using AstrometryTask
108 
109  See \ref pipe_tasks_photocal_Example.
110 
111  @section meas_astrom_astrometry_Debug Debug variables
112 
113  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
114  flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
115  @b debug.py files.
116 
117  The available variables in AstrometryTask are:
118  <DL>
119  <DT> @c display (bool)
120  <DD> If True display information at three stages: after finding reference objects,
121  after matching sources to reference objects, and after fitting the WCS; defaults to False
122  <DT> @c frame (int)
123  <DD> ds9 frame to use to display the reference objects; the next two frames are used
124  to display the match list and the results of the final WCS; defaults to 0
125  </DL>
126 
127  To investigate the @ref meas_astrom_astrometry_Debug, put something like
128  @code{.py}
129  import lsstDebug
130  def DebugInfo(name):
131  debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
132  if name == "lsst.meas.astrom.astrometry":
133  debug.display = True
134 
135  return debug
136 
137  lsstDebug.Info = DebugInfo
138  @endcode
139  into your debug.py file and run this task with the @c --debug flag.
140  """
141  ConfigClass = AstrometryConfig
142  _DefaultName = "astrometricSolver"
143 
144  def __init__(self, refObjLoader, schema=None, **kwargs):
145  """!Construct an AstrometryTask
146 
147  @param[in] refObjLoader A reference object loader object
148  @param[in] schema ignored; available for compatibility with an older astrometry task
149  @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_
150  """
151  RefMatchTask.__init__(self, refObjLoader, schema=schema, **kwargs)
152 
153  if schema is not None:
154  self.usedKey = schema.addField("calib_astrometryUsed", type="Flag",
155  doc="set if source was used in astrometric calibration")
156  else:
157  self.usedKey = None
158 
159  self.makeSubtask("wcsFitter")
160 
161  @pipeBase.timeMethod
162  def run(self, sourceCat, exposure):
163  """!Load reference objects, match sources and optionally fit a WCS
164 
165  This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs
166 
167  @param[in,out] exposure exposure whose WCS is to be fit
168  The following are read only:
169  - bbox
170  - calib (may be absent)
171  - filter (may be unset)
172  - detector (if wcs is pure tangent; may be absent)
173  The following are updated:
174  - wcs (the initial value is used as an initial guess, and is required)
175  @param[in] sourceCat catalog of sources detected on the exposure (an lsst.afw.table.SourceCatalog)
176  @return an lsst.pipe.base.Struct with these fields:
177  - refCat reference object catalog of objects that overlap the exposure (with some margin)
178  (an lsst::afw::table::SimpleCatalog)
179  - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch
180  - scatterOnSky median on-sky separation between reference objects and sources in "matches"
181  (an lsst.afw.geom.Angle), or None if config.forceKnownWcs True
182  - matchMeta metadata needed to unpersist matches (an lsst.daf.base.PropertyList)
183  """
184  if self.config.forceKnownWcs:
185  res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
186  res.scatterOnSky = None
187  else:
188  res = self.solve(exposure=exposure, sourceCat=sourceCat)
189  return res
190 
191  @pipeBase.timeMethod
192  def solve(self, exposure, sourceCat):
193  """!Load reference objects overlapping an exposure, match to sources and fit a WCS
194 
195  @return an lsst.pipe.base.Struct with these fields:
196  - refCat reference object catalog of objects that overlap the exposure (with some margin)
197  (an lsst::afw::table::SimpleCatalog)
198  - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch
199  - scatterOnSky median on-sky separation between reference objects and sources in "matches"
200  (an lsst.afw.geom.Angle)
201  - matchMeta metadata needed to unpersist matches (an lsst.daf.base.PropertyList)
202 
203  @note ignores config.forceKnownWcs
204  """
205  import lsstDebug
206  debug = lsstDebug.Info(__name__)
207 
208  expMd = self._getExposureMetadata(exposure)
209 
210  loadRes = self.refObjLoader.loadPixelBox(
211  bbox=expMd.bbox,
212  wcs=expMd.wcs,
213  filterName=expMd.filterName,
214  calib=expMd.calib,
215  )
216  matchMeta = self.refObjLoader.getMetadataBox(
217  bbox=expMd.bbox,
218  wcs=expMd.wcs,
219  filterName=expMd.filterName,
220  calib=expMd.calib,
221  )
222 
223  if debug.display:
224  frame = int(debug.frame)
226  refCat=loadRes.refCat,
227  sourceCat=sourceCat,
228  exposure=exposure,
229  bbox=expMd.bbox,
230  frame=frame,
231  title="Reference catalog",
232  )
233 
234  res = None
235  wcs = expMd.wcs
236  match_tolerance = None
237  for i in range(self.config.maxIter):
238  iterNum = i + 1
239  try:
240  tryRes = self._matchAndFitWcs( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None
241  refCat=loadRes.refCat,
242  sourceCat=sourceCat,
243  refFluxField=loadRes.fluxField,
244  bbox=expMd.bbox,
245  wcs=wcs,
246  exposure=exposure,
247  match_tolerance=match_tolerance,
248  )
249  except Exception as e:
250  # if we have had a succeessful iteration then use that; otherwise fail
251  if i > 0:
252  self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
253  iterNum -= 1
254  break
255  else:
256  raise
257 
258  match_tolerance = tryRes.match_tolerance
259  tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
260  self.log.debug(
261  "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
262  "max match distance = %0.3f arcsec",
263  iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
264  tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
265 
266  maxMatchDist = tryMatchDist.maxMatchDist
267  res = tryRes
268  wcs = res.wcs
269  if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
270  self.log.debug(
271  "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
272  "that's good enough",
273  maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
274  break
275  match_tolerance.maxMatchDist = maxMatchDist
276 
277  self.log.info(
278  "Matched and fit WCS in %d iterations; "
279  "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
280  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
281  tryMatchDist.distStdDev.asArcseconds()))
282  for m in res.matches:
283  if self.usedKey:
284  m.second.set(self.usedKey, True)
285  exposure.setWcs(res.wcs)
286 
287  return pipeBase.Struct(
288  refCat=loadRes.refCat,
289  matches=res.matches,
290  scatterOnSky=res.scatterOnSky,
291  matchMeta=matchMeta,
292  )
293 
294  @pipeBase.timeMethod
295  def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance,
296  exposure=None):
297  """!Match sources to reference objects and fit a WCS
298 
299  @param[in] refCat catalog of reference objects
300  @param[in] sourceCat catalog of sources detected on the exposure (an lsst.afw.table.SourceCatalog)
301  @param[in] refFluxField field of refCat to use for flux
302  @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
303  @param[in] wcs initial guess for WCS of exposure (an lsst.afw.geom.Wcs)
304  @param[in] match_tolerance a MatchTolerance object (or None) specifying
305  internal tolerances to the matcher. See the MatchTolerance
306  definition in the respective matcher for the class definition.
307  @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
308 
309  @return an lsst.pipe.base.Struct with these fields:
310  - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch
311  - wcs the fit WCS (an lsst.afw.geom.Wcs)
312  - scatterOnSky median on-sky separation between reference objects and sources in "matches"
313  (an lsst.afw.geom.Angle)
314  """
315  import lsstDebug
316  debug = lsstDebug.Info(__name__)
317  matchRes = self.matcher.matchObjectsToSources(
318  refCat=refCat,
319  sourceCat=sourceCat,
320  wcs=wcs,
321  refFluxField=refFluxField,
322  match_tolerance=match_tolerance,
323  )
324  self.log.debug("Found %s matches", len(matchRes.matches))
325  if debug.display:
326  frame = int(debug.frame)
328  refCat=refCat,
329  sourceCat=matchRes.usableSourceCat,
330  matches=matchRes.matches,
331  exposure=exposure,
332  bbox=bbox,
333  frame=frame + 1,
334  title="Initial WCS",
335  )
336 
337  self.log.debug("Fitting WCS")
338  fitRes = self.wcsFitter.fitWcs(
339  matches=matchRes.matches,
340  initWcs=wcs,
341  bbox=bbox,
342  refCat=refCat,
343  sourceCat=sourceCat,
344  exposure=exposure,
345  )
346  fitWcs = fitRes.wcs
347  scatterOnSky = fitRes.scatterOnSky
348  if debug.display:
349  frame = int(debug.frame)
351  refCat=refCat,
352  sourceCat=matchRes.usableSourceCat,
353  matches=matchRes.matches,
354  exposure=exposure,
355  bbox=bbox,
356  frame=frame + 2,
357  title="Fit TAN-SIP WCS",
358  )
359 
360  return pipeBase.Struct(
361  matches=matchRes.matches,
362  wcs=fitWcs,
363  scatterOnSky=scatterOnSky,
364  match_tolerance=matchRes.match_tolerance,
365  )
def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
Match sources to reference objects and fit a WCS.
Definition: astrometry.py:296
def solve(self, exposure, sourceCat)
Load reference objects overlapping an exposure, match to sources and fit a WCS.
Definition: astrometry.py:192
def _computeMatchStatsOnSky(self, matchList)
Definition: ref_match.py:156
def _getExposureMetadata(self, exposure)
Extract metadata from an exposure.
Definition: ref_match.py:176
def run(self, sourceCat, exposure)
Load reference objects, match sources and optionally fit a WCS.
Definition: astrometry.py:162
Match an input source catalog with objects from a reference catalog.
Definition: ref_match.py:63
def __init__(self, refObjLoader, schema=None, kwargs)
Construct an AstrometryTask.
Definition: astrometry.py:144
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:37
def loadAndMatch(self, exposure, sourceCat)
Load reference objects overlapping an exposure and match to sources detected on that exposure...
Definition: ref_match.py:85
Match an input source catalog with objects from a reference catalog and solve for the WCS...
Definition: astrometry.py:70