208 def solve(self, exposure, sourceCat):
209 """Load reference objects overlapping an exposure, match to sources and
214 result : `lsst.pipe.base.Struct`
215 Result struct with components:
217 - ``refCat`` : reference object catalog of objects that overlap the
218 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
219 - ``matches`` : astrometric matches
220 (`list` of `lsst.afw.table.ReferenceMatch`).
221 - ``scatterOnSky`` : median on-sky separation between reference
222 objects and sources in "matches" (`lsst.geom.Angle`)
223 - ``matchMeta`` : metadata needed to unpersist matches
224 (`lsst.daf.base.PropertyList`)
229 If the measured mean on-sky distance between the matched source and
230 reference objects is greater than
231 ``self.config.maxMeanDistanceArcsec``.
235 ignores config.forceKnownWcs
238 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
242 epoch = exposure.visitInfo.date.toAstropy()
244 sourceSelection = self.sourceSelector.
run(sourceCat)
246 self.log.info(
"Purged %d sources, leaving %d good sources",
247 len(sourceCat) - len(sourceSelection.sourceCat),
248 len(sourceSelection.sourceCat))
249 if len(sourceSelection.sourceCat) == 0:
251 "No good sources selected for astrometry.",
252 lenSourceSelectionCat=len(sourceSelection.sourceCat)
256 bbox=exposure.getBBox(),
258 filterName=exposure.filter.bandLabel,
261 refSelection = self.referenceSelector.
run(loadResult.refCat, exposure=exposure)
262 refCat = refSelection.sourceCat
264 if self.config.doFiducialZeroPointCull:
265 nRefCatPreCull = len(refCat)
267 psfFlux = sourceSelection.sourceCat[
"base_PsfFlux_instFlux"]
268 expTime = exposure.visitInfo.getExposureTime()
269 fiducialZeroPoint = self.config.fiducialZeroPoint[exposure.filter.bandLabel]
270 psfMag = -2.5*np.log10(psfFlux) + fiducialZeroPoint + 2.5*np.log10(expTime)
271 sourceMagMin = min(psfMag) - self.config.cullMagBuffer
272 sourceMagMax = max(psfMag) + self.config.cullMagBuffer
273 self.log.info(
"sourceMagMin = %0.3f sourceMagMax = %0.3f", sourceMagMin, sourceMagMax)
275 refCat = refCat[np.isfinite(refCat[loadResult.fluxField])]
276 refMag = (refCat[loadResult.fluxField]*units.nJy).to_value(units.ABmag)
277 refCat = refCat[(refMag > sourceMagMin) & (refMag < sourceMagMax)]
278 self.log.info(
"Selected %d/%d reference sources based on fiducial zeropoint culling.",
279 len(refCat), nRefCatPreCull)
283 if not refCat.isContiguous():
284 refCat = refCat.copy(deep=
True)
287 frame = int(debug.frame)
290 sourceCat=sourceSelection.sourceCat,
292 bbox=exposure.getBBox(),
294 title=
"Reference catalog",
297 result = pipeBase.Struct(matchTolerance=
None)
298 maxMatchDistance = np.inf
300 while (maxMatchDistance > self.config.minMatchDistanceArcSec
and i < self.config.maxIter):
306 goodSourceCat=sourceSelection.sourceCat,
307 refFluxField=loadResult.fluxField,
308 bbox=exposure.getBBox(),
311 matchTolerance=result.matchTolerance,
313 exposure.setWcs(result.wcs)
315 e._metadata[
'iterations'] = i
316 sourceCat[
"coord_ra"] = np.nan
317 sourceCat[
"coord_dec"] = np.nan
318 exposure.setWcs(
None)
319 self.log.error(
"Failure fitting astrometry. %s: %s", type(e).__name__, e)
323 maxMatchDistance = result.stats.maxMatchDist.asArcseconds()
324 distMean = result.stats.distMean.asArcseconds()
325 distStdDev = result.stats.distStdDev.asArcseconds()
326 self.log.info(
"Astrometric fit iteration %d: found %d matches with mean separation "
327 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec.",
328 i, len(result.matches), distMean, distStdDev, maxMatchDistance)
333 md = exposure.getMetadata()
334 md[
'SFM_ASTROM_OFFSET_MEAN'] = distMean
335 md[
'SFM_ASTROM_OFFSET_STD'] = distStdDev
336 md[
'SFM_ASTROM_OFFSET_MEDIAN'] = result.scatterOnSky.asArcseconds()
339 for m
in result.matches:
340 m.second.set(self.
usedKey,
True)
343 bbox=exposure.getBBox(),
345 filterName=exposure.filter.bandLabel,
349 return pipeBase.Struct(
351 matches=result.matches,
352 scatterOnSky=result.scatterOnSky,
356 def check(self, exposure, sourceCat, nMatches):
357 """Validate the astrometric fit against the maxMeanDistance threshold.
359 If the distMean metric does not satisfy the requirement of being less
360 than the value set in config.maxMeanDistanceArcsec, the WCS on the
361 exposure will be set to None and the coordinate values in the
362 source catalog will be set to NaN to reflect a failed astrometric fit.
366 exposure : `lsst.afw.image.Exposure`
367 The exposure whose astrometric fit is being evaluated.
368 sourceCat : `lsst.afw.table.SourceCatalog`
369 The catalog of sources associated with the exposure.
371 The number of matches that were found and used during
372 the astrometric fit (for logging purposes only).
377 If the measured mean on-sky distance between the matched source and
378 reference objects is greater than
379 ``self.config.maxMeanDistanceArcsec``.
382 md = exposure.getMetadata()
383 distMean = md[
'SFM_ASTROM_OFFSET_MEAN']
384 distMedian = md[
'SFM_ASTROM_OFFSET_MEDIAN']
385 if distMean > self.config.maxMeanDistanceArcsec:
387 maxMeanDist=self.config.maxMeanDistanceArcsec,
388 distMedian=distMedian)
389 exposure.setWcs(
None)
390 sourceCat[
"coord_ra"] = np.nan
391 sourceCat[
"coord_dec"] = np.nan
392 self.log.error(exception)
397 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance,
399 """Match sources to reference objects and fit a WCS.
403 refCat : `lsst.afw.table.SimpleCatalog`
404 catalog of reference objects
405 sourceCat : `lsst.afw.table.SourceCatalog`
406 catalog of sources detected on the exposure
407 goodSourceCat : `lsst.afw.table.SourceCatalog`
408 catalog of down-selected good sources detected on the exposure
410 field of refCat to use for flux
411 bbox : `lsst.geom.Box2I`
412 bounding box of exposure
413 wcs : `lsst.afw.geom.SkyWcs`
414 initial guess for WCS of exposure
415 matchTolerance : `lsst.meas.astrom.MatchTolerance`
416 a MatchTolerance object (or None) specifying
417 internal tolerances to the matcher. See the MatchTolerance
418 definition in the respective matcher for the class definition.
419 exposure : `lsst.afw.image.Exposure`, optional
420 exposure whose WCS is to be fit, or None; used only for the debug
425 result : `lsst.pipe.base.Struct`
426 Result struct with components:
428 - ``matches``: astrometric matches
429 (`list` of `lsst.afw.table.ReferenceMatch`).
430 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
431 - ``scatterOnSky`` : median on-sky separation between reference
432 objects and sources in "matches" (`lsst.afw.geom.Angle`).
437 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
439 matchRes = self.matcher.matchObjectsToSources(
441 sourceCat=goodSourceCat,
443 sourceFluxField=sourceFluxField,
444 refFluxField=refFluxField,
445 matchTolerance=matchTolerance,
448 self.log.debug(
"Found %s matches", len(matchRes.matches))
450 frame = int(debug.frame)
453 sourceCat=matchRes.usableSourceCat,
454 matches=matchRes.matches,
461 if self.config.doMagnitudeOutlierRejection:
464 matches = matchRes.matches
466 self.log.debug(
"Fitting WCS")
467 fitRes = self.wcsFitter.fitWcs(
476 scatterOnSky = fitRes.scatterOnSky
478 frame = int(debug.frame)
481 sourceCat=matchRes.usableSourceCat,
486 title=f
"Fitter: {self.wcsFitter._DefaultName}",
489 return pipeBase.Struct(
492 scatterOnSky=scatterOnSky,
493 matchTolerance=matchRes.matchTolerance,
497 """Remove magnitude outliers, computing a simple zeropoint.
501 sourceFluxField : `str`
502 Field in source catalog for instrumental fluxes.
504 Field in reference catalog for fluxes (nJy).
505 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
506 List of source/reference matches input
510 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
511 List of source/reference matches with magnitude
514 nMatch = len(matchesIn)
515 sourceMag = np.zeros(nMatch)
516 refMag = np.zeros(nMatch)
517 for i, match
in enumerate(matchesIn):
518 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
519 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
521 deltaMag = refMag - sourceMag
523 goodDelta, = np.where(np.isfinite(deltaMag))
524 zp = np.median(deltaMag[goodDelta])
528 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
532 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
535 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
536 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
538 nOutlier = nMatch - goodStars.size
539 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
542 matchesOut = [matchesIn[idx]
for idx
in goodStars]