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