LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
astrometry.py
Go to the documentation of this file.
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
25import numpy as np
26from astropy import units
27import scipy.stats
28
29import lsst.pex.config as pexConfig
30import lsst.pipe.base as pipeBase
31from lsst.utils.timer import timeMethod
32from .ref_match import RefMatchTask, RefMatchConfig
33from .fitTanSipWcs import FitTanSipWcsTask
34from .display import displayAstrometry
35
36
38 """Config for AstrometryTask.
39 """
40 wcsFitter = pexConfig.ConfigurableField(
41 target=FitTanSipWcsTask,
42 doc="WCS fitter",
43 )
44 forceKnownWcs = pexConfig.Field(
45 dtype=bool,
46 doc="If True then load reference objects and match sources but do not fit a WCS; "
47 "this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
48 default=False,
49 )
50 maxIter = pexConfig.RangeField(
51 doc="maximum number of iterations of match sources and fit WCS"
52 "ignored if not fitting a WCS",
53 dtype=int,
54 default=3,
55 min=1,
56 )
57 minMatchDistanceArcSec = pexConfig.RangeField(
58 doc="the match distance below which further iteration is pointless (arcsec); "
59 "ignored if not fitting a WCS",
60 dtype=float,
61 default=0.001,
62 min=0,
63 )
64 maxMeanDistanceArcsec = pexConfig.RangeField(
65 doc="Maximum mean on-sky distance (in arcsec) between matched source and rerference "
66 "objects post-fit. A mean distance greater than this threshold raises a TaskError "
67 "and the WCS fit is considered a failure. The default is set to the maximum tolerated "
68 "by the external global calibration (e.g. jointcal) step for conceivable recovery. "
69 "Appropriate value will be dataset and workflow dependent.",
70 dtype=float,
71 default=0.5,
72 min=0,
73 )
74 doMagnitudeOutlierRejection = pexConfig.Field(
75 dtype=bool,
76 doc=("If True then a rough zeropoint will be computed from matched sources "
77 "and outliers will be rejected in the iterations."),
78 default=False,
79 )
80 magnitudeOutlierRejectionNSigma = pexConfig.Field(
81 dtype=float,
82 doc=("Number of sigma (measured from the distribution) in magnitude "
83 "for a potential reference/source match to be rejected during "
84 "iteration."),
85 default=3.0,
86 )
87
88 def setDefaults(self):
89 # Override the default source selector for astrometry tasks
90 self.sourceFluxTypesourceFluxTypesourceFluxType = "Ap"
91
92 self.sourceSelectorsourceSelector.name = "matcher"
93 self.sourceSelectorsourceSelector["matcher"].sourceFluxType = self.sourceFluxTypesourceFluxTypesourceFluxType
94
95 # Note that if the matcher is MatchOptimisticBTask, then the
96 # default should be self.sourceSelector['matcher'].excludePixelFlags = False
97 # However, there is no way to do this automatically.
98
99
101 """Match an input source catalog with objects from a reference catalog and
102 solve for the WCS.
103
104 This task is broken into two main subasks: matching and WCS fitting which
105 are very interactive. The matching here can be considered in part a first
106 pass WCS fitter due to the fitter's sensitivity to outliers.
107
108 Parameters
109 ----------
110 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
111 A reference object loader object
112 schema : `lsst.afw.table.Schema`
113 Used to set "calib_astrometry_used" flag in output source catalog.
114 **kwargs
115 additional keyword arguments for pipe_base
116 `lsst.pipe.base.Task.__init__`
117 """
118 ConfigClass = AstrometryConfig
119 _DefaultName = "astrometricSolver"
120
121 def __init__(self, refObjLoader, schema=None, **kwargs):
122 RefMatchTask.__init__(self, refObjLoader, **kwargs)
123
124 if schema is not None:
125 self.usedKeyusedKey = schema.addField("calib_astrometry_used", type="Flag",
126 doc="set if source was used in astrometric calibration")
127 else:
128 self.usedKeyusedKey = None
129
130 self.makeSubtask("wcsFitter")
131
132 @timeMethod
133 def run(self, sourceCat, exposure):
134 """Load reference objects, match sources and optionally fit a WCS.
135
136 This is a thin layer around solve or loadAndMatch, depending on
137 config.forceKnownWcs.
138
139 Parameters
140 ----------
141 exposure : `lsst.afw.image.Exposure`
142 exposure whose WCS is to be fit
143 The following are read only:
144
145 - bbox
146 - filter (may be unset)
147 - detector (if wcs is pure tangent; may be absent)
148
149 The following are updated:
150
151 - wcs (the initial value is used as an initial guess, and is
152 required)
153
154 sourceCat : `lsst.afw.table.SourceCatalog`
155 catalog of sources detected on the exposure
156
157 Returns
158 -------
159 result : `lsst.pipe.base.Struct`
160 with these fields:
161
162 - ``refCat`` : reference object catalog of objects that overlap the
163 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
164 - ``matches`` : astrometric matches
165 (`list` of `lsst.afw.table.ReferenceMatch`).
166 - ``scatterOnSky`` : median on-sky separation between reference
167 objects and sources in "matches"
168 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
169 - ``matchMeta`` : metadata needed to unpersist matches
171 """
172 if self.refObjLoaderrefObjLoader is None:
173 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
174 if self.config.forceKnownWcs:
175 res = self.loadAndMatchloadAndMatch(exposure=exposure, sourceCat=sourceCat)
176 res.scatterOnSky = None
177 else:
178 res = self.solvesolve(exposure=exposure, sourceCat=sourceCat)
179 return res
180
181 @timeMethod
182 def solve(self, exposure, sourceCat):
183 """Load reference objects overlapping an exposure, match to sources and
184 fit a WCS
185
186 Returns
187 -------
188 result : `lsst.pipe.base.Struct`
189 Result struct with components:
190
191 - ``refCat`` : reference object catalog of objects that overlap the
192 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
193 - ``matches`` : astrometric matches
194 (`list` of `lsst.afw.table.ReferenceMatch`).
195 - ``scatterOnSky`` : median on-sky separation between reference
196 objects and sources in "matches" (`lsst.geom.Angle`)
197 - ``matchMeta`` : metadata needed to unpersist matches
199
200 Raises
201 ------
202 TaskError
203 If the measured mean on-sky distance between the matched source and
204 reference objects is greater than
205 ``self.config.maxMeanDistanceArcsec``.
206
207 Notes
208 -----
209 ignores config.forceKnownWcs
210 """
211 if self.refObjLoaderrefObjLoader is None:
212 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
213 import lsstDebug
214 debug = lsstDebug.Info(__name__)
215
216 expMd = self._getExposureMetadata_getExposureMetadata(exposure)
217
218 sourceSelection = self.sourceSelector.run(sourceCat)
219
220 self.log.info("Purged %d sources, leaving %d good sources",
221 len(sourceCat) - len(sourceSelection.sourceCat),
222 len(sourceSelection.sourceCat))
223
224 loadRes = self.refObjLoaderrefObjLoader.loadPixelBox(
225 bbox=expMd.bbox,
226 wcs=expMd.wcs,
227 filterName=expMd.filterName,
228 epoch=expMd.epoch,
229 )
230
231 refSelection = self.referenceSelector.run(loadRes.refCat)
232
233 matchMeta = self.refObjLoaderrefObjLoader.getMetadataBox(
234 bbox=expMd.bbox,
235 wcs=expMd.wcs,
236 filterName=expMd.filterName,
237 epoch=expMd.epoch,
238 )
239
240 if debug.display:
241 frame = int(debug.frame)
243 refCat=refSelection.sourceCat,
244 sourceCat=sourceSelection.sourceCat,
245 exposure=exposure,
246 bbox=expMd.bbox,
247 frame=frame,
248 title="Reference catalog",
249 )
250
251 res = None
252 wcs = expMd.wcs
253 match_tolerance = None
254 for i in range(self.config.maxIter):
255 iterNum = i + 1
256 try:
257 tryRes = self._matchAndFitWcs_matchAndFitWcs(
258 refCat=refSelection.sourceCat,
259 sourceCat=sourceCat,
260 goodSourceCat=sourceSelection.sourceCat,
261 refFluxField=loadRes.fluxField,
262 bbox=expMd.bbox,
263 wcs=wcs,
264 exposure=exposure,
265 match_tolerance=match_tolerance,
266 )
267 except Exception as e:
268 # if we have had a succeessful iteration then use that; otherwise fail
269 if i > 0:
270 self.log.info("Fit WCS iter %d failed; using previous iteration: %s", iterNum, e)
271 iterNum -= 1
272 break
273 else:
274 raise
275
276 match_tolerance = tryRes.match_tolerance
277 tryMatchDist = self._computeMatchStatsOnSky_computeMatchStatsOnSky(tryRes.matches)
278 self.log.debug(
279 "Match and fit WCS iteration %d: found %d matches with on-sky distance mean "
280 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec",
281 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
282 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
283
284 maxMatchDist = tryMatchDist.maxMatchDist
285 res = tryRes
286 wcs = res.wcs
287 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
288 self.log.debug(
289 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
290 "that's good enough",
291 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
292 break
293 match_tolerance.maxMatchDist = maxMatchDist
294
295 self.log.info(
296 "Matched and fit WCS in %d iterations; "
297 "found %d matches with on-sky distance mean and scatter = %0.3f +- %0.3f arcsec",
298 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
299 tryMatchDist.distStdDev.asArcseconds())
300 if tryMatchDist.distMean.asArcseconds() > self.config.maxMeanDistanceArcsec:
301 raise pipeBase.TaskError(
302 "Fatal astrometry failure detected: mean on-sky distance = %0.3f arcsec > %0.3f "
303 "(maxMeanDistanceArcsec)" %
304 (tryMatchDist.distMean.asArcseconds(), self.config.maxMeanDistanceArcsec))
305 for m in res.matches:
306 if self.usedKeyusedKey:
307 m.second.set(self.usedKeyusedKey, True)
308 exposure.setWcs(res.wcs)
309
310 # Record the scatter in the exposure metadata
311 md = exposure.getMetadata()
312 md['SFM_ASTROM_OFFSET_MEAN'] = tryMatchDist.distMean.asArcseconds()
313 md['SFM_ASTROM_OFFSET_STD'] = tryMatchDist.distStdDev.asArcseconds()
314
315 return pipeBase.Struct(
316 refCat=refSelection.sourceCat,
317 matches=res.matches,
318 scatterOnSky=res.scatterOnSky,
319 matchMeta=matchMeta,
320 )
321
322 @timeMethod
323 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
324 exposure=None):
325 """Match sources to reference objects and fit a WCS.
326
327 Parameters
328 ----------
330 catalog of reference objects
331 sourceCat : `lsst.afw.table.SourceCatalog`
332 catalog of sources detected on the exposure
333 goodSourceCat : `lsst.afw.table.SourceCatalog`
334 catalog of down-selected good sources detected on the exposure
335 refFluxField : 'str'
336 field of refCat to use for flux
337 bbox : `lsst.geom.Box2I`
338 bounding box of exposure
340 initial guess for WCS of exposure
341 match_tolerance : `lsst.meas.astrom.MatchTolerance`
342 a MatchTolerance object (or None) specifying
343 internal tolerances to the matcher. See the MatchTolerance
344 definition in the respective matcher for the class definition.
345 exposure : `lsst.afw.image.Exposure`
346 exposure whose WCS is to be fit, or None; used only for the debug
347 display.
348
349 Returns
350 -------
351 result : `lsst.pipe.base.Struct`
352 Result struct with components:
353
354 - ``matches``: astrometric matches
355 (`list` of `lsst.afw.table.ReferenceMatch`).
356 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
357 - ``scatterOnSky`` : median on-sky separation between reference
358 objects and sources in "matches" (`lsst.afw.geom.Angle`).
359 """
360 import lsstDebug
361 debug = lsstDebug.Info(__name__)
362
363 sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
364
365 matchRes = self.matcher.matchObjectsToSources(
366 refCat=refCat,
367 sourceCat=goodSourceCat,
368 wcs=wcs,
369 sourceFluxField=sourceFluxField,
370 refFluxField=refFluxField,
371 match_tolerance=match_tolerance,
372 )
373 self.log.debug("Found %s matches", len(matchRes.matches))
374 if debug.display:
375 frame = int(debug.frame)
377 refCat=refCat,
378 sourceCat=matchRes.usableSourceCat,
379 matches=matchRes.matches,
380 exposure=exposure,
381 bbox=bbox,
382 frame=frame + 1,
383 title="Initial WCS",
384 )
385
386 if self.config.doMagnitudeOutlierRejection:
387 matches = self._removeMagnitudeOutliers_removeMagnitudeOutliers(sourceFluxField, refFluxField, matchRes.matches)
388 else:
389 matches = matchRes.matches
390
391 self.log.debug("Fitting WCS")
392 fitRes = self.wcsFitter.fitWcs(
393 matches=matches,
394 initWcs=wcs,
395 bbox=bbox,
396 refCat=refCat,
397 sourceCat=sourceCat,
398 exposure=exposure,
399 )
400 fitWcs = fitRes.wcs
401 scatterOnSky = fitRes.scatterOnSky
402 if debug.display:
403 frame = int(debug.frame)
405 refCat=refCat,
406 sourceCat=matchRes.usableSourceCat,
407 matches=matches,
408 exposure=exposure,
409 bbox=bbox,
410 frame=frame + 2,
411 title="Fit TAN-SIP WCS",
412 )
413
414 return pipeBase.Struct(
415 matches=matches,
416 wcs=fitWcs,
417 scatterOnSky=scatterOnSky,
418 match_tolerance=matchRes.match_tolerance,
419 )
420
421 def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn):
422 """Remove magnitude outliers, computing a simple zeropoint.
423
424 Parameters
425 ----------
426 sourceFluxField : `str`
427 Field in source catalog for instrumental fluxes.
428 refFluxField : `str`
429 Field in reference catalog for fluxes (nJy).
430 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
431 List of source/reference matches input
432
433 Returns
434 -------
435 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
436 List of source/reference matches with magnitude
437 outliers removed.
438 """
439 nMatch = len(matchesIn)
440 sourceMag = np.zeros(nMatch)
441 refMag = np.zeros(nMatch)
442 for i, match in enumerate(matchesIn):
443 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
444 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
445
446 deltaMag = refMag - sourceMag
447 # Protect against negative fluxes and nans in the reference catalog.
448 goodDelta, = np.where(np.isfinite(deltaMag))
449 zp = np.median(deltaMag[goodDelta])
450 # Use median absolute deviation (MAD) for zpSigma.
451 # Also require a minimum scatter to prevent floating-point errors from
452 # rejecting objects in zero-noise tests.
453 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale='normal'),
454 1e-3,
455 None)
456
457 self.log.info("Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
458 zp, zpSigma)
459
460 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
461 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
462
463 nOutlier = nMatch - goodStars.size
464 self.log.info("Removed %d magnitude outliers out of %d total astrometry matches.",
465 nOutlier, nMatch)
466
467 matchesOut = []
468 for matchInd in goodStars:
469 matchesOut.append(matchesIn[matchInd])
470
471 return matchesOut
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
Defines the fields and offsets for a table.
Definition: Schema.h:51
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Definition: SortedCatalog.h:42
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
A class representing an angle.
Definition: Angle.h:128
An integer coordinate rectangle.
Definition: Box.h:55
def solve(self, exposure, sourceCat)
Definition: astrometry.py:182
def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn)
Definition: astrometry.py:421
def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
Definition: astrometry.py:324
def run(self, sourceCat, exposure)
Definition: astrometry.py:133
def __init__(self, refObjLoader, schema=None, **kwargs)
Definition: astrometry.py:121
def _computeMatchStatsOnSky(self, matchList)
Definition: ref_match.py:206
def loadAndMatch(self, exposure, sourceCat)
Definition: ref_match.py:117
def _getExposureMetadata(self, exposure)
Definition: ref_match.py:235
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:34
Lightweight representation of a geometric match between two records.
Definition: Match.h:67