LSSTApplications  18.1.0
LSSTDataManagementBasePackage
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 
23 __all__ = ["AstrometryConfig", "AstrometryTask"]
24 
25 
26 import lsst.pex.config as pexConfig
27 import lsst.pipe.base as pipeBase
28 from .ref_match import RefMatchTask, RefMatchConfig
29 from .fitTanSipWcs import FitTanSipWcsTask
30 from .display import displayAstrometry
31 
32 
34  """Config for AstrometryTask.
35  """
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  def setDefaults(self):
62  # Override the default source selector for astrometry tasks
63  self.sourceFluxType = "Ap"
64 
65  self.sourceSelector.name = "matcher"
66  self.sourceSelector["matcher"].sourceFluxType = self.sourceFluxType
67 
68  # Note that if the matcher is MatchOptimisticBTask, then the
69  # default should be self.sourceSelector['matcher'].excludePixelFlags = False
70  # However, there is no way to do this automatically.
71 
72 
74  """Match an input source catalog with objects from a reference catalog and
75  solve for the WCS.
76 
77  This task is broken into two main subasks: matching and WCS fitting which
78  are very interactive. The matching here can be considered in part a first
79  pass WCS fitter due to the fitter's sensitivity to outliers.
80 
81  Parameters
82  ----------
83  refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
84  A reference object loader object
85  schema : `lsst.afw.table.Schema`
86  Used to set "calib_astrometry_used" flag in output source catalog.
87  **kwargs
88  additional keyword arguments for pipe_base
89  `lsst.pipe.base.Task.__init__`
90  """
91  ConfigClass = AstrometryConfig
92  _DefaultName = "astrometricSolver"
93 
94  def __init__(self, refObjLoader, schema=None, **kwargs):
95  RefMatchTask.__init__(self, refObjLoader, **kwargs)
96 
97  if schema is not None:
98  self.usedKey = schema.addField("calib_astrometry_used", type="Flag",
99  doc="set if source was used in astrometric calibration")
100  else:
101  self.usedKey = None
102 
103  self.makeSubtask("wcsFitter")
104 
105  @pipeBase.timeMethod
106  def run(self, sourceCat, exposure):
107  """Load reference objects, match sources and optionally fit a WCS.
108 
109  This is a thin layer around solve or loadAndMatch, depending on
110  config.forceKnownWcs.
111 
112  Parameters
113  ----------
114  exposure : `lsst.afw.image.Exposure`
115  exposure whose WCS is to be fit
116  The following are read only:
117 
118  - bbox
119  - photoCalib (may be absent)
120  - filter (may be unset)
121  - detector (if wcs is pure tangent; may be absent)
122 
123  The following are updated:
124 
125  - wcs (the initial value is used as an initial guess, and is
126  required)
127 
128  sourceCat : `lsst.afw.table.SourceCatalog`
129  catalog of sources detected on the exposure
130 
131  Returns
132  -------
133  result : `lsst.pipe.base.Struct`
134  with these fields:
135 
136  - ``refCat`` : reference object catalog of objects that overlap the
137  exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
138  - ``matches`` : astrometric matches
139  (`list` of `lsst.afw.table.ReferenceMatch`).
140  - ``scatterOnSky`` : median on-sky separation between reference
141  objects and sources in "matches"
142  (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
143  - ``matchMeta`` : metadata needed to unpersist matches
144  (`lsst.daf.base.PropertyList`)
145  """
146  if self.refObjLoader is None:
147  raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
148  if self.config.forceKnownWcs:
149  res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
150  res.scatterOnSky = None
151  else:
152  res = self.solve(exposure=exposure, sourceCat=sourceCat)
153  return res
154 
155  @pipeBase.timeMethod
156  def solve(self, exposure, sourceCat):
157  """Load reference objects overlapping an exposure, match to sources and
158  fit a WCS
159 
160  Returns
161  -------
162  result : `lsst.pipe.base.Struct`
163  Result struct with components:
164 
165  - ``refCat`` : reference object catalog of objects that overlap the
166  exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
167  - ``matches`` : astrometric matches
168  (`list` of `lsst.afw.table.ReferenceMatch`).
169  - ``scatterOnSky`` : median on-sky separation between reference
170  objects and sources in "matches" (`lsst.geom.Angle`)
171  - ``matchMeta`` : metadata needed to unpersist matches
172  (`lsst.daf.base.PropertyList`)
173 
174  Notes
175  -----
176  ignores config.forceKnownWcs
177  """
178  if self.refObjLoader is None:
179  raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
180  import lsstDebug
181  debug = lsstDebug.Info(__name__)
182 
183  expMd = self._getExposureMetadata(exposure)
184 
185  sourceSelection = self.sourceSelector.run(sourceCat)
186 
187  self.log.info("Purged %d sources, leaving %d good sources" %
188  (len(sourceCat) - len(sourceSelection.sourceCat),
189  len(sourceSelection.sourceCat)))
190 
191  loadRes = self.refObjLoader.loadPixelBox(
192  bbox=expMd.bbox,
193  wcs=expMd.wcs,
194  filterName=expMd.filterName,
195  photoCalib=expMd.photoCalib,
196  epoch=expMd.epoch,
197  )
198 
199  refSelection = self.referenceSelector.run(loadRes.refCat)
200 
201  matchMeta = self.refObjLoader.getMetadataBox(
202  bbox=expMd.bbox,
203  wcs=expMd.wcs,
204  filterName=expMd.filterName,
205  photoCalib=expMd.photoCalib,
206  epoch=expMd.epoch,
207  )
208 
209  if debug.display:
210  frame = int(debug.frame)
212  refCat=refSelection.sourceCat,
213  sourceCat=sourceSelection.sourceCat,
214  exposure=exposure,
215  bbox=expMd.bbox,
216  frame=frame,
217  title="Reference catalog",
218  )
219 
220  res = None
221  wcs = expMd.wcs
222  match_tolerance = None
223  for i in range(self.config.maxIter):
224  iterNum = i + 1
225  try:
226  tryRes = self._matchAndFitWcs(
227  refCat=refSelection.sourceCat,
228  sourceCat=sourceCat,
229  goodSourceCat=sourceSelection.sourceCat,
230  refFluxField=loadRes.fluxField,
231  bbox=expMd.bbox,
232  wcs=wcs,
233  exposure=exposure,
234  match_tolerance=match_tolerance,
235  )
236  except Exception as e:
237  # if we have had a succeessful iteration then use that; otherwise fail
238  if i > 0:
239  self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
240  iterNum -= 1
241  break
242  else:
243  raise
244 
245  match_tolerance = tryRes.match_tolerance
246  tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
247  self.log.debug(
248  "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
249  "max match distance = %0.3f arcsec",
250  iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
251  tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
252 
253  maxMatchDist = tryMatchDist.maxMatchDist
254  res = tryRes
255  wcs = res.wcs
256  if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
257  self.log.debug(
258  "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
259  "that's good enough",
260  maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
261  break
262  match_tolerance.maxMatchDist = maxMatchDist
263 
264  self.log.info(
265  "Matched and fit WCS in %d iterations; "
266  "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
267  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
268  tryMatchDist.distStdDev.asArcseconds()))
269  for m in res.matches:
270  if self.usedKey:
271  m.second.set(self.usedKey, True)
272  exposure.setWcs(res.wcs)
273 
274  return pipeBase.Struct(
275  refCat=refSelection.sourceCat,
276  matches=res.matches,
277  scatterOnSky=res.scatterOnSky,
278  matchMeta=matchMeta,
279  )
280 
281  @pipeBase.timeMethod
282  def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
283  exposure=None):
284  """Match sources to reference objects and fit a WCS.
285 
286  Parameters
287  ----------
288  refCat : `lsst.afw.table.SimpleCatalog`
289  catalog of reference objects
290  sourceCat : `lsst.afw.table.SourceCatalog`
291  catalog of sources detected on the exposure
292  goodSourceCat : `lsst.afw.table.SourceCatalog`
293  catalog of down-selected good sources detected on the exposure
294  refFluxField : 'str'
295  field of refCat to use for flux
296  bbox : `lsst.geom.Box2I`
297  bounding box of exposure
298  wcs : `lsst.afw.geom.SkyWcs`
299  initial guess for WCS of exposure
300  match_tolerance : `lsst.meas.astrom.MatchTolerance`
301  a MatchTolerance object (or None) specifying
302  internal tolerances to the matcher. See the MatchTolerance
303  definition in the respective matcher for the class definition.
304  exposure : `lsst.afw.image.Exposure`
305  exposure whose WCS is to be fit, or None; used only for the debug
306  display.
307 
308  Returns
309  -------
310  result : `lsst.pipe.base.Struct`
311  Result struct with components:
312 
313  - ``matches``: astrometric matches
314  (`list` of `lsst.afw.table.ReferenceMatch`).
315  - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
316  - ``scatterOnSky`` : median on-sky separation between reference
317  objects and sources in "matches" (`lsst.afw.geom.Angle`).
318  """
319  import lsstDebug
320  debug = lsstDebug.Info(__name__)
321 
322  sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
323 
324  matchRes = self.matcher.matchObjectsToSources(
325  refCat=refCat,
326  sourceCat=goodSourceCat,
327  wcs=wcs,
328  sourceFluxField=sourceFluxField,
329  refFluxField=refFluxField,
330  match_tolerance=match_tolerance,
331  )
332  self.log.debug("Found %s matches", len(matchRes.matches))
333  if debug.display:
334  frame = int(debug.frame)
336  refCat=refCat,
337  sourceCat=matchRes.usableSourceCat,
338  matches=matchRes.matches,
339  exposure=exposure,
340  bbox=bbox,
341  frame=frame + 1,
342  title="Initial WCS",
343  )
344 
345  self.log.debug("Fitting WCS")
346  fitRes = self.wcsFitter.fitWcs(
347  matches=matchRes.matches,
348  initWcs=wcs,
349  bbox=bbox,
350  refCat=refCat,
351  sourceCat=sourceCat,
352  exposure=exposure,
353  )
354  fitWcs = fitRes.wcs
355  scatterOnSky = fitRes.scatterOnSky
356  if debug.display:
357  frame = int(debug.frame)
359  refCat=refCat,
360  sourceCat=matchRes.usableSourceCat,
361  matches=matchRes.matches,
362  exposure=exposure,
363  bbox=bbox,
364  frame=frame + 2,
365  title="Fit TAN-SIP WCS",
366  )
367 
368  return pipeBase.Struct(
369  matches=matchRes.matches,
370  wcs=fitWcs,
371  scatterOnSky=scatterOnSky,
372  match_tolerance=matchRes.match_tolerance,
373  )
def solve(self, exposure, sourceCat)
Definition: astrometry.py:156
def _computeMatchStatsOnSky(self, matchList)
Definition: ref_match.py:205
def _getExposureMetadata(self, exposure)
Definition: ref_match.py:234
def run(self, sourceCat, exposure)
Definition: astrometry.py:106
def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
Definition: astrometry.py:283
def __init__(self, refObjLoader, schema=None, kwargs)
Definition: astrometry.py:94
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:34
def loadAndMatch(self, exposure, sourceCat)
Definition: ref_match.py:116