181 def run(self, sourceCat, exposure):
182 """Load reference objects, match sources and optionally fit a WCS.
184 This is a thin layer around solve or loadAndMatch, depending on
185 config.forceKnownWcs.
189 exposure : `lsst.afw.image.Exposure`
190 exposure whose WCS is to be fit
191 The following are read only:
194 - filter (may be unset)
195 - detector (if wcs is pure tangent; may be absent)
197 The following are updated:
199 - wcs (the initial value is used as an initial guess, and is
202 sourceCat : `lsst.afw.table.SourceCatalog`
203 catalog of sources detected on the exposure
207 result : `lsst.pipe.base.Struct`
210 - ``refCat`` : reference object catalog of objects that overlap the
211 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
212 - ``matches`` : astrometric matches
213 (`list` of `lsst.afw.table.ReferenceMatch`).
214 - ``scatterOnSky`` : median on-sky separation between reference
215 objects and sources in "matches"
216 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
217 - ``matchMeta`` : metadata needed to unpersist matches
218 (`lsst.daf.base.PropertyList`)
221 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
222 if self.config.forceKnownWcs:
223 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
224 res.scatterOnSky =
None
226 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
230 def solve(self, exposure, sourceCat):
231 """Load reference objects overlapping an exposure, match to sources and
236 result : `lsst.pipe.base.Struct`
237 Result struct with components:
239 - ``refCat`` : reference object catalog of objects that overlap the
240 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
241 - ``matches`` : astrometric matches
242 (`list` of `lsst.afw.table.ReferenceMatch`).
243 - ``scatterOnSky`` : median on-sky separation between reference
244 objects and sources in "matches" (`lsst.geom.Angle`)
245 - ``matchMeta`` : metadata needed to unpersist matches
246 (`lsst.daf.base.PropertyList`)
251 If the measured mean on-sky distance between the matched source and
252 reference objects is greater than
253 ``self.config.maxMeanDistanceArcsec``.
257 ignores config.forceKnownWcs
260 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
264 epoch = exposure.visitInfo.date.toAstropy()
265 band = exposure.filter.bandLabel
267 sourceSelection = self.sourceSelector.
run(sourceCat)
269 self.log.info(
"Purged %d sources, leaving %d good sources",
270 len(sourceCat) - len(sourceSelection.sourceCat),
271 len(sourceSelection.sourceCat))
272 if len(sourceSelection.sourceCat) == 0:
274 "No good sources selected for astrometry.",
275 lenSourceSelectionCat=len(sourceSelection.sourceCat)
279 bbox=exposure.getBBox(),
285 refSelection = self.referenceSelector.
run(loadResult.refCat, exposure=exposure)
286 refCat = refSelection.sourceCat
288 if self.config.doFiducialZeroPointCull:
291 loadResult.fluxField,
292 refCat, sourceSelection.sourceCat,
293 exposure.visitInfo.getExposureTime()
298 if not refCat.isContiguous():
299 refCat = refCat.copy(deep=
True)
302 frame = int(debug.frame)
305 sourceCat=sourceSelection.sourceCat,
307 bbox=exposure.getBBox(),
309 title=
"Reference catalog",
312 result = pipeBase.Struct(matchTolerance=
None)
313 maxMatchDistance = np.inf
315 while (maxMatchDistance > self.config.minMatchDistanceArcSec
and i < self.config.maxIter):
321 goodSourceCat=sourceSelection.sourceCat,
322 refFluxField=loadResult.fluxField,
323 bbox=exposure.getBBox(),
326 matchTolerance=result.matchTolerance,
328 exposure.setWcs(result.wcs)
330 e._metadata[
'iterations'] = i
331 sourceCat[
"coord_ra"] = np.nan
332 sourceCat[
"coord_dec"] = np.nan
333 exposure.setWcs(
None)
334 self.log.error(
"Failure fitting astrometry. %s: %s", type(e).__name__, e)
338 maxMatchDistance = result.stats.maxMatchDist.asArcseconds()
339 distMean = result.stats.distMean.asArcseconds()
340 distStdDev = result.stats.distStdDev.asArcseconds()
341 self.log.info(
"Astrometric fit iteration %d: found %d matches with mean separation "
342 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec.",
343 i, len(result.matches), distMean, distStdDev, maxMatchDistance)
348 md = exposure.getMetadata()
349 md[
'SFM_ASTROM_OFFSET_MEAN'] = distMean
350 md[
'SFM_ASTROM_OFFSET_STD'] = distStdDev
351 md[
'SFM_ASTROM_OFFSET_MEDIAN'] = result.scatterOnSky.asArcseconds()
354 for m
in result.matches:
355 m.second.set(self.
usedKey,
True)
358 bbox=exposure.getBBox(),
364 return pipeBase.Struct(
366 matches=result.matches,
367 scatterOnSky=result.scatterOnSky,
371 def check(self, exposure, sourceCat, nMatches):
372 """Validate the astrometric fit against the maxMeanDistance threshold.
374 If the distMean metric does not satisfy the requirement of being less
375 than the value set in config.maxMeanDistanceArcsec, the WCS on the
376 exposure will be set to None and the coordinate values in the
377 source catalog will be set to NaN to reflect a failed astrometric fit.
381 exposure : `lsst.afw.image.Exposure`
382 The exposure whose astrometric fit is being evaluated.
383 sourceCat : `lsst.afw.table.SourceCatalog`
384 The catalog of sources associated with the exposure.
386 The number of matches that were found and used during
387 the astrometric fit (for logging purposes only).
392 If the measured mean on-sky distance between the matched source and
393 reference objects is greater than
394 ``self.config.maxMeanDistanceArcsec``.
397 md = exposure.getMetadata()
398 distMean = md[
'SFM_ASTROM_OFFSET_MEAN']
399 distMedian = md[
'SFM_ASTROM_OFFSET_MEDIAN']
400 if distMean > self.config.maxMeanDistanceArcsec:
402 maxMeanDist=self.config.maxMeanDistanceArcsec,
403 distMedian=distMedian)
404 exposure.setWcs(
None)
405 sourceCat[
"coord_ra"] = np.nan
406 sourceCat[
"coord_dec"] = np.nan
407 self.log.error(exception)
412 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance,
414 """Match sources to reference objects and fit a WCS.
418 refCat : `lsst.afw.table.SimpleCatalog`
419 catalog of reference objects
420 sourceCat : `lsst.afw.table.SourceCatalog`
421 catalog of sources detected on the exposure
422 goodSourceCat : `lsst.afw.table.SourceCatalog`
423 catalog of down-selected good sources detected on the exposure
425 field of refCat to use for flux
426 bbox : `lsst.geom.Box2I`
427 bounding box of exposure
428 wcs : `lsst.afw.geom.SkyWcs`
429 initial guess for WCS of exposure
430 matchTolerance : `lsst.meas.astrom.MatchTolerance`
431 a MatchTolerance object (or None) specifying
432 internal tolerances to the matcher. See the MatchTolerance
433 definition in the respective matcher for the class definition.
434 exposure : `lsst.afw.image.Exposure`, optional
435 exposure whose WCS is to be fit, or None; used only for the debug
440 result : `lsst.pipe.base.Struct`
441 Result struct with components:
443 - ``matches``: astrometric matches
444 (`list` of `lsst.afw.table.ReferenceMatch`).
445 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
446 - ``scatterOnSky`` : median on-sky separation between reference
447 objects and sources in "matches" (`lsst.afw.geom.Angle`).
452 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
454 matchRes = self.matcher.matchObjectsToSources(
456 sourceCat=goodSourceCat,
458 sourceFluxField=sourceFluxField,
459 refFluxField=refFluxField,
460 matchTolerance=matchTolerance,
463 self.log.debug(
"Found %s matches", len(matchRes.matches))
465 frame = int(debug.frame)
468 sourceCat=matchRes.usableSourceCat,
469 matches=matchRes.matches,
476 if self.config.doMagnitudeOutlierRejection:
479 matches = matchRes.matches
481 self.log.debug(
"Fitting WCS")
482 fitRes = self.wcsFitter.fitWcs(
491 scatterOnSky = fitRes.scatterOnSky
493 frame = int(debug.frame)
496 sourceCat=matchRes.usableSourceCat,
501 title=f
"Fitter: {self.wcsFitter._DefaultName}",
504 return pipeBase.Struct(
507 scatterOnSky=scatterOnSky,
508 matchTolerance=matchRes.matchTolerance,
512 """Remove magnitude outliers, computing a simple zeropoint.
516 sourceFluxField : `str`
517 Field in source catalog for instrumental fluxes.
519 Field in reference catalog for fluxes (nJy).
520 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
521 List of source/reference matches input
525 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
526 List of source/reference matches with magnitude
529 nMatch = len(matchesIn)
530 sourceMag = np.zeros(nMatch)
531 refMag = np.zeros(nMatch)
532 for i, match
in enumerate(matchesIn):
533 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
534 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
536 deltaMag = refMag - sourceMag
538 goodDelta, = np.where(np.isfinite(deltaMag))
539 zp = np.median(deltaMag[goodDelta])
543 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
547 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
550 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
551 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
553 nOutlier = nMatch - goodStars.size
554 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
557 matchesOut = [matchesIn[idx]
for idx
in goodStars]
562 """Compute the median ref flux - source filter color difference.
564 The median difference in color between the flux field used for
565 selection and that of the observations being calibrated is computed
566 from the values for each in the reference catalog.
571 Bandpass of observed data.
573 Name of the flux field used in the reference catalog.
574 refCat : `lsst.afw.table.SimpleCatalog`
575 Catalog of reference objects from which to compute the color
580 refColorDelta : `float`
581 The median color difference.
583 if band
in self.config.filterMap:
584 refFilterNameForBand = self.config.filterMap.get(band) +
"_flux"
585 refCatTemp = refCat[(np.isfinite(refCat[refFluxField])
586 & np.isfinite(refCat[refFilterNameForBand]))].copy(deep=
True)
587 if len(refCatTemp) < 3:
588 refColorDeltaDefaults = self.config.refColorDeltaDefaults
589 if band
in refColorDeltaDefaults:
590 refColorDelta = refColorDeltaDefaults[band]
591 self.log.warning(
"Not enough reference sources with finite fluxes in %s and %s, "
592 "so can't compute color shift; a default vaulue of %.2f will "
593 "be applied.", refFluxField, refFilterNameForBand, refColorDelta)
595 refMag = (refCatTemp[refFluxField]*units.nJy).to_value(units.ABmag)
596 refMagSrcBand = (refCatTemp[refFilterNameForBand]*units.nJy).to_value(units.ABmag)
597 refColorDelta = np.nanmedian(refMag - refMagSrcBand)
598 self.log.info(
"Adjusting refCat cutoffs for color shift: %s - %s = %.2f.",
599 refFluxField, refFilterNameForBand, refColorDelta)
602 self.log.warning(
"Band %s not found in filterMap: %s. No adjustment for filter "
603 "differences between reference and source catalogs attempted.",
604 band, self.config.filterMap)
608 """Perform a culling of the catalogs to attempt to match their
609 effective magnitude ranges.
611 This uses a fiducial zeropoint along with the exposure time for
612 the observations to compute our best-guess magnitudes. Also,
613 accommodation is made for the median difference in color between
614 the flux field used for selection and that of the observations being
615 calibrated, which is computed from the values for each in the reference
621 Bandpass of observed data.
623 Name of the flux field used in the reference catalog.
624 refCat : `lsst.afw.table.SimpleCatalog`
625 Catalog of reference objects to be passed to the matcher. Modified
627 sourceCat : `lsst.afw.table.SourceCatalog`
628 Catalog of observed sources to be passed to the matcher. Modified
631 Exposure time of the observation being calibrated.
635 refColorDelta : `float`
636 The median color difference.
638 nRefCatPreCull = len(refCat)
639 nSelectedSourceCat = len(sourceCat)
641 psfFlux = sourceCat[
"base_PsfFlux_instFlux"]
642 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
643 psfMag = -2.5*np.log10(psfFlux) + fiducialZeroPoint + 2.5*np.log10(expTime)
644 sourceMagMin = min(psfMag) - self.config.cullMagBuffer
645 sourceMagMax = max(psfMag) + self.config.cullMagBuffer
650 if refColorDelta > self.config.cullMagBuffer:
652 sourceMagMin += 0.5*refColorDelta
653 sourceMagMax += 0.5*refColorDelta
654 if refColorDelta > 3.0*self.config.cullMagBuffer:
657 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
658 sourceMagMax += 0.5*refColorDelta
659 if refColorDelta < -1.0*self.config.cullMagBuffer:
663 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
664 sourceMagMax += refColorDelta
665 self.log.debug(
"Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
666 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
672 if nRefCatPreCull/nSelectedSourceCat > self.config.maxRefToSourceRatio:
673 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
675 self.log.info(
"Source selection: sourceMag (min, max) = (%.3f, %.3f)", sourceMagMin, sourceMagMax)
676 refCat = refCat[np.isfinite(refCat[refFluxField])]
677 refMag = (refCat[refFluxField]*units.nJy).to_value(units.ABmag)
678 refCat = refCat[(refMag < sourceMagMax) & (refMag > sourceMagMin)]
679 refMagMin = np.nanmin(refMag)
680 refMagMax = np.nanmax(refMag)
682 goodSources = ((psfMag < refMagMax - refColorDelta) & (psfMag > refMagMin - refColorDelta))
683 if len(goodSources) < np.sum(goodSources):
684 sourceCat = sourceCat[goodSources].copy(deep=
True)
685 nSelectedSourceCat = len(sourceCat)
686 self.log.debug(
"Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
687 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
689 self.log.info(
"Final: Selected %d/%d reference sources based on fiducial zeropoint culling. "
690 "Mag range in %s = (%.2f, %.2f)", len(refCat), nRefCatPreCull,
691 refFluxField, refMagMin, refMagMax)
692 return refCat, sourceCat