LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
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 import numpy as np
26 from astropy import units
27 import scipy.stats
28 
29 import lsst.pex.config as pexConfig
30 import lsst.pipe.base as pipeBase
31 from .ref_match import RefMatchTask, RefMatchConfig
32 from .fitTanSipWcs import FitTanSipWcsTask
33 from .display import displayAstrometry
34 
35 
37  """Config for AstrometryTask.
38  """
39  wcsFitter = pexConfig.ConfigurableField(
40  target=FitTanSipWcsTask,
41  doc="WCS fitter",
42  )
43  forceKnownWcs = pexConfig.Field(
44  dtype=bool,
45  doc="If True then load reference objects and match sources but do not fit a WCS; "
46  "this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
47  default=False,
48  )
49  maxIter = pexConfig.RangeField(
50  doc="maximum number of iterations of match sources and fit WCS"
51  "ignored if not fitting a WCS",
52  dtype=int,
53  default=3,
54  min=1,
55  )
56  minMatchDistanceArcSec = pexConfig.RangeField(
57  doc="the match distance below which further iteration is pointless (arcsec); "
58  "ignored if not fitting a WCS",
59  dtype=float,
60  default=0.001,
61  min=0,
62  )
63  doMagnitudeOutlierRejection = pexConfig.Field(
64  dtype=bool,
65  doc=("If True then a rough zeropoint will be computed from matched sources "
66  "and outliers will be rejected in the iterations."),
67  default=False,
68  )
69  magnitudeOutlierRejectionNSigma = pexConfig.Field(
70  dtype=float,
71  doc=("Number of sigma (measured from the distribution) in magnitude "
72  "for a potential reference/source match to be rejected during "
73  "iteration."),
74  default=3.0,
75  )
76 
77  def setDefaults(self):
78  # Override the default source selector for astrometry tasks
79  self.sourceFluxTypesourceFluxTypesourceFluxType = "Ap"
80 
81  self.sourceSelectorsourceSelector.name = "matcher"
82  self.sourceSelectorsourceSelector["matcher"].sourceFluxType = self.sourceFluxTypesourceFluxTypesourceFluxType
83 
84  # Note that if the matcher is MatchOptimisticBTask, then the
85  # default should be self.sourceSelector['matcher'].excludePixelFlags = False
86  # However, there is no way to do this automatically.
87 
88 
90  """Match an input source catalog with objects from a reference catalog and
91  solve for the WCS.
92 
93  This task is broken into two main subasks: matching and WCS fitting which
94  are very interactive. The matching here can be considered in part a first
95  pass WCS fitter due to the fitter's sensitivity to outliers.
96 
97  Parameters
98  ----------
99  refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
100  A reference object loader object
101  schema : `lsst.afw.table.Schema`
102  Used to set "calib_astrometry_used" flag in output source catalog.
103  **kwargs
104  additional keyword arguments for pipe_base
105  `lsst.pipe.base.Task.__init__`
106  """
107  ConfigClass = AstrometryConfig
108  _DefaultName = "astrometricSolver"
109 
110  def __init__(self, refObjLoader, schema=None, **kwargs):
111  RefMatchTask.__init__(self, refObjLoader, **kwargs)
112 
113  if schema is not None:
114  self.usedKeyusedKey = schema.addField("calib_astrometry_used", type="Flag",
115  doc="set if source was used in astrometric calibration")
116  else:
117  self.usedKeyusedKey = None
118 
119  self.makeSubtask("wcsFitter")
120 
121  @pipeBase.timeMethod
122  def run(self, sourceCat, exposure):
123  """Load reference objects, match sources and optionally fit a WCS.
124 
125  This is a thin layer around solve or loadAndMatch, depending on
126  config.forceKnownWcs.
127 
128  Parameters
129  ----------
130  exposure : `lsst.afw.image.Exposure`
131  exposure whose WCS is to be fit
132  The following are read only:
133 
134  - bbox
135  - photoCalib (may be absent)
136  - filter (may be unset)
137  - detector (if wcs is pure tangent; may be absent)
138 
139  The following are updated:
140 
141  - wcs (the initial value is used as an initial guess, and is
142  required)
143 
144  sourceCat : `lsst.afw.table.SourceCatalog`
145  catalog of sources detected on the exposure
146 
147  Returns
148  -------
149  result : `lsst.pipe.base.Struct`
150  with these fields:
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"
158  (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
159  - ``matchMeta`` : metadata needed to unpersist matches
160  (`lsst.daf.base.PropertyList`)
161  """
162  if self.refObjLoaderrefObjLoader is None:
163  raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
164  if self.config.forceKnownWcs:
165  res = self.loadAndMatchloadAndMatch(exposure=exposure, sourceCat=sourceCat)
166  res.scatterOnSky = None
167  else:
168  res = self.solvesolve(exposure=exposure, sourceCat=sourceCat)
169  return res
170 
171  @pipeBase.timeMethod
172  def solve(self, exposure, sourceCat):
173  """Load reference objects overlapping an exposure, match to sources and
174  fit a WCS
175 
176  Returns
177  -------
178  result : `lsst.pipe.base.Struct`
179  Result struct with components:
180 
181  - ``refCat`` : reference object catalog of objects that overlap the
182  exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
183  - ``matches`` : astrometric matches
184  (`list` of `lsst.afw.table.ReferenceMatch`).
185  - ``scatterOnSky`` : median on-sky separation between reference
186  objects and sources in "matches" (`lsst.geom.Angle`)
187  - ``matchMeta`` : metadata needed to unpersist matches
188  (`lsst.daf.base.PropertyList`)
189 
190  Notes
191  -----
192  ignores config.forceKnownWcs
193  """
194  if self.refObjLoaderrefObjLoader is None:
195  raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
196  import lsstDebug
197  debug = lsstDebug.Info(__name__)
198 
199  expMd = self._getExposureMetadata_getExposureMetadata(exposure)
200 
201  sourceSelection = self.sourceSelector.run(sourceCat)
202 
203  self.log.info("Purged %d sources, leaving %d good sources" %
204  (len(sourceCat) - len(sourceSelection.sourceCat),
205  len(sourceSelection.sourceCat)))
206 
207  loadRes = self.refObjLoaderrefObjLoader.loadPixelBox(
208  bbox=expMd.bbox,
209  wcs=expMd.wcs,
210  filterName=expMd.filterName,
211  photoCalib=expMd.photoCalib,
212  epoch=expMd.epoch,
213  )
214 
215  refSelection = self.referenceSelector.run(loadRes.refCat)
216 
217  matchMeta = self.refObjLoaderrefObjLoader.getMetadataBox(
218  bbox=expMd.bbox,
219  wcs=expMd.wcs,
220  filterName=expMd.filterName,
221  photoCalib=expMd.photoCalib,
222  epoch=expMd.epoch,
223  )
224 
225  if debug.display:
226  frame = int(debug.frame)
228  refCat=refSelection.sourceCat,
229  sourceCat=sourceSelection.sourceCat,
230  exposure=exposure,
231  bbox=expMd.bbox,
232  frame=frame,
233  title="Reference catalog",
234  )
235 
236  res = None
237  wcs = expMd.wcs
238  match_tolerance = None
239  for i in range(self.config.maxIter):
240  iterNum = i + 1
241  try:
242  tryRes = self._matchAndFitWcs_matchAndFitWcs(
243  refCat=refSelection.sourceCat,
244  sourceCat=sourceCat,
245  goodSourceCat=sourceSelection.sourceCat,
246  refFluxField=loadRes.fluxField,
247  bbox=expMd.bbox,
248  wcs=wcs,
249  exposure=exposure,
250  match_tolerance=match_tolerance,
251  )
252  except Exception as e:
253  # if we have had a succeessful iteration then use that; otherwise fail
254  if i > 0:
255  self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
256  iterNum -= 1
257  break
258  else:
259  raise
260 
261  match_tolerance = tryRes.match_tolerance
262  tryMatchDist = self._computeMatchStatsOnSky_computeMatchStatsOnSky(tryRes.matches)
263  self.log.debug(
264  "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
265  "max match distance = %0.3f arcsec",
266  iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
267  tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
268 
269  maxMatchDist = tryMatchDist.maxMatchDist
270  res = tryRes
271  wcs = res.wcs
272  if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
273  self.log.debug(
274  "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
275  "that's good enough",
276  maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
277  break
278  match_tolerance.maxMatchDist = maxMatchDist
279 
280  self.log.info(
281  "Matched and fit WCS in %d iterations; "
282  "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
283  (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
284  tryMatchDist.distStdDev.asArcseconds()))
285  for m in res.matches:
286  if self.usedKeyusedKey:
287  m.second.set(self.usedKeyusedKey, True)
288  exposure.setWcs(res.wcs)
289 
290  # Record the scatter in the exposure metadata
291  md = exposure.getMetadata()
292  md['SFM_ASTROM_OFFSET_MEAN'] = tryMatchDist.distMean.asArcseconds()
293  md['SFM_ASTROM_OFFSET_STD'] = tryMatchDist.distStdDev.asArcseconds()
294 
295  return pipeBase.Struct(
296  refCat=refSelection.sourceCat,
297  matches=res.matches,
298  scatterOnSky=res.scatterOnSky,
299  matchMeta=matchMeta,
300  )
301 
302  @pipeBase.timeMethod
303  def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
304  exposure=None):
305  """Match sources to reference objects and fit a WCS.
306 
307  Parameters
308  ----------
309  refCat : `lsst.afw.table.SimpleCatalog`
310  catalog of reference objects
311  sourceCat : `lsst.afw.table.SourceCatalog`
312  catalog of sources detected on the exposure
313  goodSourceCat : `lsst.afw.table.SourceCatalog`
314  catalog of down-selected good sources detected on the exposure
315  refFluxField : 'str'
316  field of refCat to use for flux
317  bbox : `lsst.geom.Box2I`
318  bounding box of exposure
319  wcs : `lsst.afw.geom.SkyWcs`
320  initial guess for WCS of exposure
321  match_tolerance : `lsst.meas.astrom.MatchTolerance`
322  a MatchTolerance object (or None) specifying
323  internal tolerances to the matcher. See the MatchTolerance
324  definition in the respective matcher for the class definition.
325  exposure : `lsst.afw.image.Exposure`
326  exposure whose WCS is to be fit, or None; used only for the debug
327  display.
328 
329  Returns
330  -------
331  result : `lsst.pipe.base.Struct`
332  Result struct with components:
333 
334  - ``matches``: astrometric matches
335  (`list` of `lsst.afw.table.ReferenceMatch`).
336  - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
337  - ``scatterOnSky`` : median on-sky separation between reference
338  objects and sources in "matches" (`lsst.afw.geom.Angle`).
339  """
340  import lsstDebug
341  debug = lsstDebug.Info(__name__)
342 
343  sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
344 
345  matchRes = self.matcher.matchObjectsToSources(
346  refCat=refCat,
347  sourceCat=goodSourceCat,
348  wcs=wcs,
349  sourceFluxField=sourceFluxField,
350  refFluxField=refFluxField,
351  match_tolerance=match_tolerance,
352  )
353  self.log.debug("Found %s matches", len(matchRes.matches))
354  if debug.display:
355  frame = int(debug.frame)
357  refCat=refCat,
358  sourceCat=matchRes.usableSourceCat,
359  matches=matchRes.matches,
360  exposure=exposure,
361  bbox=bbox,
362  frame=frame + 1,
363  title="Initial WCS",
364  )
365 
366  if self.config.doMagnitudeOutlierRejection:
367  matches = self._removeMagnitudeOutliers_removeMagnitudeOutliers(sourceFluxField, refFluxField, matchRes.matches)
368  else:
369  matches = matchRes.matches
370 
371  self.log.debug("Fitting WCS")
372  fitRes = self.wcsFitter.fitWcs(
373  matches=matches,
374  initWcs=wcs,
375  bbox=bbox,
376  refCat=refCat,
377  sourceCat=sourceCat,
378  exposure=exposure,
379  )
380  fitWcs = fitRes.wcs
381  scatterOnSky = fitRes.scatterOnSky
382  if debug.display:
383  frame = int(debug.frame)
385  refCat=refCat,
386  sourceCat=matchRes.usableSourceCat,
387  matches=matches,
388  exposure=exposure,
389  bbox=bbox,
390  frame=frame + 2,
391  title="Fit TAN-SIP WCS",
392  )
393 
394  return pipeBase.Struct(
395  matches=matches,
396  wcs=fitWcs,
397  scatterOnSky=scatterOnSky,
398  match_tolerance=matchRes.match_tolerance,
399  )
400 
401  def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn):
402  """Remove magnitude outliers, computing a simple zeropoint.
403 
404  Parameters
405  ----------
406  sourceFluxField : `str`
407  Field in source catalog for instrumental fluxes.
408  refFluxField : `str`
409  Field in reference catalog for fluxes (nJy).
410  matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
411  List of source/reference matches input
412 
413  Returns
414  -------
415  matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
416  List of source/reference matches with magnitude
417  outliers removed.
418  """
419  nMatch = len(matchesIn)
420  sourceMag = np.zeros(nMatch)
421  refMag = np.zeros(nMatch)
422  for i, match in enumerate(matchesIn):
423  sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
424  refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
425 
426  deltaMag = refMag - sourceMag
427  # Protect against negative fluxes and nans in the reference catalog.
428  goodDelta, = np.where(np.isfinite(deltaMag))
429  zp = np.median(deltaMag[goodDelta])
430  # Use median absolute deviation (MAD) for zpSigma.
431  # Also require a minimum scatter to prevent floating-point errors from
432  # rejecting objects in zero-noise tests.
433  zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale='normal'),
434  1e-3,
435  None)
436 
437  self.log.info("Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
438  zp, zpSigma)
439 
440  goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
441  <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
442 
443  nOutlier = nMatch - goodStars.size
444  self.log.info("Removed %d magnitude outliers out of %d total astrometry matches.",
445  nOutlier, nMatch)
446 
447  matchesOut = []
448  for matchInd in goodStars:
449  matchesOut.append(matchesIn[matchInd])
450 
451  return matchesOut
def solve(self, exposure, sourceCat)
Definition: astrometry.py:172
def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn)
Definition: astrometry.py:401
def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
Definition: astrometry.py:304
def run(self, sourceCat, exposure)
Definition: astrometry.py:122
def __init__(self, refObjLoader, schema=None, **kwargs)
Definition: astrometry.py:110
def _computeMatchStatsOnSky(self, matchList)
Definition: ref_match.py:207
def loadAndMatch(self, exposure, sourceCat)
Definition: ref_match.py:116
def _getExposureMetadata(self, exposure)
Definition: ref_match.py:236
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:34