193 def run(self, sourceCat, exposure):
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 exposure whose WCS is to be fit
203 The following are read only:
206 - filter (may be unset)
207 - detector (if wcs is pure tangent; may be absent)
209 The following are updated:
211 - wcs (the initial value is used as an initial guess, and is
214 sourceCat : `lsst.afw.table.SourceCatalog`
215 catalog of sources detected on the exposure
219 result : `lsst.pipe.base.Struct`
222 - ``refCat`` : reference object catalog of objects that overlap the
223 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
224 - ``matches`` : astrometric matches
225 (`list` of `lsst.afw.table.ReferenceMatch`).
226 - ``scatterOnSky`` : median on-sky separation between reference
227 objects and sources in "matches"
228 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
229 - ``matchMeta`` : metadata needed to unpersist matches
230 (`lsst.daf.base.PropertyList`)
231 - ``sip`` : a nested struct returned by
232 `FitSipApproximationTask.run`.
235 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
236 if self.config.forceKnownWcs:
237 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
238 res.scatterOnSky =
None
240 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
241 if self.config.doFitSipApproximation:
242 res.sip = self.fitSipApproximation.
run(wcs=exposure.getWcs(), bbox=exposure.getBBox())
243 exposure.setWcs(res.sip.wcs)
247 def solve(self, exposure, sourceCat):
248 """Load reference objects overlapping an exposure, match to sources and
253 result : `lsst.pipe.base.Struct`
254 Result struct with components:
256 - ``refCat`` : reference object catalog of objects that overlap the
257 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
258 - ``matches`` : astrometric matches
259 (`list` of `lsst.afw.table.ReferenceMatch`).
260 - ``scatterOnSky`` : median on-sky separation between reference
261 objects and sources in "matches" (`lsst.geom.Angle`)
262 - ``matchMeta`` : metadata needed to unpersist matches
263 (`lsst.daf.base.PropertyList`)
268 If the measured mean on-sky distance between the matched source and
269 reference objects is greater than
270 ``self.config.maxMeanDistanceArcsec``.
274 ignores config.forceKnownWcs
277 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
281 epoch = exposure.visitInfo.date.toAstropy()
282 band = exposure.filter.bandLabel
284 sourceSelection = self.sourceSelector.
run(sourceCat)
286 self.log.info(
"Purged %d sources, leaving %d good sources",
287 len(sourceCat) - len(sourceSelection.sourceCat),
288 len(sourceSelection.sourceCat))
289 if len(sourceSelection.sourceCat) == 0:
291 "No good sources selected for astrometry.",
292 lenSourceSelectionCat=len(sourceSelection.sourceCat)
296 bbox=exposure.getBBox(),
302 refSelection = self.referenceSelector.
run(loadResult.refCat, exposure=exposure)
303 refCat = refSelection.sourceCat
305 if self.config.doFiducialZeroPointCull:
308 loadResult.fluxField,
309 refCat, sourceSelection.sourceCat,
310 exposure.visitInfo.getExposureTime()
315 if not refCat.isContiguous():
316 refCat = refCat.copy(deep=
True)
319 frame = int(debug.frame)
322 sourceCat=sourceSelection.sourceCat,
324 bbox=exposure.getBBox(),
326 title=
"Reference catalog",
329 result = pipeBase.Struct(matchTolerance=
None)
330 maxMatchDistance = np.inf
332 while (maxMatchDistance > self.config.minMatchDistanceArcSec
and i < self.config.maxIter):
338 goodSourceCat=sourceSelection.sourceCat,
339 refFluxField=loadResult.fluxField,
340 bbox=exposure.getBBox(),
343 matchTolerance=result.matchTolerance,
345 exposure.setWcs(result.wcs)
347 e._metadata[
'iterations'] = i
348 sourceCat[
"coord_ra"] = np.nan
349 sourceCat[
"coord_dec"] = np.nan
350 exposure.setWcs(
None)
351 self.log.error(
"Failure fitting astrometry. %s: %s", type(e).__name__, e)
355 maxMatchDistance = result.stats.maxMatchDist.asArcseconds()
356 distMean = result.stats.distMean.asArcseconds()
357 distStdDev = result.stats.distStdDev.asArcseconds()
358 self.log.info(
"Astrometric fit iteration %d: found %d matches with mean separation "
359 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec.",
360 i, len(result.matches), distMean, distStdDev, maxMatchDistance)
365 md = exposure.getMetadata()
366 md[
'SFM_ASTROM_OFFSET_MEAN'] = distMean
367 md[
'SFM_ASTROM_OFFSET_STD'] = distStdDev
368 md[
'SFM_ASTROM_OFFSET_MEDIAN'] = result.scatterOnSky.asArcseconds()
371 for m
in result.matches:
372 m.second.set(self.
usedKey,
True)
375 bbox=exposure.getBBox(),
381 return pipeBase.Struct(
383 matches=result.matches,
384 scatterOnSky=result.scatterOnSky,
388 def check(self, exposure, sourceCat, nMatches):
389 """Validate the astrometric fit against the maxMeanDistance threshold.
391 If the distMean metric does not satisfy the requirement of being less
392 than the value set in config.maxMeanDistanceArcsec, the WCS on the
393 exposure will be set to None and the coordinate values in the
394 source catalog will be set to NaN to reflect a failed astrometric fit.
398 exposure : `lsst.afw.image.Exposure`
399 The exposure whose astrometric fit is being evaluated.
400 sourceCat : `lsst.afw.table.SourceCatalog`
401 The catalog of sources associated with the exposure.
403 The number of matches that were found and used during
404 the astrometric fit (for logging purposes only).
409 If the measured mean on-sky distance between the matched source and
410 reference objects is greater than
411 ``self.config.maxMeanDistanceArcsec``.
414 md = exposure.getMetadata()
415 distMean = md[
'SFM_ASTROM_OFFSET_MEAN']
416 distMedian = md[
'SFM_ASTROM_OFFSET_MEDIAN']
417 if distMean > self.config.maxMeanDistanceArcsec:
419 maxMeanDist=self.config.maxMeanDistanceArcsec,
420 distMedian=distMedian)
421 exposure.setWcs(
None)
422 sourceCat[
"coord_ra"] = np.nan
423 sourceCat[
"coord_dec"] = np.nan
424 self.log.error(exception)
429 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance,
431 """Match sources to reference objects and fit a WCS.
435 refCat : `lsst.afw.table.SimpleCatalog`
436 catalog of reference objects
437 sourceCat : `lsst.afw.table.SourceCatalog`
438 catalog of sources detected on the exposure
439 goodSourceCat : `lsst.afw.table.SourceCatalog`
440 catalog of down-selected good sources detected on the exposure
442 field of refCat to use for flux
443 bbox : `lsst.geom.Box2I`
444 bounding box of exposure
445 wcs : `lsst.afw.geom.SkyWcs`
446 initial guess for WCS of exposure
447 matchTolerance : `lsst.meas.astrom.MatchTolerance`
448 a MatchTolerance object (or None) specifying
449 internal tolerances to the matcher. See the MatchTolerance
450 definition in the respective matcher for the class definition.
451 exposure : `lsst.afw.image.Exposure`, optional
452 exposure whose WCS is to be fit, or None; used only for the debug
457 result : `lsst.pipe.base.Struct`
458 Result struct with components:
460 - ``matches``: astrometric matches
461 (`list` of `lsst.afw.table.ReferenceMatch`).
462 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
463 - ``scatterOnSky`` : median on-sky separation between reference
464 objects and sources in "matches" (`lsst.afw.geom.Angle`).
469 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
471 matchRes = self.matcher.matchObjectsToSources(
473 sourceCat=goodSourceCat,
475 sourceFluxField=sourceFluxField,
476 refFluxField=refFluxField,
477 matchTolerance=matchTolerance,
480 self.log.debug(
"Found %s matches", len(matchRes.matches))
482 frame = int(debug.frame)
485 sourceCat=matchRes.usableSourceCat,
486 matches=matchRes.matches,
493 if self.config.doMagnitudeOutlierRejection:
496 matches = matchRes.matches
498 self.log.debug(
"Fitting WCS")
499 fitRes = self.wcsFitter.fitWcs(
508 scatterOnSky = fitRes.scatterOnSky
510 frame = int(debug.frame)
513 sourceCat=matchRes.usableSourceCat,
518 title=f
"Fitter: {self.wcsFitter._DefaultName}",
521 return pipeBase.Struct(
524 scatterOnSky=scatterOnSky,
525 matchTolerance=matchRes.matchTolerance,
529 """Remove magnitude outliers, computing a simple zeropoint.
533 sourceFluxField : `str`
534 Field in source catalog for instrumental fluxes.
536 Field in reference catalog for fluxes (nJy).
537 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
538 List of source/reference matches input
542 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
543 List of source/reference matches with magnitude
546 nMatch = len(matchesIn)
547 sourceMag = np.zeros(nMatch)
548 refMag = np.zeros(nMatch)
549 for i, match
in enumerate(matchesIn):
550 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
551 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
553 deltaMag = refMag - sourceMag
555 goodDelta, = np.where(np.isfinite(deltaMag))
556 zp = np.median(deltaMag[goodDelta])
560 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
564 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
567 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
568 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
570 nOutlier = nMatch - goodStars.size
571 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
574 matchesOut = [matchesIn[idx]
for idx
in goodStars]
579 """Compute the median ref flux - source filter color difference.
581 The median difference in color between the flux field used for
582 selection and that of the observations being calibrated is computed
583 from the values for each in the reference catalog.
588 Bandpass of observed data.
590 Name of the flux field used in the reference catalog.
591 refCat : `lsst.afw.table.SimpleCatalog`
592 Catalog of reference objects from which to compute the color
597 refColorDelta : `float`
598 The median color difference.
600 if band
in self.config.filterMap:
601 refFilterNameForBand = self.config.filterMap.get(band) +
"_flux"
602 refCatTemp = refCat[(np.isfinite(refCat[refFluxField])
603 & np.isfinite(refCat[refFilterNameForBand]))].copy(deep=
True)
604 if len(refCatTemp) < 3:
605 refColorDeltaDefaults = self.config.refColorDeltaDefaults
606 if band
in refColorDeltaDefaults:
607 refColorDelta = refColorDeltaDefaults[band]
609 self.log.warning(
"Band %s was not found in refColorDeltaDefaults dict "
610 "(currently set as: %s. Will use a default of 0.0 (i.e. "
611 "no color shift).", band, self.config.refColorDeltaDefaults)
613 self.log.warning(
"Not enough reference sources with finite fluxes in %s and %s, "
614 "so can't compute color shift; a default vaulue of %.2f will "
615 "be applied.", refFluxField, refFilterNameForBand, refColorDelta)
617 refMag = (refCatTemp[refFluxField]*units.nJy).to_value(units.ABmag)
618 refMagSrcBand = (refCatTemp[refFilterNameForBand]*units.nJy).to_value(units.ABmag)
619 refColorDelta = np.nanmedian(refMag - refMagSrcBand)
620 self.log.info(
"Adjusting refCat cutoffs for color shift: %s - %s = %.2f.",
621 refFluxField, refFilterNameForBand, refColorDelta)
624 self.log.warning(
"Band %s not found in filterMap: %s. No adjustment for filter "
625 "differences between reference and source catalogs attempted.",
626 band, self.config.filterMap)
630 """Perform a culling of the catalogs to attempt to match their
631 effective magnitude ranges.
633 This uses a fiducial zeropoint along with the exposure time for
634 the observations to compute our best-guess magnitudes. Also,
635 accommodation is made for the median difference in color between
636 the flux field used for selection and that of the observations being
637 calibrated, which is computed from the values for each in the reference
643 Bandpass of observed data.
645 Name of the flux field used in the reference catalog.
646 refCat : `lsst.afw.table.SimpleCatalog`
647 Catalog of reference objects to be passed to the matcher. Modified
649 sourceCat : `lsst.afw.table.SourceCatalog`
650 Catalog of observed sources to be passed to the matcher. Modified
653 Exposure time of the observation being calibrated.
657 refColorDelta : `float`
658 The median color difference.
660 nRefCatPreCull = len(refCat)
661 nSelectedSourceCat = len(sourceCat)
663 psfFlux = sourceCat[
"base_PsfFlux_instFlux"]
664 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
665 psfMag = -2.5*np.log10(psfFlux) + fiducialZeroPoint + 2.5*np.log10(expTime)
666 sourceMagMin = min(psfMag) - self.config.cullMagBuffer
667 sourceMagMax = max(psfMag) + self.config.cullMagBuffer
672 if refColorDelta > self.config.cullMagBuffer:
674 sourceMagMin += 0.5*refColorDelta
675 sourceMagMax += 0.5*refColorDelta
676 if refColorDelta > 3.0*self.config.cullMagBuffer:
679 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
680 sourceMagMax += 0.5*refColorDelta
681 if refColorDelta < -1.0*self.config.cullMagBuffer:
685 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
686 sourceMagMax += refColorDelta
687 self.log.debug(
"Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
688 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
694 if nRefCatPreCull/nSelectedSourceCat > self.config.maxRefToSourceRatio:
695 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
697 self.log.info(
"Source selection: sourceMag (min, max) = (%.3f, %.3f)", sourceMagMin, sourceMagMax)
698 refCat = refCat[np.isfinite(refCat[refFluxField])]
699 refMag = (refCat[refFluxField]*units.nJy).to_value(units.ABmag)
700 refCat = refCat[(refMag < sourceMagMax) & (refMag > sourceMagMin)]
701 refMagMin = np.nanmin(refMag)
702 refMagMax = np.nanmax(refMag)
704 goodSources = ((psfMag < refMagMax - refColorDelta) & (psfMag > refMagMin - refColorDelta))
705 if len(goodSources) < np.sum(goodSources):
706 sourceCat = sourceCat[goodSources].copy(deep=
True)
707 nSelectedSourceCat = len(sourceCat)
708 self.log.debug(
"Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
709 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
711 self.log.info(
"Final: Selected %d/%d reference sources based on fiducial zeropoint culling. "
712 "Mag range in %s = (%.2f, %.2f)", len(refCat), nRefCatPreCull,
713 refFluxField, refMagMin, refMagMax)
714 return refCat, sourceCat