lsst.meas.astrom  16.0-18-g51a54b3+2
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, schema=schema, **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.config.forceKnownWcs:
134  res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
135  res.scatterOnSky = None
136  else:
137  res = self.solve(exposure=exposure, sourceCat=sourceCat)
138  return res
139 
140  @pipeBase.timeMethod
141  def solve(self, exposure, sourceCat):
142  """Load reference objects overlapping an exposure, match to sources and
143  fit a WCS
144 
145  Returns
146  -------
147  result : `lsst.pipe.base.Struct`
148  Result struct with components:
149 
150  - ``refCat`` : reference object catalog of objects that overlap the
151  exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
152  - ``matches`` : astrometric matches
153  (`list` of `lsst.afw.table.ReferenceMatch`).
154  - ``scatterOnSky`` : median on-sky separation between reference
155  objects and sources in "matches" (`lsst.geom.Angle`)
156  - ``matchMeta`` : metadata needed to unpersist matches
157  (`lsst.daf.base.PropertyList`)
158 
159  Notes
160  -----
161  ignores config.forceKnownWcs
162  """
163  import lsstDebug
164  debug = lsstDebug.Info(__name__)
165 
166  expMd = self._getExposureMetadata(exposure)
167 
168  loadRes = self.refObjLoader.loadPixelBox(
169  bbox=expMd.bbox,
170  wcs=expMd.wcs,
171  filterName=expMd.filterName,
172  calib=expMd.calib,
173  epoch=expMd.epoch,
174  )
175  matchMeta = self.refObjLoader.getMetadataBox(
176  bbox=expMd.bbox,
177  wcs=expMd.wcs,
178  filterName=expMd.filterName,
179  calib=expMd.calib,
180  epoch=expMd.epoch,
181  )
182 
183  if debug.display:
184  frame = int(debug.frame)
186  refCat=loadRes.refCat,
187  sourceCat=sourceCat,
188  exposure=exposure,
189  bbox=expMd.bbox,
190  frame=frame,
191  title="Reference catalog",
192  )
193 
194  res = None
195  wcs = expMd.wcs
196  match_tolerance = None
197  for i in range(self.config.maxIter):
198  iterNum = i + 1
199  try:
200  tryRes = self._matchAndFitWcs( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None
201  refCat=loadRes.refCat,
202  sourceCat=sourceCat,
203  refFluxField=loadRes.fluxField,
204  bbox=expMd.bbox,
205  wcs=wcs,
206  exposure=exposure,
207  match_tolerance=match_tolerance,
208  )
209  except Exception as e:
210  # if we have had a succeessful iteration then use that; otherwise fail
211  if i > 0:
212  self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
213  iterNum -= 1
214  break
215  else:
216  raise
217 
218  match_tolerance = tryRes.match_tolerance
219  tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
220  self.log.debug(
221  "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
222  "max match distance = %0.3f arcsec",
223  iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
224  tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
225 
226  maxMatchDist = tryMatchDist.maxMatchDist
227  res = tryRes
228  wcs = res.wcs
229  if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
230  self.log.debug(
231  "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
232  "that's good enough",
233  maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
234  break
235  match_tolerance.maxMatchDist = maxMatchDist
236 
237  self.log.info(
238  "Matched and fit WCS in %d iterations; "
239  "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
240  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
241  tryMatchDist.distStdDev.asArcseconds()))
242  for m in res.matches:
243  if self.usedKey:
244  m.second.set(self.usedKey, True)
245  exposure.setWcs(res.wcs)
246 
247  return pipeBase.Struct(
248  refCat=loadRes.refCat,
249  matches=res.matches,
250  scatterOnSky=res.scatterOnSky,
251  matchMeta=matchMeta,
252  )
253 
254  @pipeBase.timeMethod
255  def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance,
256  exposure=None):
257  """Match sources to reference objects and fit a WCS.
258 
259  Parameters
260  ----------
261  refCat : `lsst.afw.table.SimpleCatalog`
262  catalog of reference objects
263  sourceCat : `lsst.afw.table.SourceCatalog`
264  catalog of sources detected on the exposure
265  refFluxField : 'str'
266  field of refCat to use for flux
267  bbox : `lsst.geom.Box2I`
268  bounding box of exposure
269  wcs : `lsst.afw.geom.SkyWcs`
270  initial guess for WCS of exposure
271  match_tolerance : `lsst.meas.astrom.MatchTolerance`
272  a MatchTolerance object (or None) specifying
273  internal tolerances to the matcher. See the MatchTolerance
274  definition in the respective matcher for the class definition.
275  exposure : `lsst.afw.image.Exposure`
276  exposure whose WCS is to be fit, or None; used only for the debug
277  display.
278 
279  Returns
280  -------
281  result : `lsst.pipe.base.Struct`
282  Result struct with components:
283 
284  - ``matches``: astrometric matches
285  (`list` of `lsst.afw.table.ReferenceMatch`).
286  - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
287  - ``scatterOnSky`` : median on-sky separation between reference
288  objects and sources in "matches" (`lsst.afw.geom.Angle`).
289  """
290  import lsstDebug
291  debug = lsstDebug.Info(__name__)
292  matchRes = self.matcher.matchObjectsToSources(
293  refCat=refCat,
294  sourceCat=sourceCat,
295  wcs=wcs,
296  refFluxField=refFluxField,
297  match_tolerance=match_tolerance,
298  )
299  self.log.debug("Found %s matches", len(matchRes.matches))
300  if debug.display:
301  frame = int(debug.frame)
303  refCat=refCat,
304  sourceCat=matchRes.usableSourceCat,
305  matches=matchRes.matches,
306  exposure=exposure,
307  bbox=bbox,
308  frame=frame + 1,
309  title="Initial WCS",
310  )
311 
312  self.log.debug("Fitting WCS")
313  fitRes = self.wcsFitter.fitWcs(
314  matches=matchRes.matches,
315  initWcs=wcs,
316  bbox=bbox,
317  refCat=refCat,
318  sourceCat=sourceCat,
319  exposure=exposure,
320  )
321  fitWcs = fitRes.wcs
322  scatterOnSky = fitRes.scatterOnSky
323  if debug.display:
324  frame = int(debug.frame)
326  refCat=refCat,
327  sourceCat=matchRes.usableSourceCat,
328  matches=matchRes.matches,
329  exposure=exposure,
330  bbox=bbox,
331  frame=frame + 2,
332  title="Fit TAN-SIP WCS",
333  )
334 
335  return pipeBase.Struct(
336  matches=matchRes.matches,
337  wcs=fitWcs,
338  scatterOnSky=scatterOnSky,
339  match_tolerance=matchRes.match_tolerance,
340  )
def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
Definition: astrometry.py:256
def solve(self, exposure, sourceCat)
Definition: astrometry.py:141
def _computeMatchStatsOnSky(self, matchList)
Definition: ref_match.py:164
def _getExposureMetadata(self, exposure)
Definition: ref_match.py:193
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:80