210 match_tolerance=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 match_tolerance : `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
238 result : `lsst.pipe.base.Struct`
239 Result struct with components:
241 - ``matches`` : source to reference matches found (`list` of
242 `lsst.afw.table.ReferenceMatch`)
243 - ``usableSourceCat`` : a catalog of sources potentially usable for
244 matching and WCS fitting (`lsst.afw.table.SourceCatalog`).
245 - ``match_tolerance`` : a MatchTolerance object containing the
246 resulting state variables from the match
247 (`lsst.meas.astrom.MatchTolerancePessimistic`).
254 if match_tolerance
is None:
259 goodSourceCat = sourceCat
261 numUsableSources = len(goodSourceCat)
263 if len(goodSourceCat) == 0:
264 raise pipeBase.TaskError(
"No sources are good")
266 minMatchedPairs =
min(self.config.minMatchedPairs,
267 int(self.config.minFracMatchedPairs
268 *
min([len(refCat), len(goodSourceCat)])))
270 if len(goodSourceCat) <= self.config.numPointsForShape:
271 msg = (f
"Not enough catalog objects ({len(goodSourceCat)}) to make a "
272 f
"shape for the matcher (need {self.config.numPointsForShape}).")
273 raise RuntimeError(msg)
274 if len(refCat) <= self.config.numPointsForShape:
275 msg = (f
"Not enough refcat objects ({len(refCat)}) to make a "
276 f
"shape for the matcher (need {self.config.numPointsForShape}).")
277 raise RuntimeError(msg)
279 if len(refCat) > self.config.maxRefObjects:
281 "WARNING: Reference catalog larger that maximum allowed. "
282 "Trimming to %i", self.config.maxRefObjects)
285 trimmedRefCat = refCat
288 refCat=trimmedRefCat,
289 sourceCat=goodSourceCat,
291 refFluxField=refFluxField,
292 numUsableSources=numUsableSources,
293 minMatchedPairs=minMatchedPairs,
294 match_tolerance=match_tolerance,
295 sourceFluxField=sourceFluxField,
296 verbose=debug.verbose,
298 matches = doMatchReturn.matches
299 match_tolerance = doMatchReturn.match_tolerance
301 if len(matches) == 0:
302 raise RuntimeError(
"Unable to match sources")
304 self.
log.info(
"Matched %d sources", len(matches))
305 if len(matches) < minMatchedPairs:
306 self.
log.warning(
"Number of matches is smaller than request")
308 return pipeBase.Struct(
310 usableSourceCat=goodSourceCat,
311 match_tolerance=match_tolerance,
348 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
349 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
350 """Implementation of matching sources to position reference objects
352 Unlike matchObjectsToSources, this method does not check if the sources
357 refCat : `lsst.afw.table.SimpleCatalog`
358 catalog of position reference objects that overlap an exposure
359 sourceCat : `lsst.afw.table.SourceCatalog`
360 catalog of sources found on the exposure
361 wcs : `lsst.afw.geom.SkyWcs`
362 estimated WCS of exposure
364 field of refCat to use for flux
365 numUsableSources : `int`
366 number of usable sources (sources with known centroid that are not
367 near the edge, but may be saturated)
368 minMatchedPairs : `int`
369 minimum number of matches
370 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic`
371 a MatchTolerance object containing variables specifying matcher
372 tolerances and state from possible previous runs.
373 sourceFluxField : `str`
374 Name of the flux field in the source catalog.
376 Set true to print diagnostic information to std::cout
381 Results struct with components:
383 - ``matches`` : a list the matches found
384 (`list` of `lsst.afw.table.ReferenceMatch`).
385 - ``match_tolerance`` : MatchTolerance containing updated values from
386 this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`)
395 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
396 for src_idx, srcObj
in enumerate(sourceCat):
397 coord = wcs.pixelToSky(srcObj.getCentroid())
398 theta = np.pi / 2 - coord.getLatitude().asRadians()
399 phi = coord.getLongitude().asRadians()
400 flux = srcObj[sourceFluxField]
401 src_array[src_idx, :] = \
404 if match_tolerance.PPMbObj
is None or \
405 match_tolerance.autoMaxMatchDist
is None:
409 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
410 for ref_idx, refObj
in enumerate(refCat):
411 theta = np.pi / 2 - refObj.getDec().asRadians()
412 phi = refObj.getRa().asRadians()
413 flux = refObj[refFluxField]
414 ref_array[ref_idx, :] = \
418 ref_array[:, :3], self.
log)
419 self.
log.debug(
"Computing source statistics...")
422 self.
log.debug(
"Computing reference statistics...")
425 maxMatchDistArcSec = np.max((
426 self.config.minMatchDistPixels
427 * wcs.getPixelScale().asArcseconds(),
428 np.min((maxMatchDistArcSecSrc,
429 maxMatchDistArcSecRef))))
430 match_tolerance.autoMaxMatchDist =
geom.Angle(
431 maxMatchDistArcSec, geom.arcseconds)
435 if match_tolerance.maxShift
is None:
436 maxShiftArcseconds = (self.config.maxOffsetPix
437 * wcs.getPixelScale().asArcseconds())
441 maxShiftArcseconds = np.max(
442 (match_tolerance.maxShift.asArcseconds(),
443 self.config.minMatchDistPixels
444 * wcs.getPixelScale().asArcseconds()))
450 if match_tolerance.maxMatchDist
is None:
451 match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
453 maxMatchDistArcSec = np.max(
454 (self.config.minMatchDistPixels
455 * wcs.getPixelScale().asArcseconds(),
456 np.min((match_tolerance.maxMatchDist.asArcseconds(),
457 match_tolerance.autoMaxMatchDist.asArcseconds()))))
463 numConsensus = self.config.numPatternConsensus
464 if len(refCat) < self.config.numRefRequireConsensus:
465 minObjectsForConsensus = \
466 self.config.numBrightStars + \
467 self.config.numPointsForShapeAttempt
468 if len(refCat) < minObjectsForConsensus
or \
469 len(sourceCat) < minObjectsForConsensus:
472 self.
log.debug(
"Current tol maxDist: %.4f arcsec",
474 self.
log.debug(
"Current shift: %.4f arcsec",
479 for soften_dist
in range(self.config.matcherIterations):
480 if soften_dist == 0
and \
481 match_tolerance.lastMatchedPattern
is not None:
490 run_n_consent = numConsensus
493 matcher_struct = match_tolerance.PPMbObj.match(
494 source_array=src_array,
495 n_check=self.config.numPointsForShapeAttempt,
496 n_match=self.config.numPointsForShape,
497 n_agree=run_n_consent,
498 max_n_patterns=self.config.numBrightStars,
499 max_shift=maxShiftArcseconds,
500 max_rotation=self.config.maxRotationDeg,
501 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
502 min_matches=minMatchedPairs,
503 pattern_skip_array=np.array(
504 match_tolerance.failedPatternList)
507 if soften_dist == 0
and \
508 len(matcher_struct.match_ids) == 0
and \
509 match_tolerance.lastMatchedPattern
is not None:
516 match_tolerance.failedPatternList.append(
517 match_tolerance.lastMatchedPattern)
518 match_tolerance.lastMatchedPattern =
None
519 maxShiftArcseconds = \
520 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
521 elif len(matcher_struct.match_ids) > 0:
524 match_tolerance.maxShift = \
525 matcher_struct.shift * geom.arcseconds
526 match_tolerance.lastMatchedPattern = \
527 matcher_struct.pattern_idx
533 return pipeBase.Struct(
535 match_tolerance=match_tolerance,
547 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
548 dist_cut_arcsec = np.max(
549 (np.degrees(matcher_struct.max_dist_rad) * 3600,
550 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds()))
555 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
557 if dist_arcsec < dist_cut_arcsec:
559 match.first = refCat[int(match_id_pair[1])]
560 match.second = sourceCat[int(match_id_pair[0])]
564 match.distance = match.first.getCoord().separation(
565 match.second.getCoord()).asArcseconds()
566 matches.append(match)
568 return pipeBase.Struct(
570 match_tolerance=match_tolerance,
607 """ Compute the tolerances for the matcher automatically by comparing
608 pinwheel patterns as we would in the matcher.
610 We test how similar the patterns we can create from a given set of
611 objects by computing the spoke lengths for each pattern and sorting
612 them from smallest to largest. The match tolerance is the average
613 distance per spoke between the closest two patterns in the sorted
618 cat_array : `numpy.ndarray`, (N, 3)
619 array of 3 vectors representing the x, y, z position of catalog
620 objects on the unit sphere.
625 Suggested max match tolerance distance calculated from comparisons
626 between pinwheel patterns used in optimistic/pessimistic pattern
630 self.
log.debug(
"Starting automated tolerance calculation...")
634 pattern_array = np.empty(
635 (cat_array.shape[0] - self.config.numPointsForShape,
636 self.config.numPointsForShape - 1))
637 flux_args_array = np.argsort(cat_array[:, -1])
640 tmp_sort_array = cat_array[flux_args_array]
643 for start_idx
in range(cat_array.shape[0]
644 - self.config.numPointsForShape):
645 pattern_points = tmp_sort_array[start_idx:start_idx
646 + self.config.numPointsForShape, :-1]
647 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
648 pattern_array[start_idx, :] = np.sqrt(
649 pattern_delta[:, 0] ** 2
650 + pattern_delta[:, 1] ** 2
651 + pattern_delta[:, 2] ** 2)
656 pattern_array[start_idx, :] = pattern_array[
657 start_idx, np.argsort(pattern_array[start_idx, :])]
663 pattern_array[:, :(self.config.numPointsForShape - 1)])
664 dist_nearest_array, ids = dist_tree.query(
665 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
666 dist_nearest_array = dist_nearest_array[:, 1]
667 dist_nearest_array.sort()
671 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600.
672 / (self.config.numPointsForShape - 1.))
674 self.
log.debug(
"Automated tolerance")
675 self.
log.debug(
"\tdistance/match tol: %.4f [arcsec]", dist_tol)