193 def run(self, sourceCat, exposure, exposure_region=None, load_result=None):
194 """Load reference objects, match sources and optionally fit a WCS.
196 This is a thin layer around solve or loadAndMatch, depending on
197 config.forceKnownWcs.
201 exposure : `lsst.afw.image.Exposure`
202 The exposure whose WCS is to be fit.
203 The following are read only:
205 - filter (may be unset)
206 - detector (if wcs is pure tangent; may be absent)
208 The following are updated:
209 - wcs (the initial value is used as an initial guess, and is
211 sourceCat : `lsst.afw.table.SourceCatalog`
212 The catalog of sources detected on the exposure.
213 exposure_region : `lsst.sphgeom.Region`, optional
214 The exposure region to use for the for the reference catalog
215 filtering. If `None`, this region will be set as a padded bbox +
216 current WCS of the exposure.
217 load_result : `lsst.pipe.base.Struct`, optional
218 A pre-loaded reference catalog struct to use instead of loading
223 result : `lsst.pipe.base.Struct`
226 - ``refCat`` : reference object catalog of objects that overlap the
227 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
228 - ``matches`` : astrometric matches
229 (`list` of `lsst.afw.table.ReferenceMatch`).
230 - ``scatterOnSky`` : median on-sky separation between reference
231 objects and sources in "matches"
232 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
233 - ``matchMeta`` : metadata needed to unpersist matches
234 (`lsst.daf.base.PropertyList`)
235 - ``sip`` : a nested struct returned by
236 `FitSipApproximationTask.run`.
239 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
240 if self.config.forceKnownWcs:
241 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
242 res.scatterOnSky =
None
247 exposure_region=exposure_region,
248 load_result=load_result,
250 if self.config.doFitSipApproximation:
251 res.sip = self.fitSipApproximation.
run(wcs=exposure.getWcs(), bbox=exposure.getBBox())
252 exposure.setWcs(res.sip.wcs)
256 def solve(self, exposure, sourceCat, exposure_region=None, load_result=None):
257 """Load reference objects overlapping an exposure, match to sources and
262 exposure : `lsst.afw.image.Exposure`
263 The exposure whose WCS is to be fit
264 The following are read only:
266 - filter (may be unset)
267 - detector (if wcs is pure tangent; may be absent)
269 The following are updated:
270 - wcs (the initial value is used as an initial guess, and is
272 sourceCat : `lsst.afw.table.SourceCatalog`
273 The catalog of sources detected on the exposure.
274 exposure_region : `lsst.sphgeom.Region`, optional
275 The exposure region to use for the for the reference catalog
276 filtering. If `None`, this region will be set as a padded bbox +
277 current WCS of the exposure.
278 load_result : `lsst.pipe.base.Struct`, optional
279 A pre-loaded reference catalog struct to use instead of loading
284 result : `lsst.pipe.base.Struct`
285 Result struct with components:
287 - ``refCat`` : reference object catalog of objects that overlap the
288 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
289 - ``matches`` : astrometric matches
290 (`list` of `lsst.afw.table.ReferenceMatch`).
291 - ``scatterOnSky`` : median on-sky separation between reference
292 objects and sources in "matches" (`lsst.geom.Angle`)
293 - ``matchMeta`` : metadata needed to unpersist matches
294 (`lsst.daf.base.PropertyList`)
299 If the measured mean on-sky distance between the matched source and
300 reference objects is greater than
301 ``self.config.maxMeanDistanceArcsec``.
305 ignores config.forceKnownWcs
308 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or "
309 "setRefObjLoader and no pre-loaded reference catalog provided.")
313 epoch = exposure.visitInfo.date.toAstropy()
314 band = exposure.filter.bandLabel
316 sourceSelection = self.sourceSelector.
run(sourceCat)
318 self.log.info(
"Purged %d sources, leaving %d good sources",
319 len(sourceCat) - len(sourceSelection.sourceCat),
320 len(sourceSelection.sourceCat))
321 if len(sourceSelection.sourceCat) == 0:
323 "No good sources selected for astrometry.",
324 lenSourceSelectionCat=len(sourceSelection.sourceCat)
327 if load_result
is None:
330 refSelection = self.referenceSelector.
run(load_result.refCat, exposure=exposure)
331 refCat = refSelection.sourceCat
333 if self.config.doFiducialZeroPointCull:
336 load_result.fluxField,
337 refCat, sourceSelection.sourceCat,
338 exposure.visitInfo.getExposureTime()
343 if not refCat.isContiguous():
344 refCat = refCat.copy(deep=
True)
347 frame = int(debug.frame)
350 sourceCat=sourceSelection.sourceCat,
352 bbox=exposure.getBBox(),
354 title=
"Reference catalog",
357 result = pipeBase.Struct(matchTolerance=
None)
358 maxMatchDistance = np.inf
360 while (maxMatchDistance > self.config.minMatchDistanceArcSec
and i < self.config.maxIter):
366 goodSourceCat=sourceSelection.sourceCat,
367 refFluxField=load_result.fluxField,
368 bbox=exposure.getBBox(),
371 matchTolerance=result.matchTolerance,
373 exposure.setWcs(result.wcs)
375 e._metadata[
'iterations'] = i
376 sourceCat[
"coord_ra"] = np.nan
377 sourceCat[
"coord_dec"] = np.nan
378 exposure.setWcs(
None)
379 self.log.error(
"Failure fitting astrometry. %s: %s", type(e).__name__, e)
383 maxMatchDistance = result.stats.maxMatchDist.asArcseconds()
384 distMean = result.stats.distMean.asArcseconds()
385 distStdDev = result.stats.distStdDev.asArcseconds()
386 self.log.info(
"Astrometric fit iteration %d: found %d matches with mean separation "
387 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec.",
388 i, len(result.matches), distMean, distStdDev, maxMatchDistance)
393 md = exposure.getMetadata()
394 md[
'SFM_ASTROM_OFFSET_MEAN'] = distMean
395 md[
'SFM_ASTROM_OFFSET_STD'] = distStdDev
396 md[
'SFM_ASTROM_OFFSET_MEDIAN'] = result.scatterOnSky.asArcseconds()
399 for m
in result.matches:
400 m.second.set(self.
usedKey,
True)
403 bbox=exposure.getBBox(),
409 return pipeBase.Struct(
411 matches=result.matches,
412 scatterOnSky=result.scatterOnSky,
456 def check(self, exposure, sourceCat, nMatches):
457 """Validate the astrometric fit against the maxMeanDistance threshold.
459 If the distMean metric does not satisfy the requirement of being less
460 than the value set in config.maxMeanDistanceArcsec, the WCS on the
461 exposure will be set to None and the coordinate values in the
462 source catalog will be set to NaN to reflect a failed astrometric fit.
466 exposure : `lsst.afw.image.Exposure`
467 The exposure whose astrometric fit is being evaluated.
468 sourceCat : `lsst.afw.table.SourceCatalog`
469 The catalog of sources associated with the exposure.
471 The number of matches that were found and used during
472 the astrometric fit (for logging purposes only).
477 If the measured mean on-sky distance between the matched source and
478 reference objects is greater than
479 ``self.config.maxMeanDistanceArcsec``.
482 md = exposure.getMetadata()
483 distMean = md[
'SFM_ASTROM_OFFSET_MEAN']
484 distMedian = md[
'SFM_ASTROM_OFFSET_MEDIAN']
485 if distMean > self.config.maxMeanDistanceArcsec:
487 maxMeanDist=self.config.maxMeanDistanceArcsec,
488 distMedian=distMedian)
489 exposure.setWcs(
None)
490 sourceCat[
"coord_ra"] = np.nan
491 sourceCat[
"coord_dec"] = np.nan
492 self.log.error(exception)
497 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance,
499 """Match sources to reference objects and fit a WCS.
503 refCat : `lsst.afw.table.SimpleCatalog`
504 catalog of reference objects
505 sourceCat : `lsst.afw.table.SourceCatalog`
506 catalog of sources detected on the exposure
507 goodSourceCat : `lsst.afw.table.SourceCatalog`
508 catalog of down-selected good sources detected on the exposure
510 field of refCat to use for flux
511 bbox : `lsst.geom.Box2I`
512 bounding box of exposure
513 wcs : `lsst.afw.geom.SkyWcs`
514 initial guess for WCS of exposure
515 matchTolerance : `lsst.meas.astrom.MatchTolerance`
516 a MatchTolerance object (or None) specifying
517 internal tolerances to the matcher. See the MatchTolerance
518 definition in the respective matcher for the class definition.
519 exposure : `lsst.afw.image.Exposure`, optional
520 exposure whose WCS is to be fit, or None; used only for the debug
525 result : `lsst.pipe.base.Struct`
526 Result struct with components:
528 - ``matches``: astrometric matches
529 (`list` of `lsst.afw.table.ReferenceMatch`).
530 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
531 - ``scatterOnSky`` : median on-sky separation between reference
532 objects and sources in "matches" (`lsst.afw.geom.Angle`).
537 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
539 matchRes = self.matcher.matchObjectsToSources(
541 sourceCat=goodSourceCat,
543 sourceFluxField=sourceFluxField,
544 refFluxField=refFluxField,
545 matchTolerance=matchTolerance,
548 self.log.debug(
"Found %s matches", len(matchRes.matches))
550 frame = int(debug.frame)
553 sourceCat=matchRes.usableSourceCat,
554 matches=matchRes.matches,
561 if self.config.doMagnitudeOutlierRejection:
564 matches = matchRes.matches
566 self.log.debug(
"Fitting WCS")
567 fitRes = self.wcsFitter.fitWcs(
576 scatterOnSky = fitRes.scatterOnSky
578 frame = int(debug.frame)
581 sourceCat=matchRes.usableSourceCat,
586 title=f
"Fitter: {self.wcsFitter._DefaultName}",
589 return pipeBase.Struct(
592 scatterOnSky=scatterOnSky,
593 matchTolerance=matchRes.matchTolerance,
597 """Remove magnitude outliers, computing a simple zeropoint.
601 sourceFluxField : `str`
602 Field in source catalog for instrumental fluxes.
604 Field in reference catalog for fluxes (nJy).
605 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
606 List of source/reference matches input
610 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
611 List of source/reference matches with magnitude
614 nMatch = len(matchesIn)
615 sourceMag = np.zeros(nMatch)
616 refMag = np.zeros(nMatch)
617 for i, match
in enumerate(matchesIn):
618 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
619 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
621 deltaMag = refMag - sourceMag
623 goodDelta, = np.where(np.isfinite(deltaMag))
624 zp = np.median(deltaMag[goodDelta])
628 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
632 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
635 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
636 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
638 nOutlier = nMatch - goodStars.size
639 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
642 matchesOut = [matchesIn[idx]
for idx
in goodStars]
647 """Compute the median ref flux - source filter color difference.
649 The median difference in color between the flux field used for
650 selection and that of the observations being calibrated is computed
651 from the values for each in the reference catalog.
656 Bandpass of observed data.
658 Name of the flux field used in the reference catalog.
659 refCat : `lsst.afw.table.SimpleCatalog`
660 Catalog of reference objects from which to compute the color
665 refColorDelta : `float`
666 The median color difference.
668 if band
in self.config.filterMap:
669 refFilterNameForBand = self.config.filterMap.get(band) +
"_flux"
670 refCatTemp = refCat[(np.isfinite(refCat[refFluxField])
671 & np.isfinite(refCat[refFilterNameForBand]))].copy(deep=
True)
672 if len(refCatTemp) < 3:
673 refColorDeltaDefaults = self.config.refColorDeltaDefaults
674 if band
in refColorDeltaDefaults:
675 refColorDelta = refColorDeltaDefaults[band]
677 self.log.warning(
"Band %s was not found in refColorDeltaDefaults dict "
678 "(currently set as: %s. Will use a default of 0.0 (i.e. "
679 "no color shift).", band, self.config.refColorDeltaDefaults)
681 self.log.warning(
"Not enough reference sources with finite fluxes in %s and %s, "
682 "so can't compute color shift; a default vaulue of %.2f will "
683 "be applied.", refFluxField, refFilterNameForBand, refColorDelta)
685 refMag = (refCatTemp[refFluxField]*units.nJy).to_value(units.ABmag)
686 refMagSrcBand = (refCatTemp[refFilterNameForBand]*units.nJy).to_value(units.ABmag)
687 refColorDelta = np.nanmedian(refMag - refMagSrcBand)
688 self.log.info(
"Adjusting refCat cutoffs for color shift: %s - %s = %.2f.",
689 refFluxField, refFilterNameForBand, refColorDelta)
692 self.log.warning(
"Band %s not found in filterMap: %s. No adjustment for filter "
693 "differences between reference and source catalogs attempted.",
694 band, self.config.filterMap)
698 """Perform a culling of the catalogs to attempt to match their
699 effective magnitude ranges.
701 This uses a fiducial zeropoint along with the exposure time for
702 the observations to compute our best-guess magnitudes. Also,
703 accommodation is made for the median difference in color between
704 the flux field used for selection and that of the observations being
705 calibrated, which is computed from the values for each in the reference
711 Bandpass of observed data.
713 Name of the flux field used in the reference catalog.
714 refCat : `lsst.afw.table.SimpleCatalog`
715 Catalog of reference objects to be passed to the matcher. Modified
717 sourceCat : `lsst.afw.table.SourceCatalog`
718 Catalog of observed sources to be passed to the matcher. Modified
721 Exposure time of the observation being calibrated.
725 refColorDelta : `float`
726 The median color difference.
728 nRefCatPreCull = len(refCat)
729 nSelectedSourceCat = len(sourceCat)
731 psfFlux = sourceCat[
"base_PsfFlux_instFlux"]
732 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
733 psfMag = -2.5*np.log10(psfFlux) + fiducialZeroPoint + 2.5*np.log10(expTime)
734 sourceMagMin = min(psfMag) - self.config.cullMagBuffer
735 sourceMagMax = max(psfMag) + self.config.cullMagBuffer
740 if refColorDelta > self.config.cullMagBuffer:
742 sourceMagMin += 0.5*refColorDelta
743 sourceMagMax += 0.5*refColorDelta
744 if refColorDelta > 3.0*self.config.cullMagBuffer:
747 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
748 sourceMagMax += 0.5*refColorDelta
749 if refColorDelta < -1.0*self.config.cullMagBuffer:
753 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
754 sourceMagMax += refColorDelta
755 self.log.debug(
"Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
756 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
762 if nRefCatPreCull/nSelectedSourceCat > self.config.maxRefToSourceRatio:
763 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
765 self.log.info(
"Source selection: sourceMag (min, max) = (%.3f, %.3f)", sourceMagMin, sourceMagMax)
766 refCat = refCat[np.isfinite(refCat[refFluxField])]
767 refMag = (refCat[refFluxField]*units.nJy).to_value(units.ABmag)
768 refCat = refCat[(refMag < sourceMagMax) & (refMag > sourceMagMin)]
769 refMagMin = np.nanmin(refMag)
770 refMagMax = np.nanmax(refMag)
772 goodSources = ((psfMag < refMagMax - refColorDelta) & (psfMag > refMagMin - refColorDelta))
773 if len(goodSources) < np.sum(goodSources):
774 sourceCat = sourceCat[goodSources].copy(deep=
True)
775 nSelectedSourceCat = len(sourceCat)
776 self.log.debug(
"Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
777 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
779 self.log.info(
"Final: Selected %d/%d reference sources based on fiducial zeropoint culling. "
780 "Mag range in %s = (%.2f, %.2f)", len(refCat), nRefCatPreCull,
781 refFluxField, refMagMin, refMagMax)
782 return refCat, sourceCat