210 matchTolerance=None, bbox=None):
211 """Match sources to position reference stars
213 refCat : `lsst.afw.table.SimpleCatalog`
214 catalog of reference objects that overlap the exposure; reads
218 - the specified flux field
220 sourceCat : `lsst.afw.table.SourceCatalog`
221 Catalog of sources found on an exposure. This should already be
222 down-selected to "good"/"usable" sources in the calling Task.
223 wcs : `lsst.afw.geom.SkyWcs`
225 sourceFluxField: `str`
226 field of sourceCat to use for flux
228 field of refCat to use for flux
229 matchTolerance : `lsst.meas.astrom.MatchTolerancePessimistic`
230 is a MatchTolerance class object or `None`. This this class is used
231 to communicate state between AstrometryTask and MatcherTask.
232 AstrometryTask will also set the MatchTolerance class variable
233 maxMatchDist based on the scatter AstrometryTask has found after
235 bbox : `lsst.geom.Box2I`, optional
236 Bounding box of the exposure for evaluating the local pixelScale
237 (defaults to the Sky Origin of the ``wcs`` provided if ``bbox``
242 result : `lsst.pipe.base.Struct`
243 Result struct with components:
245 - ``matches`` : source to reference matches found (`list` of
246 `lsst.afw.table.ReferenceMatch`)
247 - ``usableSourceCat`` : a catalog of sources potentially usable for
248 matching and WCS fitting (`lsst.afw.table.SourceCatalog`).
249 - ``matchTolerance`` : a MatchTolerance object containing the
250 resulting state variables from the match
251 (`lsst.meas.astrom.MatchTolerancePessimistic`).
258 if matchTolerance
is None:
263 goodSourceCat = sourceCat
265 if (numUsableSources := len(goodSourceCat)) == 0:
268 minMatchedPairs =
min(self.config.minMatchedPairs,
269 int(self.config.minFracMatchedPairs
270 *
min([len(refCat), len(goodSourceCat)])))
272 if len(goodSourceCat) <= self.config.numPointsForShape:
273 msg = (f
"Not enough catalog objects ({len(goodSourceCat)}) to make a "
274 f
"shape for the matcher (need {self.config.numPointsForShape}).")
276 if len(refCat) <= self.config.numPointsForShape:
277 msg = (f
"Not enough refcat objects ({len(refCat)}) to make a "
278 f
"shape for the matcher (need {self.config.numPointsForShape}).")
281 if len(refCat) > self.config.maxRefObjects:
283 "WARNING: Reference catalog larger than maximum allowed. "
284 "Trimming to %i", self.config.maxRefObjects)
287 trimmedRefCat = refCat
290 refCat=trimmedRefCat,
291 sourceCat=goodSourceCat,
293 refFluxField=refFluxField,
294 numUsableSources=numUsableSources,
295 minMatchedPairs=minMatchedPairs,
296 matchTolerance=matchTolerance,
297 sourceFluxField=sourceFluxField,
298 verbose=debug.verbose,
301 matches = doMatchReturn.matches
302 matchTolerance = doMatchReturn.matchTolerance
304 if (nMatches := len(matches)) == 0:
307 self.
log.info(
"Matched %d sources", nMatches)
308 if nMatches < minMatchedPairs:
309 self.
log.warning(
"Number of matches (%s) is smaller than minimum requested (%s)",
310 nMatches, minMatchedPairs)
312 return pipeBase.Struct(
314 usableSourceCat=goodSourceCat,
315 matchTolerance=matchTolerance,
352 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
353 minMatchedPairs, matchTolerance, sourceFluxField, verbose, bbox=None):
354 """Implementation of matching sources to position reference objects
356 Unlike matchObjectsToSources, this method does not check if the sources
361 refCat : `lsst.afw.table.SimpleCatalog`
362 catalog of position reference objects that overlap an exposure
363 sourceCat : `lsst.afw.table.SourceCatalog`
364 catalog of sources found on the exposure
365 wcs : `lsst.afw.geom.SkyWcs`
366 estimated WCS of exposure
368 field of refCat to use for flux
369 numUsableSources : `int`
370 number of usable sources (sources with known centroid that are not
371 near the edge, but may be saturated)
372 minMatchedPairs : `int`
373 minimum number of matches
374 matchTolerance : `lsst.meas.astrom.MatchTolerancePessimistic`
375 a MatchTolerance object containing variables specifying matcher
376 tolerances and state from possible previous runs.
377 sourceFluxField : `str`
378 Name of the flux field in the source catalog.
380 Set true to print diagnostic information to std::cout
381 bbox : `lsst.geom.Box2I`, optional
382 Bounding box of the exposure for evaluating the local pixelScale
383 (defaults to the Sky Origin of the ``wcs`` provided if ``bbox``
388 result : `lsst.pipe.base.Struct`
389 Results struct with components:
391 - ``matches`` : a list the matches found
392 (`list` of `lsst.afw.table.ReferenceMatch`).
393 - ``matchTolerance`` : MatchTolerance containing updated values from
394 this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`)
397 pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
399 pixelScale = wcs.getPixelScale().asArcseconds()
407 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
408 for src_idx, srcObj
in enumerate(sourceCat):
409 coord = wcs.pixelToSky(srcObj.getCentroid())
410 theta = np.pi / 2 - coord.getLatitude().asRadians()
411 phi = coord.getLongitude().asRadians()
412 flux = srcObj[sourceFluxField]
413 src_array[src_idx, :] = \
416 if matchTolerance.PPMbObj
is None or \
417 matchTolerance.autoMaxMatchDist
is None:
421 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
422 for ref_idx, refObj
in enumerate(refCat):
423 theta = np.pi / 2 - refObj.getDec().asRadians()
424 phi = refObj.getRa().asRadians()
425 flux = refObj[refFluxField]
426 ref_array[ref_idx, :] = \
430 ref_array[:, :3], self.
log)
431 self.
log.debug(
"Computing source statistics...")
434 self.
log.debug(
"Computing reference statistics...")
437 maxMatchDistArcSec = np.max((
438 self.config.minMatchDistPixels * pixelScale,
439 np.min((maxMatchDistArcSecSrc,
440 maxMatchDistArcSecRef))))
442 maxMatchDistArcSec, geom.arcseconds)
446 if matchTolerance.maxShift
is None:
447 maxShiftArcseconds = self.config.maxOffsetPix * pixelScale
451 maxShiftArcseconds = np.max(
452 (matchTolerance.maxShift.asArcseconds(),
453 self.config.minMatchDistPixels * pixelScale))
459 if matchTolerance.maxMatchDist
is None:
460 matchTolerance.maxMatchDist = matchTolerance.autoMaxMatchDist
462 maxMatchDistArcSec = np.max(
463 (self.config.minMatchDistPixels * pixelScale,
464 np.min((matchTolerance.maxMatchDist.asArcseconds(),
465 matchTolerance.autoMaxMatchDist.asArcseconds()))))
471 numConsensus = self.config.numPatternConsensus
472 if len(refCat) < self.config.numRefRequireConsensus:
473 minObjectsForConsensus = \
474 self.config.numBrightStars + \
475 self.config.numPointsForShapeAttempt
476 if len(refCat) < minObjectsForConsensus
or \
477 len(sourceCat) < minObjectsForConsensus:
480 self.
log.debug(
"Current tol maxDist: %.4f arcsec",
482 self.
log.debug(
"Current shift: %.4f arcsec",
487 for soften_dist
in range(self.config.matcherIterations):
488 if soften_dist == 0
and \
489 matchTolerance.lastMatchedPattern
is not None:
498 run_n_consent = numConsensus
501 matcher_struct = matchTolerance.PPMbObj.match(
502 source_array=src_array,
503 n_check=self.config.numPointsForShapeAttempt,
504 n_match=self.config.numPointsForShape,
505 n_agree=run_n_consent,
506 max_n_patterns=self.config.numBrightStars,
507 max_shift=maxShiftArcseconds,
508 max_rotation=self.config.maxRotationDeg,
509 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
510 min_matches=minMatchedPairs,
511 pattern_skip_array=np.array(
512 matchTolerance.failedPatternList)
515 if soften_dist == 0
and \
516 len(matcher_struct.match_ids) == 0
and \
517 matchTolerance.lastMatchedPattern
is not None:
524 matchTolerance.failedPatternList.append(
525 matchTolerance.lastMatchedPattern)
526 matchTolerance.lastMatchedPattern =
None
527 maxShiftArcseconds = self.config.maxOffsetPix * pixelScale
528 elif len(matcher_struct.match_ids) > 0:
531 matchTolerance.maxShift = \
532 matcher_struct.shift * geom.arcseconds
533 matchTolerance.lastMatchedPattern = \
534 matcher_struct.pattern_idx
540 return pipeBase.Struct(
542 matchTolerance=matchTolerance,
554 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
555 dist_cut_arcsec = np.max(
556 (np.degrees(matcher_struct.max_dist_rad) * 3600,
557 self.config.minMatchDistPixels * pixelScale))
562 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
564 if dist_arcsec < dist_cut_arcsec:
566 match.first = refCat[int(match_id_pair[1])]
567 match.second = sourceCat[int(match_id_pair[0])]
571 match.distance = match.first.getCoord().separation(
572 match.second.getCoord()).asArcseconds()
573 matches.append(match)
575 return pipeBase.Struct(
577 matchTolerance=matchTolerance,
614 """ Compute the tolerances for the matcher automatically by comparing
615 pinwheel patterns as we would in the matcher.
617 We test how similar the patterns we can create from a given set of
618 objects by computing the spoke lengths for each pattern and sorting
619 them from smallest to largest. The match tolerance is the average
620 distance per spoke between the closest two patterns in the sorted
625 cat_array : `numpy.ndarray`, (N, 3)
626 array of 3 vectors representing the x, y, z position of catalog
627 objects on the unit sphere.
632 Suggested max match tolerance distance calculated from comparisons
633 between pinwheel patterns used in optimistic/pessimistic pattern
637 self.
log.debug(
"Starting automated tolerance calculation...")
641 pattern_array = np.empty(
642 (cat_array.shape[0] - self.config.numPointsForShape,
643 self.config.numPointsForShape - 1))
644 flux_args_array = np.argsort(cat_array[:, -1])
647 tmp_sort_array = cat_array[flux_args_array]
650 for start_idx
in range(cat_array.shape[0]
651 - self.config.numPointsForShape):
652 pattern_points = tmp_sort_array[start_idx:start_idx
653 + self.config.numPointsForShape, :-1]
654 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
655 pattern_array[start_idx, :] = np.sqrt(
656 pattern_delta[:, 0] ** 2
657 + pattern_delta[:, 1] ** 2
658 + pattern_delta[:, 2] ** 2)
663 pattern_array[start_idx, :] = pattern_array[
664 start_idx, np.argsort(pattern_array[start_idx, :])]
670 pattern_array[:, :(self.config.numPointsForShape - 1)])
671 dist_nearest_array, ids = dist_tree.query(
672 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
673 dist_nearest_array = dist_nearest_array[:, 1]
674 dist_nearest_array.sort()
678 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600.
679 / (self.config.numPointsForShape - 1.))
681 self.
log.debug(
"Automated tolerance")
682 self.
log.debug(
"\tdistance/match tol: %.4f [arcsec]", dist_tol)