23__all__ = [
"AstrometryConfig",
"AstrometryTask"]
26from astropy
import units
31from lsst.utils.timer
import timeMethod
32from .ref_match
import RefMatchTask, RefMatchConfig
33from .fitTanSipWcs
import FitTanSipWcsTask
34from .display
import displayAstrometry
38 """Config for AstrometryTask.
40 wcsFitter = pexConfig.ConfigurableField(
41 target=FitTanSipWcsTask,
44 forceKnownWcs = pexConfig.Field(
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'",
50 maxIter = pexConfig.RangeField(
51 doc=
"maximum number of iterations of match sources and fit WCS"
52 "ignored if not fitting a WCS",
57 minMatchDistanceArcSec = pexConfig.RangeField(
58 doc=
"the match distance below which further iteration is pointless (arcsec); "
59 "ignored if not fitting a WCS",
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.",
74 doMagnitudeOutlierRejection = pexConfig.Field(
76 doc=(
"If True then a rough zeropoint will be computed from matched sources "
77 "and outliers will be rejected in the iterations."),
80 magnitudeOutlierRejectionNSigma = pexConfig.Field(
82 doc=(
"Number of sigma (measured from the distribution) in magnitude "
83 "for a potential reference/source match to be rejected during "
101 """Match an input source catalog with objects from a reference catalog and
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.
110 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
111 A reference object loader object
113 Used to set "calib_astrometry_used" flag
in output source catalog.
115 additional keyword arguments
for pipe_base
116 `lsst.pipe.base.Task.__init__`
118 ConfigClass = AstrometryConfig
119 _DefaultName = "astrometricSolver"
121 def __init__(self, refObjLoader, schema=None, **kwargs):
122 RefMatchTask.__init__(self, refObjLoader, **kwargs)
124 if schema
is not None:
125 self.
usedKey = schema.addField(
"calib_astrometry_used", type=
"Flag",
126 doc=
"set if source was used in astrometric calibration")
130 self.makeSubtask(
"wcsFitter")
133 def run(self, sourceCat, exposure):
134 """Load reference objects, match sources and optionally fit a WCS.
136 This is a thin layer around solve
or loadAndMatch, depending on
137 config.forceKnownWcs.
142 exposure whose WCS
is to be fit
143 The following are read only:
146 - filter (may be unset)
147 - detector (
if wcs
is pure tangent; may be absent)
149 The following are updated:
151 - wcs (the initial value
is used
as an initial guess,
and is
155 catalog of sources detected on the exposure
159 result : `lsst.pipe.base.Struct`
162 - ``refCat`` : reference object catalog of objects that overlap the
164 - ``matches`` : astrometric matches
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
173 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
174 if self.config.forceKnownWcs:
175 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
176 res.scatterOnSky =
None
178 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
182 def solve(self, exposure, sourceCat):
183 """Load reference objects overlapping an exposure, match to sources and
188 result : `lsst.pipe.base.Struct`
189 Result struct with components:
191 - ``refCat`` : reference object catalog of objects that overlap the
192 exposure (
with some margin) (`lsst::afw::table::SimpleCatalog`).
193 - ``matches`` : astrometric matches
195 - ``scatterOnSky`` : median on-sky separation between reference
197 - ``matchMeta`` : metadata needed to unpersist matches
203 If the measured mean on-sky distance between the matched source
and
204 reference objects
is greater than
205 ``self.config.maxMeanDistanceArcsec``.
209 ignores config.forceKnownWcs
212 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
218 sourceSelection = self.sourceSelector.run(sourceCat)
220 self.log.info(
"Purged %d sources, leaving %d good sources",
221 len(sourceCat) - len(sourceSelection.sourceCat),
222 len(sourceSelection.sourceCat))
227 filterName=expMd.filterName,
231 refSelection = self.referenceSelector.run(loadRes.refCat)
236 filterName=expMd.filterName,
241 frame = int(debug.frame)
243 refCat=refSelection.sourceCat,
244 sourceCat=sourceSelection.sourceCat,
248 title=
"Reference catalog",
253 match_tolerance =
None
255 for i
in range(self.config.maxIter):
260 refCat=refSelection.sourceCat,
262 goodSourceCat=sourceSelection.sourceCat,
263 refFluxField=loadRes.fluxField,
267 match_tolerance=match_tolerance,
269 except Exception
as e:
273 self.log.info(
"Fit WCS iter %d failed; using previous iteration: %s", iterNum, e)
277 self.log.info(
"Fit WCS iter %d failed: %s" % (iterNum, e))
281 match_tolerance = tryRes.match_tolerance
284 "Match and fit WCS iteration %d: found %d matches with on-sky distance mean and "
285 "scatter = %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec",
286 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
287 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
289 maxMatchDist = tryMatchDist.maxMatchDist
292 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
294 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
295 "that's good enough",
296 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
298 match_tolerance.maxMatchDist = maxMatchDist
301 self.log.info(
"Matched and fit WCS in %d iterations; "
302 "found %d matches with mean and scatter = %0.3f +- %0.3f arcsec" %
303 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
304 tryMatchDist.distStdDev.asArcseconds()))
305 if tryMatchDist.distMean.asArcseconds() > self.config.maxMeanDistanceArcsec:
306 self.log.info(
"Assigning as a fit failure: mean on-sky distance = %0.3f arcsec > %0.3f "
307 "(maxMeanDistanceArcsec)" % (tryMatchDist.distMean.asArcseconds(),
308 self.config.maxMeanDistanceArcsec))
312 self.log.warning(
"WCS fit failed. Setting exposure's WCS to None and coord_ra & coord_dec "
313 "cols in sourceCat to nan.")
314 sourceCat[
"coord_ra"] = np.nan
315 sourceCat[
"coord_dec"] = np.nan
316 exposure.setWcs(
None)
320 for m
in res.matches:
322 m.second.set(self.
usedKey,
True)
323 exposure.setWcs(res.wcs)
324 matches = res.matches
325 scatterOnSky = res.scatterOnSky
331 md = exposure.getMetadata()
332 md[
'SFM_ASTROM_OFFSET_MEAN'] = tryMatchDist.distMean.asArcseconds()
333 md[
'SFM_ASTROM_OFFSET_STD'] = tryMatchDist.distStdDev.asArcseconds()
335 return pipeBase.Struct(
336 refCat=refSelection.sourceCat,
338 scatterOnSky=scatterOnSky,
343 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
345 """Match sources to reference objects and fit a WCS.
350 catalog of reference objects
352 catalog of sources detected on the exposure
354 catalog of down-selected good sources detected on the exposure
356 field of refCat to use
for flux
358 bounding box of exposure
360 initial guess
for WCS of exposure
362 a MatchTolerance object (
or None) specifying
363 internal tolerances to the matcher. See the MatchTolerance
364 definition
in the respective matcher
for the
class definition.
366 exposure whose WCS
is to be fit,
or None; used only
for the debug
371 result : `lsst.pipe.base.Struct`
372 Result struct
with components:
374 - ``matches``: astrometric matches
377 - ``scatterOnSky`` : median on-sky separation between reference
378 objects
and sources
in "matches" (`lsst.afw.geom.Angle`).
383 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
385 matchRes = self.matcher.matchObjectsToSources(
387 sourceCat=goodSourceCat,
389 sourceFluxField=sourceFluxField,
390 refFluxField=refFluxField,
391 match_tolerance=match_tolerance,
393 self.log.debug(
"Found %s matches", len(matchRes.matches))
395 frame = int(debug.frame)
398 sourceCat=matchRes.usableSourceCat,
399 matches=matchRes.matches,
406 if self.config.doMagnitudeOutlierRejection:
409 matches = matchRes.matches
411 self.log.debug(
"Fitting WCS")
412 fitRes = self.wcsFitter.fitWcs(
421 scatterOnSky = fitRes.scatterOnSky
423 frame = int(debug.frame)
426 sourceCat=matchRes.usableSourceCat,
431 title=
"Fit TAN-SIP WCS",
434 return pipeBase.Struct(
437 scatterOnSky=scatterOnSky,
438 match_tolerance=matchRes.match_tolerance,
441 def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn):
442 """Remove magnitude outliers, computing a simple zeropoint.
446 sourceFluxField : `str`
447 Field in source catalog
for instrumental fluxes.
449 Field
in reference catalog
for fluxes (nJy).
451 List of source/reference matches input
456 List of source/reference matches
with magnitude
459 nMatch = len(matchesIn)
460 sourceMag = np.zeros(nMatch)
461 refMag = np.zeros(nMatch)
462 for i, match
in enumerate(matchesIn):
463 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
464 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
466 deltaMag = refMag - sourceMag
468 goodDelta, = np.where(np.isfinite(deltaMag))
469 zp = np.median(deltaMag[goodDelta])
473 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
477 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
480 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
481 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
483 nOutlier = nMatch - goodStars.size
484 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
488 for matchInd
in goodStars:
489 matchesOut.append(matchesIn[matchInd])
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Defines the fields and offsets for a table.
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Class for storing ordered metadata with comments.
A class representing an angle.
An integer coordinate rectangle.
def solve(self, exposure, sourceCat)
def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn)
def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
def __init__(self, refObjLoader, schema=None, **kwargs)
def _computeMatchStatsOnSky(self, matchList)
def loadAndMatch(self, exposure, sourceCat)
def _getExposureMetadata(self, exposure)
Lightweight representation of a geometric match between two records.