23 __all__ = [
"AstrometryConfig",
"AstrometryTask"]
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",
74 """Match an input source catalog with objects from a reference catalog and
77 This task is broken into two main subasks: matching and WCS fitting which
78 are very interactive. The matching here can be considered in part a first
79 pass WCS fitter due to the fitter's sensitivity to outliers.
83 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
84 A reference object loader object
85 schema : `lsst.afw.table.Schema`
86 Used to set "calib_astrometry_used" flag in output source catalog.
88 additional keyword arguments for pipe_base
89 `lsst.pipe.base.Task.__init__`
91 ConfigClass = AstrometryConfig
92 _DefaultName =
"astrometricSolver"
94 def __init__(self, refObjLoader, schema=None, **kwargs):
95 RefMatchTask.__init__(self, refObjLoader, **kwargs)
97 if schema
is not None:
98 self.
usedKeyusedKey = schema.addField(
"calib_astrometry_used", type=
"Flag",
99 doc=
"set if source was used in astrometric calibration")
103 self.makeSubtask(
"wcsFitter")
106 def run(self, sourceCat, exposure):
107 """Load reference objects, match sources and optionally fit a WCS.
109 This is a thin layer around solve or loadAndMatch, depending on
110 config.forceKnownWcs.
114 exposure : `lsst.afw.image.Exposure`
115 exposure whose WCS is to be fit
116 The following are read only:
119 - photoCalib (may be absent)
120 - filter (may be unset)
121 - detector (if wcs is pure tangent; may be absent)
123 The following are updated:
125 - wcs (the initial value is used as an initial guess, and is
128 sourceCat : `lsst.afw.table.SourceCatalog`
129 catalog of sources detected on the exposure
133 result : `lsst.pipe.base.Struct`
136 - ``refCat`` : reference object catalog of objects that overlap the
137 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
138 - ``matches`` : astrometric matches
139 (`list` of `lsst.afw.table.ReferenceMatch`).
140 - ``scatterOnSky`` : median on-sky separation between reference
141 objects and sources in "matches"
142 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
143 - ``matchMeta`` : metadata needed to unpersist matches
144 (`lsst.daf.base.PropertyList`)
147 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
148 if self.config.forceKnownWcs:
149 res = self.
loadAndMatchloadAndMatch(exposure=exposure, sourceCat=sourceCat)
150 res.scatterOnSky =
None
152 res = self.
solvesolve(exposure=exposure, sourceCat=sourceCat)
156 def solve(self, exposure, sourceCat):
157 """Load reference objects overlapping an exposure, match to sources and
162 result : `lsst.pipe.base.Struct`
163 Result struct with components:
165 - ``refCat`` : reference object catalog of objects that overlap the
166 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
167 - ``matches`` : astrometric matches
168 (`list` of `lsst.afw.table.ReferenceMatch`).
169 - ``scatterOnSky`` : median on-sky separation between reference
170 objects and sources in "matches" (`lsst.geom.Angle`)
171 - ``matchMeta`` : metadata needed to unpersist matches
172 (`lsst.daf.base.PropertyList`)
176 ignores config.forceKnownWcs
179 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
185 sourceSelection = self.sourceSelector.
run(sourceCat)
187 self.log.
info(
"Purged %d sources, leaving %d good sources" %
188 (len(sourceCat) - len(sourceSelection.sourceCat),
189 len(sourceSelection.sourceCat)))
194 filterName=expMd.filterName,
195 photoCalib=expMd.photoCalib,
199 refSelection = self.referenceSelector.
run(loadRes.refCat)
201 matchMeta = self.
refObjLoaderrefObjLoader.getMetadataBox(
204 filterName=expMd.filterName,
205 photoCalib=expMd.photoCalib,
210 frame = int(debug.frame)
212 refCat=refSelection.sourceCat,
213 sourceCat=sourceSelection.sourceCat,
217 title=
"Reference catalog",
222 match_tolerance =
None
223 for i
in range(self.config.maxIter):
227 refCat=refSelection.sourceCat,
229 goodSourceCat=sourceSelection.sourceCat,
230 refFluxField=loadRes.fluxField,
234 match_tolerance=match_tolerance,
236 except Exception
as e:
239 self.log.
info(
"Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
245 match_tolerance = tryRes.match_tolerance
248 "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
249 "max match distance = %0.3f arcsec",
250 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
251 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
253 maxMatchDist = tryMatchDist.maxMatchDist
256 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
258 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
259 "that's good enough",
260 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
262 match_tolerance.maxMatchDist = maxMatchDist
265 "Matched and fit WCS in %d iterations; "
266 "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
267 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
268 tryMatchDist.distStdDev.asArcseconds()))
269 for m
in res.matches:
271 m.second.set(self.
usedKeyusedKey,
True)
272 exposure.setWcs(res.wcs)
274 return pipeBase.Struct(
275 refCat=refSelection.sourceCat,
277 scatterOnSky=res.scatterOnSky,
282 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
284 """Match sources to reference objects and fit a WCS.
288 refCat : `lsst.afw.table.SimpleCatalog`
289 catalog of reference objects
290 sourceCat : `lsst.afw.table.SourceCatalog`
291 catalog of sources detected on the exposure
292 goodSourceCat : `lsst.afw.table.SourceCatalog`
293 catalog of down-selected good sources detected on the exposure
295 field of refCat to use for flux
296 bbox : `lsst.geom.Box2I`
297 bounding box of exposure
298 wcs : `lsst.afw.geom.SkyWcs`
299 initial guess for WCS of exposure
300 match_tolerance : `lsst.meas.astrom.MatchTolerance`
301 a MatchTolerance object (or None) specifying
302 internal tolerances to the matcher. See the MatchTolerance
303 definition in the respective matcher for the class definition.
304 exposure : `lsst.afw.image.Exposure`
305 exposure whose WCS is to be fit, or None; used only for the debug
310 result : `lsst.pipe.base.Struct`
311 Result struct with components:
313 - ``matches``: astrometric matches
314 (`list` of `lsst.afw.table.ReferenceMatch`).
315 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
316 - ``scatterOnSky`` : median on-sky separation between reference
317 objects and sources in "matches" (`lsst.afw.geom.Angle`).
322 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
324 matchRes = self.matcher.matchObjectsToSources(
326 sourceCat=goodSourceCat,
328 sourceFluxField=sourceFluxField,
329 refFluxField=refFluxField,
330 match_tolerance=match_tolerance,
332 self.log.
debug(
"Found %s matches", len(matchRes.matches))
334 frame = int(debug.frame)
337 sourceCat=matchRes.usableSourceCat,
338 matches=matchRes.matches,
345 self.log.
debug(
"Fitting WCS")
346 fitRes = self.wcsFitter.fitWcs(
347 matches=matchRes.matches,
355 scatterOnSky = fitRes.scatterOnSky
357 frame = int(debug.frame)
360 sourceCat=matchRes.usableSourceCat,
361 matches=matchRes.matches,
365 title=
"Fit TAN-SIP WCS",
368 return pipeBase.Struct(
369 matches=matchRes.matches,
371 scatterOnSky=scatterOnSky,
372 match_tolerance=matchRes.match_tolerance,
def solve(self, exposure, sourceCat)
def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance, exposure=None)
def run(self, sourceCat, exposure)
def __init__(self, refObjLoader, schema=None, **kwargs)
def _computeMatchStatsOnSky(self, matchList)
def loadAndMatch(self, exposure, sourceCat)
def _getExposureMetadata(self, exposure)
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)