189 def solve(self, exposure, sourceCat):
190 """Load reference objects overlapping an exposure, match to sources and
191 fit a WCS
192
193 Returns
194 -------
195 result : `lsst.pipe.base.Struct`
196 Result struct with components:
197
198 - ``refCat`` : reference object catalog of objects that overlap the
199 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
200 - ``matches`` : astrometric matches
201 (`list` of `lsst.afw.table.ReferenceMatch`).
202 - ``scatterOnSky`` : median on-sky separation between reference
203 objects and sources in "matches" (`lsst.geom.Angle`)
204 - ``matchMeta`` : metadata needed to unpersist matches
205 (`lsst.daf.base.PropertyList`)
206
207 Raises
208 ------
209 TaskError
210 If the measured mean on-sky distance between the matched source and
211 reference objects is greater than
212 ``self.config.maxMeanDistanceArcsec``.
213
214 Notes
215 -----
216 ignores config.forceKnownWcs
217 """
218 if self.refObjLoader is None:
219 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
220 import lsstDebug
222
223 expMd = self._getExposureMetadata(exposure)
224
225 sourceSelection = self.sourceSelector.run(sourceCat)
226
227 self.log.info("Purged %d sources, leaving %d good sources",
228 len(sourceCat) - len(sourceSelection.sourceCat),
229 len(sourceSelection.sourceCat))
230
231 loadRes = self.refObjLoader.loadPixelBox(
232 bbox=expMd.bbox,
233 wcs=expMd.wcs,
234 filterName=expMd.filterName,
235 epoch=expMd.epoch,
236 )
237
238 refSelection = self.referenceSelector.run(loadRes.refCat)
239
240 matchMeta = self.refObjLoader.getMetadataBox(
241 bbox=expMd.bbox,
242 wcs=expMd.wcs,
243 filterName=expMd.filterName,
244 epoch=expMd.epoch,
245 )
246
247 if debug.display:
248 frame = int(debug.frame)
249 displayAstrometry(
250 refCat=refSelection.sourceCat,
251 sourceCat=sourceSelection.sourceCat,
252 exposure=exposure,
253 bbox=expMd.bbox,
254 frame=frame,
255 title="Reference catalog",
256 )
257
258 res = None
259 wcs = expMd.wcs
260 match_tolerance = None
261 fitFailed = False
262 for i in range(self.config.maxIter):
263 if not fitFailed:
264 iterNum = i + 1
265 try:
266 tryRes = self._matchAndFitWcs(
267 refCat=refSelection.sourceCat,
268 sourceCat=sourceCat,
269 goodSourceCat=sourceSelection.sourceCat,
270 refFluxField=loadRes.fluxField,
271 bbox=expMd.bbox,
272 wcs=wcs,
273 exposure=exposure,
274 match_tolerance=match_tolerance,
275 )
276 except Exception as e:
277
278
279 if i > 0:
280 self.log.info("Fit WCS iter %d failed; using previous iteration: %s", iterNum, e)
281 iterNum -= 1
282 break
283 else:
284 self.log.info("Fit WCS iter %d failed: %s" % (iterNum, e))
285 fitFailed = True
286
287 if not fitFailed:
288 match_tolerance = tryRes.match_tolerance
289 tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
290 self.log.debug(
291 "Match and fit WCS iteration %d: found %d matches with on-sky distance mean and "
292 "scatter = %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec",
293 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
294 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
295
296 maxMatchDist = tryMatchDist.maxMatchDist
297 res = tryRes
298 wcs = res.wcs
299 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
300 self.log.debug(
301 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
302 "that's good enough",
303 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
304 break
305 match_tolerance.maxMatchDist = maxMatchDist
306
307 if not fitFailed:
308 self.log.info("Matched and fit WCS in %d iterations; "
309 "found %d matches with mean and scatter = %0.3f +- %0.3f arcsec" %
310 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
311 tryMatchDist.distStdDev.asArcseconds()))
312 if tryMatchDist.distMean.asArcseconds() > self.config.maxMeanDistanceArcsec:
313 self.log.info("Assigning as a fit failure: mean on-sky distance = %0.3f arcsec > %0.3f "
314 "(maxMeanDistanceArcsec)" % (tryMatchDist.distMean.asArcseconds(),
315 self.config.maxMeanDistanceArcsec))
316 fitFailed = True
317
318 if fitFailed:
319 self.log.warning("WCS fit failed. Setting exposure's WCS to None and coord_ra & coord_dec "
320 "cols in sourceCat to nan.")
321 sourceCat["coord_ra"] = np.nan
322 sourceCat["coord_dec"] = np.nan
323 exposure.setWcs(None)
324 matches = None
325 scatterOnSky = None
326 else:
327 for m in res.matches:
328 if self.usedKey:
329 m.second.set(self.usedKey, True)
330 exposure.setWcs(res.wcs)
331 matches = res.matches
332 scatterOnSky = res.scatterOnSky
333
334
335
336
337 if res is not None:
338 md = exposure.getMetadata()
339 md['SFM_ASTROM_OFFSET_MEAN'] = tryMatchDist.distMean.asArcseconds()
340 md['SFM_ASTROM_OFFSET_STD'] = tryMatchDist.distStdDev.asArcseconds()
341
342 return pipeBase.Struct(
343 refCat=refSelection.sourceCat,
344 matches=matches,
345 scatterOnSky=scatterOnSky,
346 matchMeta=matchMeta,
347 )
348