140 def run(self, sourceCat, exposure):
141 """Load reference objects, match sources and optionally fit a WCS.
143 This is a thin layer around solve or loadAndMatch, depending on
144 config.forceKnownWcs.
148 exposure : `lsst.afw.image.Exposure`
149 exposure whose WCS is to be fit
150 The following are read only:
153 - filter (may be unset)
154 - detector (if wcs is pure tangent; may be absent)
156 The following are updated:
158 - wcs (the initial value is used as an initial guess, and is
161 sourceCat : `lsst.afw.table.SourceCatalog`
162 catalog of sources detected on the exposure
166 result : `lsst.pipe.base.Struct`
169 - ``refCat`` : reference object catalog of objects that overlap the
170 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
171 - ``matches`` : astrometric matches
172 (`list` of `lsst.afw.table.ReferenceMatch`).
173 - ``scatterOnSky`` : median on-sky separation between reference
174 objects and sources in "matches"
175 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
176 - ``matchMeta`` : metadata needed to unpersist matches
177 (`lsst.daf.base.PropertyList`)
180 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
181 if self.config.forceKnownWcs:
182 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
183 res.scatterOnSky =
None
185 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
189 def solve(self, exposure, sourceCat):
190 """Load reference objects overlapping an exposure, match to sources and
195 result : `lsst.pipe.base.Struct`
196 Result struct with components:
198 - ``refCat`` : reference object catalog of objects that overlap the
199 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
200 - ``matches`` : astrometric matches
201 (`list` of `lsst.afw.table.ReferenceMatch`).
202 - ``scatterOnSky`` : median on-sky separation between reference
203 objects and sources in "matches" (`lsst.geom.Angle`)
204 - ``matchMeta`` : metadata needed to unpersist matches
205 (`lsst.daf.base.PropertyList`)
210 If the measured mean on-sky distance between the matched source and
211 reference objects is greater than
212 ``self.config.maxMeanDistanceArcsec``.
216 ignores config.forceKnownWcs
219 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
225 sourceSelection = self.sourceSelector.
run(sourceCat)
227 self.log.info(
"Purged %d sources, leaving %d good sources",
228 len(sourceCat) - len(sourceSelection.sourceCat),
229 len(sourceSelection.sourceCat))
234 filterName=expMd.filterName,
238 refSelection = self.referenceSelector.
run(loadRes.refCat)
243 filterName=expMd.filterName,
248 frame = int(debug.frame)
250 refCat=refSelection.sourceCat,
251 sourceCat=sourceSelection.sourceCat,
255 title=
"Reference catalog",
260 match_tolerance =
None
262 for i
in range(self.config.maxIter):
267 refCat=refSelection.sourceCat,
269 goodSourceCat=sourceSelection.sourceCat,
270 refFluxField=loadRes.fluxField,
274 match_tolerance=match_tolerance,
276 except Exception
as e:
280 self.log.info(
"Fit WCS iter %d failed; using previous iteration: %s", iterNum, e)
284 self.log.info(
"Fit WCS iter %d failed: %s" % (iterNum, e))
288 match_tolerance = tryRes.match_tolerance
291 "Match and fit WCS iteration %d: found %d matches with on-sky distance mean and "
292 "scatter = %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec",
293 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
294 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
296 maxMatchDist = tryMatchDist.maxMatchDist
299 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
301 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
302 "that's good enough",
303 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
305 match_tolerance.maxMatchDist = maxMatchDist
308 self.log.info(
"Matched and fit WCS in %d iterations; "
309 "found %d matches with mean and scatter = %0.3f +- %0.3f arcsec" %
310 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
311 tryMatchDist.distStdDev.asArcseconds()))
312 if tryMatchDist.distMean.asArcseconds() > self.config.maxMeanDistanceArcsec:
313 self.log.info(
"Assigning as a fit failure: mean on-sky distance = %0.3f arcsec > %0.3f "
314 "(maxMeanDistanceArcsec)" % (tryMatchDist.distMean.asArcseconds(),
315 self.config.maxMeanDistanceArcsec))
319 self.log.warning(
"WCS fit failed. Setting exposure's WCS to None and coord_ra & coord_dec "
320 "cols in sourceCat to nan.")
321 sourceCat[
"coord_ra"] = np.nan
322 sourceCat[
"coord_dec"] = np.nan
323 exposure.setWcs(
None)
327 for m
in res.matches:
329 m.second.set(self.
usedKey,
True)
330 exposure.setWcs(res.wcs)
331 matches = res.matches
332 scatterOnSky = res.scatterOnSky
338 md = exposure.getMetadata()
339 md[
'SFM_ASTROM_OFFSET_MEAN'] = tryMatchDist.distMean.asArcseconds()
340 md[
'SFM_ASTROM_OFFSET_STD'] = tryMatchDist.distStdDev.asArcseconds()
342 return pipeBase.Struct(
343 refCat=refSelection.sourceCat,
345 scatterOnSky=scatterOnSky,
350 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
352 """Match sources to reference objects and fit a WCS.
356 refCat : `lsst.afw.table.SimpleCatalog`
357 catalog of reference objects
358 sourceCat : `lsst.afw.table.SourceCatalog`
359 catalog of sources detected on the exposure
360 goodSourceCat : `lsst.afw.table.SourceCatalog`
361 catalog of down-selected good sources detected on the exposure
363 field of refCat to use for flux
364 bbox : `lsst.geom.Box2I`
365 bounding box of exposure
366 wcs : `lsst.afw.geom.SkyWcs`
367 initial guess for WCS of exposure
368 match_tolerance : `lsst.meas.astrom.MatchTolerance`
369 a MatchTolerance object (or None) specifying
370 internal tolerances to the matcher. See the MatchTolerance
371 definition in the respective matcher for the class definition.
372 exposure : `lsst.afw.image.Exposure`
373 exposure whose WCS is to be fit, or None; used only for the debug
378 result : `lsst.pipe.base.Struct`
379 Result struct with components:
381 - ``matches``: astrometric matches
382 (`list` of `lsst.afw.table.ReferenceMatch`).
383 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
384 - ``scatterOnSky`` : median on-sky separation between reference
385 objects and sources in "matches" (`lsst.afw.geom.Angle`).
390 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
392 matchRes = self.matcher.matchObjectsToSources(
394 sourceCat=goodSourceCat,
396 sourceFluxField=sourceFluxField,
397 refFluxField=refFluxField,
398 match_tolerance=match_tolerance,
400 self.log.debug(
"Found %s matches", len(matchRes.matches))
402 frame = int(debug.frame)
405 sourceCat=matchRes.usableSourceCat,
406 matches=matchRes.matches,
413 if self.config.doMagnitudeOutlierRejection:
416 matches = matchRes.matches
418 self.log.debug(
"Fitting WCS")
419 fitRes = self.wcsFitter.fitWcs(
428 scatterOnSky = fitRes.scatterOnSky
430 frame = int(debug.frame)
433 sourceCat=matchRes.usableSourceCat,
438 title=f
"Fitter: {self.wcsFitter._DefaultName}",
441 return pipeBase.Struct(
444 scatterOnSky=scatterOnSky,
445 match_tolerance=matchRes.match_tolerance,
449 """Remove magnitude outliers, computing a simple zeropoint.
453 sourceFluxField : `str`
454 Field in source catalog for instrumental fluxes.
456 Field in reference catalog for fluxes (nJy).
457 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
458 List of source/reference matches input
462 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
463 List of source/reference matches with magnitude
466 nMatch = len(matchesIn)
467 sourceMag = np.zeros(nMatch)
468 refMag = np.zeros(nMatch)
469 for i, match
in enumerate(matchesIn):
470 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
471 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
473 deltaMag = refMag - sourceMag
475 goodDelta, = np.where(np.isfinite(deltaMag))
476 zp = np.median(deltaMag[goodDelta])
480 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
484 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
487 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
488 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
490 nOutlier = nMatch - goodStars.size
491 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
494 matchesOut = [matchesIn[idx]
for idx
in goodStars]