134 def run(self, sourceCat, exposure):
135 """Load reference objects, match sources and optionally fit a WCS.
137 This is a thin layer around solve or loadAndMatch, depending on
138 config.forceKnownWcs.
142 exposure : `lsst.afw.image.Exposure`
143 exposure whose WCS is to be fit
144 The following are read only:
147 - filter (may be unset)
148 - detector (if wcs is pure tangent; may be absent)
150 The following are updated:
152 - wcs (the initial value is used as an initial guess, and is
155 sourceCat : `lsst.afw.table.SourceCatalog`
156 catalog of sources detected on the exposure
160 result : `lsst.pipe.base.Struct`
163 - ``refCat`` : reference object catalog of objects that overlap the
164 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
165 - ``matches`` : astrometric matches
166 (`list` of `lsst.afw.table.ReferenceMatch`).
167 - ``scatterOnSky`` : median on-sky separation between reference
168 objects and sources in "matches"
169 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
170 - ``matchMeta`` : metadata needed to unpersist matches
171 (`lsst.daf.base.PropertyList`)
174 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
175 if self.config.forceKnownWcs:
176 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
177 res.scatterOnSky =
None
179 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
183 def solve(self, exposure, sourceCat):
184 """Load reference objects overlapping an exposure, match to sources and
189 result : `lsst.pipe.base.Struct`
190 Result struct with components:
192 - ``refCat`` : reference object catalog of objects that overlap the
193 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
194 - ``matches`` : astrometric matches
195 (`list` of `lsst.afw.table.ReferenceMatch`).
196 - ``scatterOnSky`` : median on-sky separation between reference
197 objects and sources in "matches" (`lsst.geom.Angle`)
198 - ``matchMeta`` : metadata needed to unpersist matches
199 (`lsst.daf.base.PropertyList`)
204 If the measured mean on-sky distance between the matched source and
205 reference objects is greater than
206 ``self.config.maxMeanDistanceArcsec``.
210 ignores config.forceKnownWcs
213 raise RuntimeError(
"Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
219 sourceSelection = self.sourceSelector.
run(sourceCat)
221 self.log.info(
"Purged %d sources, leaving %d good sources",
222 len(sourceCat) - len(sourceSelection.sourceCat),
223 len(sourceSelection.sourceCat))
228 filterName=expMd.filterName,
232 refSelection = self.referenceSelector.
run(loadRes.refCat)
237 filterName=expMd.filterName,
242 frame = int(debug.frame)
244 refCat=refSelection.sourceCat,
245 sourceCat=sourceSelection.sourceCat,
249 title=
"Reference catalog",
254 match_tolerance =
None
256 for i
in range(self.config.maxIter):
261 refCat=refSelection.sourceCat,
263 goodSourceCat=sourceSelection.sourceCat,
264 refFluxField=loadRes.fluxField,
268 match_tolerance=match_tolerance,
270 except Exception
as e:
274 self.log.info(
"Fit WCS iter %d failed; using previous iteration: %s", iterNum, e)
278 self.log.info(
"Fit WCS iter %d failed: %s" % (iterNum, e))
282 match_tolerance = tryRes.match_tolerance
285 "Match and fit WCS iteration %d: found %d matches with on-sky distance mean and "
286 "scatter = %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec",
287 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
288 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
290 maxMatchDist = tryMatchDist.maxMatchDist
293 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
295 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
296 "that's good enough",
297 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
299 match_tolerance.maxMatchDist = maxMatchDist
302 self.log.info(
"Matched and fit WCS in %d iterations; "
303 "found %d matches with mean and scatter = %0.3f +- %0.3f arcsec" %
304 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
305 tryMatchDist.distStdDev.asArcseconds()))
306 if tryMatchDist.distMean.asArcseconds() > self.config.maxMeanDistanceArcsec:
307 self.log.info(
"Assigning as a fit failure: mean on-sky distance = %0.3f arcsec > %0.3f "
308 "(maxMeanDistanceArcsec)" % (tryMatchDist.distMean.asArcseconds(),
309 self.config.maxMeanDistanceArcsec))
313 self.log.warning(
"WCS fit failed. Setting exposure's WCS to None and coord_ra & coord_dec "
314 "cols in sourceCat to nan.")
315 sourceCat[
"coord_ra"] = np.nan
316 sourceCat[
"coord_dec"] = np.nan
317 exposure.setWcs(
None)
321 for m
in res.matches:
323 m.second.set(self.
usedKey,
True)
324 exposure.setWcs(res.wcs)
325 matches = res.matches
326 scatterOnSky = res.scatterOnSky
332 md = exposure.getMetadata()
333 md[
'SFM_ASTROM_OFFSET_MEAN'] = tryMatchDist.distMean.asArcseconds()
334 md[
'SFM_ASTROM_OFFSET_STD'] = tryMatchDist.distStdDev.asArcseconds()
336 return pipeBase.Struct(
337 refCat=refSelection.sourceCat,
339 scatterOnSky=scatterOnSky,
344 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
346 """Match sources to reference objects and fit a WCS.
350 refCat : `lsst.afw.table.SimpleCatalog`
351 catalog of reference objects
352 sourceCat : `lsst.afw.table.SourceCatalog`
353 catalog of sources detected on the exposure
354 goodSourceCat : `lsst.afw.table.SourceCatalog`
355 catalog of down-selected good sources detected on the exposure
357 field of refCat to use for flux
358 bbox : `lsst.geom.Box2I`
359 bounding box of exposure
360 wcs : `lsst.afw.geom.SkyWcs`
361 initial guess for WCS of exposure
362 match_tolerance : `lsst.meas.astrom.MatchTolerance`
363 a MatchTolerance object (or None) specifying
364 internal tolerances to the matcher. See the MatchTolerance
365 definition in the respective matcher for the class definition.
366 exposure : `lsst.afw.image.Exposure`
367 exposure whose WCS is to be fit, or None; used only for the debug
372 result : `lsst.pipe.base.Struct`
373 Result struct with components:
375 - ``matches``: astrometric matches
376 (`list` of `lsst.afw.table.ReferenceMatch`).
377 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
378 - ``scatterOnSky`` : median on-sky separation between reference
379 objects and sources in "matches" (`lsst.afw.geom.Angle`).
384 sourceFluxField =
"slot_%sFlux_instFlux" % (self.config.sourceFluxType)
386 matchRes = self.matcher.matchObjectsToSources(
388 sourceCat=goodSourceCat,
390 sourceFluxField=sourceFluxField,
391 refFluxField=refFluxField,
392 match_tolerance=match_tolerance,
394 self.log.debug(
"Found %s matches", len(matchRes.matches))
396 frame = int(debug.frame)
399 sourceCat=matchRes.usableSourceCat,
400 matches=matchRes.matches,
407 if self.config.doMagnitudeOutlierRejection:
410 matches = matchRes.matches
412 self.log.debug(
"Fitting WCS")
413 fitRes = self.wcsFitter.fitWcs(
422 scatterOnSky = fitRes.scatterOnSky
424 frame = int(debug.frame)
427 sourceCat=matchRes.usableSourceCat,
432 title=
"Fit TAN-SIP WCS",
435 return pipeBase.Struct(
438 scatterOnSky=scatterOnSky,
439 match_tolerance=matchRes.match_tolerance,
443 """Remove magnitude outliers, computing a simple zeropoint.
447 sourceFluxField : `str`
448 Field in source catalog for instrumental fluxes.
450 Field in reference catalog for fluxes (nJy).
451 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
452 List of source/reference matches input
456 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
457 List of source/reference matches with magnitude
460 nMatch = len(matchesIn)
461 sourceMag = np.zeros(nMatch)
462 refMag = np.zeros(nMatch)
463 for i, match
in enumerate(matchesIn):
464 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
465 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
467 deltaMag = refMag - sourceMag
469 goodDelta, = np.where(np.isfinite(deltaMag))
470 zp = np.median(deltaMag[goodDelta])
474 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale=
'normal'),
478 self.log.info(
"Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
481 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
482 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
484 nOutlier = nMatch - goodStars.size
485 self.log.info(
"Removed %d magnitude outliers out of %d total astrometry matches.",
489 for matchInd
in goodStars:
490 matchesOut.append(matchesIn[matchInd])