141 def run(self, sourceCat, exposure):
142 """Load reference objects, match sources and optionally fit a WCS.
144 This is a thin layer around solve or loadAndMatch, depending on
145 config.forceKnownWcs.
149 exposure : `lsst.afw.image.Exposure`
150 exposure whose WCS is to be fit
151 The following are read only:
154 - filter (may be unset)
155 - detector (if wcs is pure tangent; may be absent)
157 The following are updated:
159 - wcs (the initial value is used as an initial guess, and is
162 sourceCat : `lsst.afw.table.SourceCatalog`
163 catalog of sources detected on the exposure
167 result : `lsst.pipe.base.Struct`
170 - ``refCat`` : reference object catalog of objects that overlap the
171 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
172 - ``matches`` : astrometric matches
173 (`list` of `lsst.afw.table.ReferenceMatch`).
174 - ``scatterOnSky`` : median on-sky separation between reference
175 objects and sources in "matches"
176 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
177 - ``matchMeta`` : metadata needed to unpersist matches
178 (`lsst.daf.base.PropertyList`)
181 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
182 if self.config.forceKnownWcs:
183 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
184 res.scatterOnSky =
None
186 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
190 def solve(self, exposure, sourceCat):
191 """Load reference objects overlapping an exposure, match to sources and
196 result : `lsst.pipe.base.Struct`
197 Result struct with components:
199 - ``refCat`` : reference object catalog of objects that overlap the
200 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
201 - ``matches`` : astrometric matches
202 (`list` of `lsst.afw.table.ReferenceMatch`).
203 - ``scatterOnSky`` : median on-sky separation between reference
204 objects and sources in "matches" (`lsst.geom.Angle`)
205 - ``matchMeta`` : metadata needed to unpersist matches
206 (`lsst.daf.base.PropertyList`)
211 If the measured mean on-sky distance between the matched source and
212 reference objects is greater than
213 ``self.config.maxMeanDistanceArcsec``.
217 ignores config.forceKnownWcs
220 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
224 epoch = exposure.visitInfo.date.toAstropy()
226 sourceSelection = self.sourceSelector.
run(sourceCat)
228 self.log.info(
"Purged %d sources, leaving %d good sources",
229 len(sourceCat) - len(sourceSelection.sourceCat),
230 len(sourceSelection.sourceCat))
233 bbox=exposure.getBBox(),
235 filterName=exposure.filter.bandLabel,
239 refSelection = self.referenceSelector.
run(loadResult.refCat)
242 frame = int(debug.frame)
244 refCat=refSelection.sourceCat,
245 sourceCat=sourceSelection.sourceCat,
247 bbox=exposure.getBBox(),
249 title=
"Reference catalog",
252 result = pipeBase.Struct(matchTolerance=
None)
253 maxMatchDistance = np.inf
255 while (maxMatchDistance > self.config.minMatchDistanceArcSec
and i < self.config.maxIter):
259 refCat=refSelection.sourceCat,
261 goodSourceCat=sourceSelection.sourceCat,
262 refFluxField=loadResult.fluxField,
263 bbox=exposure.getBBox(),
266 matchTolerance=result.matchTolerance,
268 exposure.setWcs(result.wcs)
270 e._metadata[
'iterations'] = i
271 sourceCat[
"coord_ra"] = np.nan
272 sourceCat[
"coord_dec"] = np.nan
273 exposure.setWcs(
None)
274 self.log.error(
"Failure fitting astrometry. %s: %s", type(e).__name__, e)
278 maxMatchDistance = result.stats.maxMatchDist.asArcseconds()
279 distMean = result.stats.distMean.asArcseconds()
280 distStdDev = result.stats.distMean.asArcseconds()
281 self.log.info(
"Astrometric fit iteration %d: found %d matches with mean separation "
282 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec.",
283 i, len(result.matches), distMean, distStdDev, maxMatchDistance)
288 md = exposure.getMetadata()
289 md[
'SFM_ASTROM_OFFSET_MEAN'] = distMean
290 md[
'SFM_ASTROM_OFFSET_STD'] = distStdDev
293 if distMean > self.config.maxMeanDistanceArcsec:
296 maxMeanDist=self.config.maxMeanDistanceArcsec,
297 distMedian=result.scatterOnSky.asArcseconds())
298 exposure.setWcs(
None)
299 sourceCat[
"coord_ra"] = np.nan
300 sourceCat[
"coord_dec"] = np.nan
301 self.log.error(exception)
305 for m
in result.matches:
306 m.second.set(self.
usedKey,
True)
309 bbox=exposure.getBBox(),
311 filterName=exposure.filter.bandLabel,
315 return pipeBase.Struct(
316 refCat=refSelection.sourceCat,
317 matches=result.matches,
318 scatterOnSky=result.scatterOnSky,
323 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance,
325 """Match sources to reference objects and fit a WCS.
329 refCat : `lsst.afw.table.SimpleCatalog`
330 catalog of reference objects
331 sourceCat : `lsst.afw.table.SourceCatalog`
332 catalog of sources detected on the exposure
333 goodSourceCat : `lsst.afw.table.SourceCatalog`
334 catalog of down-selected good sources detected on the exposure
336 field of refCat to use for flux
337 bbox : `lsst.geom.Box2I`
338 bounding box of exposure
339 wcs : `lsst.afw.geom.SkyWcs`
340 initial guess for WCS of exposure
341 matchTolerance : `lsst.meas.astrom.MatchTolerance`
342 a MatchTolerance object (or None) specifying
343 internal tolerances to the matcher. See the MatchTolerance
344 definition in the respective matcher for the class definition.
345 exposure : `lsst.afw.image.Exposure`, optional
346 exposure whose WCS is to be fit, or None; used only for the debug
351 result : `lsst.pipe.base.Struct`
352 Result struct with components:
354 - ``matches``: astrometric matches
355 (`list` of `lsst.afw.table.ReferenceMatch`).
356 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
357 - ``scatterOnSky`` : median on-sky separation between reference
358 objects and sources in "matches" (`lsst.afw.geom.Angle`).
363 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
365 matchRes = self.matcher.matchObjectsToSources(
367 sourceCat=goodSourceCat,
369 sourceFluxField=sourceFluxField,
370 refFluxField=refFluxField,
371 matchTolerance=matchTolerance,
373 self.log.debug(
"Found %s matches", len(matchRes.matches))
375 frame = int(debug.frame)
378 sourceCat=matchRes.usableSourceCat,
379 matches=matchRes.matches,
386 if self.config.doMagnitudeOutlierRejection:
389 matches = matchRes.matches
391 self.log.debug(
"Fitting WCS")
392 fitRes = self.wcsFitter.fitWcs(
401 scatterOnSky = fitRes.scatterOnSky
403 frame = int(debug.frame)
406 sourceCat=matchRes.usableSourceCat,
411 title=f
"Fitter: {self.wcsFitter._DefaultName}",
414 return pipeBase.Struct(
417 scatterOnSky=scatterOnSky,
418 matchTolerance=matchRes.matchTolerance,
422 """Remove magnitude outliers, computing a simple zeropoint.
426 sourceFluxField : `str`
427 Field in source catalog for instrumental fluxes.
429 Field in reference catalog for fluxes (nJy).
430 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
431 List of source/reference matches input
435 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
436 List of source/reference matches with magnitude
439 nMatch = len(matchesIn)
440 sourceMag = np.zeros(nMatch)
441 refMag = np.zeros(nMatch)
442 for i, match
in enumerate(matchesIn):
443 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
444 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
446 deltaMag = refMag - sourceMag
448 goodDelta, = np.where(np.isfinite(deltaMag))
449 zp = np.median(deltaMag[goodDelta])
453 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
457 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
460 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
461 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
463 nOutlier = nMatch - goodStars.size
464 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
467 matchesOut = [matchesIn[idx]
for idx
in goodStars]