lsst.meas.astrom  17.0.1-7-ga01e3e3
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( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None
227  refCat=refSelection.sourceCat,
228  sourceCat=sourceSelection.sourceCat,
229  refFluxField=loadRes.fluxField,
230  bbox=expMd.bbox,
231  wcs=wcs,
232  exposure=exposure,
233  match_tolerance=match_tolerance,
234  )
235  except Exception as e:
236  # if we have had a succeessful iteration then use that; otherwise fail
237  if i > 0:
238  self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
239  iterNum -= 1
240  break
241  else:
242  raise
243 
244  match_tolerance = tryRes.match_tolerance
245  tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
246  self.log.debug(
247  "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
248  "max match distance = %0.3f arcsec",
249  iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
250  tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
251 
252  maxMatchDist = tryMatchDist.maxMatchDist
253  res = tryRes
254  wcs = res.wcs
255  if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
256  self.log.debug(
257  "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
258  "that's good enough",
259  maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
260  break
261  match_tolerance.maxMatchDist = maxMatchDist
262 
263  self.log.info(
264  "Matched and fit WCS in %d iterations; "
265  "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
266  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
267  tryMatchDist.distStdDev.asArcseconds()))
268  for m in res.matches:
269  if self.usedKey:
270  m.second.set(self.usedKey, True)
271  exposure.setWcs(res.wcs)
272 
273  return pipeBase.Struct(
274  refCat=refSelection.sourceCat,
275  matches=res.matches,
276  scatterOnSky=res.scatterOnSky,
277  matchMeta=matchMeta,
278  )
279 
280  @pipeBase.timeMethod
281  def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance,
282  exposure=None):
283  """Match sources to reference objects and fit a WCS.
284 
285  Parameters
286  ----------
287  refCat : `lsst.afw.table.SimpleCatalog`
288  catalog of reference objects
289  sourceCat : `lsst.afw.table.SourceCatalog`
290  catalog of sources detected on the exposure
291  refFluxField : 'str'
292  field of refCat to use for flux
293  bbox : `lsst.geom.Box2I`
294  bounding box of exposure
295  wcs : `lsst.afw.geom.SkyWcs`
296  initial guess for WCS of exposure
297  match_tolerance : `lsst.meas.astrom.MatchTolerance`
298  a MatchTolerance object (or None) specifying
299  internal tolerances to the matcher. See the MatchTolerance
300  definition in the respective matcher for the class definition.
301  exposure : `lsst.afw.image.Exposure`
302  exposure whose WCS is to be fit, or None; used only for the debug
303  display.
304 
305  Returns
306  -------
307  result : `lsst.pipe.base.Struct`
308  Result struct with components:
309 
310  - ``matches``: astrometric matches
311  (`list` of `lsst.afw.table.ReferenceMatch`).
312  - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
313  - ``scatterOnSky`` : median on-sky separation between reference
314  objects and sources in "matches" (`lsst.afw.geom.Angle`).
315  """
316  import lsstDebug
317  debug = lsstDebug.Info(__name__)
318 
319  sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
320 
321  matchRes = self.matcher.matchObjectsToSources(
322  refCat=refCat,
323  sourceCat=sourceCat,
324  wcs=wcs,
325  sourceFluxField=sourceFluxField,
326  refFluxField=refFluxField,
327  match_tolerance=match_tolerance,
328  )
329  self.log.debug("Found %s matches", len(matchRes.matches))
330  if debug.display:
331  frame = int(debug.frame)
333  refCat=refCat,
334  sourceCat=matchRes.usableSourceCat,
335  matches=matchRes.matches,
336  exposure=exposure,
337  bbox=bbox,
338  frame=frame + 1,
339  title="Initial WCS",
340  )
341 
342  self.log.debug("Fitting WCS")
343  fitRes = self.wcsFitter.fitWcs(
344  matches=matchRes.matches,
345  initWcs=wcs,
346  bbox=bbox,
347  refCat=refCat,
348  sourceCat=sourceCat,
349  exposure=exposure,
350  )
351  fitWcs = fitRes.wcs
352  scatterOnSky = fitRes.scatterOnSky
353  if debug.display:
354  frame = int(debug.frame)
356  refCat=refCat,
357  sourceCat=matchRes.usableSourceCat,
358  matches=matchRes.matches,
359  exposure=exposure,
360  bbox=bbox,
361  frame=frame + 2,
362  title="Fit TAN-SIP WCS",
363  )
364 
365  return pipeBase.Struct(
366  matches=matchRes.matches,
367  wcs=fitWcs,
368  scatterOnSky=scatterOnSky,
369  match_tolerance=matchRes.match_tolerance,
370  )
def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
Definition: astrometry.py:282
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 __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