23 __all__ = [
"AstrometryConfig",
"AstrometryTask"]
27 import lsst.pipe.base
as pipeBase
28 from .ref_match
import RefMatchTask, RefMatchConfig
29 from .fitTanSipWcs
import FitTanSipWcsTask
30 from .display
import displayAstrometry
34 """Config for AstrometryTask. 36 wcsFitter = pexConfig.ConfigurableField(
37 target=FitTanSipWcsTask,
40 forceKnownWcs = pexConfig.Field(
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'",
46 maxIter = pexConfig.RangeField(
47 doc=
"maximum number of iterations of match sources and fit WCS" 48 "ignored if not fitting a WCS",
53 minMatchDistanceArcSec = pexConfig.RangeField(
54 doc=
"the match distance below which further iteration is pointless (arcsec); " 55 "ignored if not fitting a WCS",
63 """Match an input source catalog with objects from a reference catalog and 66 # TODO: DM-16868 remove explicit and unused schema from class input. 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 75 additional keyword arguments for pipe_base 76 `lsst.pipe.base.Task.__init__` 78 ConfigClass = AstrometryConfig
79 _DefaultName =
"astrometricSolver" 81 def __init__(self, refObjLoader, schema=None, **kwargs):
82 RefMatchTask.__init__(self, refObjLoader, schema=schema, **kwargs)
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")
90 self.makeSubtask(
"wcsFitter")
93 def run(self, sourceCat, exposure):
94 """Load reference objects, match sources and optionally fit a WCS. 96 This is a thin layer around solve or loadAndMatch, depending on 101 exposure : `lsst.afw.image.Exposure` 102 exposure whose WCS is to be fit 103 The following are read only: 106 - calib (may be absent) 107 - filter (may be unset) 108 - detector (if wcs is pure tangent; may be absent) 110 The following are updated: 112 - wcs (the initial value is used as an initial guess, and is 115 sourceCat : `lsst.afw.table.SourceCatalog` 116 catalog of sources detected on the exposure 120 result : `lsst.pipe.base.Struct` 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`) 133 if self.config.forceKnownWcs:
134 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
135 res.scatterOnSky =
None 137 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
141 def solve(self, exposure, sourceCat):
142 """Load reference objects overlapping an exposure, match to sources and 147 result : `lsst.pipe.base.Struct` 148 Result struct with components: 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`) 161 ignores config.forceKnownWcs 171 filterName=expMd.filterName,
178 filterName=expMd.filterName,
184 frame = int(debug.frame)
186 refCat=loadRes.refCat,
191 title=
"Reference catalog",
196 match_tolerance =
None 197 for i
in range(self.config.maxIter):
201 refCat=loadRes.refCat,
203 refFluxField=loadRes.fluxField,
207 match_tolerance=match_tolerance,
209 except Exception
as e:
212 self.log.info(
"Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
218 match_tolerance = tryRes.match_tolerance
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())
226 maxMatchDist = tryMatchDist.maxMatchDist
229 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
231 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; " 232 "that's good enough",
233 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
235 match_tolerance.maxMatchDist = maxMatchDist
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:
244 m.second.set(self.
usedKey,
True)
245 exposure.setWcs(res.wcs)
247 return pipeBase.Struct(
248 refCat=loadRes.refCat,
250 scatterOnSky=res.scatterOnSky,
255 def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance,
257 """Match sources to reference objects and fit a WCS. 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 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 281 result : `lsst.pipe.base.Struct` 282 Result struct with components: 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`). 292 matchRes = self.matcher.matchObjectsToSources(
296 refFluxField=refFluxField,
297 match_tolerance=match_tolerance,
299 self.log.debug(
"Found %s matches", len(matchRes.matches))
301 frame = int(debug.frame)
304 sourceCat=matchRes.usableSourceCat,
305 matches=matchRes.matches,
312 self.log.debug(
"Fitting WCS")
313 fitRes = self.wcsFitter.fitWcs(
314 matches=matchRes.matches,
322 scatterOnSky = fitRes.scatterOnSky
324 frame = int(debug.frame)
327 sourceCat=matchRes.usableSourceCat,
328 matches=matchRes.matches,
332 title=
"Fit TAN-SIP WCS",
335 return pipeBase.Struct(
336 matches=matchRes.matches,
338 scatterOnSky=scatterOnSky,
339 match_tolerance=matchRes.match_tolerance,
def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
def solve(self, exposure, sourceCat)
def _computeMatchStatsOnSky(self, matchList)
def _getExposureMetadata(self, exposure)
def run(self, sourceCat, exposure)
def __init__(self, refObjLoader, schema=None, kwargs)
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
def loadAndMatch(self, exposure, sourceCat)