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