3 from scipy.spatial
import cKDTree
4 from scipy.stats
import sigmaclip
7 import lsst.pipe.base
as pipeBase
10 from lsst.meas.algorithms.sourceSelector
import sourceSelectorRegistry
12 from .matchOptimisticB
import MatchTolerance
14 from .pessimistic_pattern_matcher_b_3D
import PessimisticPatternMatcherB
16 __all__ = [
"MatchPessimisticBTask",
"MatchPessimisticBConfig",
17 "MatchTolerancePessimistic"]
21 """Stores match tolerances for use in AstrometryTask and later 22 iterations of the matcher. 24 MatchPessimisticBTask relies on several state variables to be 25 preserved over different iterations in the 26 AstrometryTask.matchAndFitWcs loop of AstrometryTask. 30 maxMatchDist : `lsst.geom.Angle` 31 Maximum distance to consider a match from the previous match/fit 33 autoMaxMatchDist : `lsst.geom.Angle` 34 Automated estimation of the maxMatchDist from the sky statistics of the 35 source and reference catalogs. 36 maxShift : `lsst.geom.Angle` 37 Maximum shift found in the previous match/fit cycle. 38 lastMatchedPattern : `int` 39 Index of the last source pattern that was matched into the reference 41 failedPatternList : `list` of `int` 42 Previous matches were found to be false positives. 43 PPMbObj : `lsst.meas.astrom.PessimisticPatternMatcherB` 44 Initialized Pessimistic pattern matcher object. Storing this prevents 45 the need for recalculation of the searchable distances in the PPMB. 48 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
49 maxShift=None, lastMatchedPattern=None,
50 failedPatternList=None, PPMbObj=None):
56 if failedPatternList
is None:
63 """Configuration for MatchPessimisticBTask 65 numBrightStars = pexConfig.RangeField(
66 doc=
"Number of bright stars to use. Sets the max number of patterns " 67 "that can be tested.",
72 minMatchedPairs = pexConfig.RangeField(
73 doc=
"Minimum number of matched pairs; see also minFracMatchedPairs.",
78 minFracMatchedPairs = pexConfig.RangeField(
79 doc=
"Minimum number of matched pairs as a fraction of the smaller of " 80 "the number of reference stars or the number of good sources; " 81 "the actual minimum is the smaller of this value or " 88 matcherIterations = pexConfig.RangeField(
89 doc=
"Number of softening iterations in matcher.",
94 maxOffsetPix = pexConfig.RangeField(
95 doc=
"Maximum allowed shift of WCS, due to matching (pixel). " 96 "When changing this value, the " 97 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
102 maxRotationDeg = pexConfig.RangeField(
103 doc=
"Rotation angle allowed between sources and position reference " 104 "objects (degrees).",
109 numPointsForShape = pexConfig.Field(
110 doc=
"Number of points to define a shape for matching.",
114 numPointsForShapeAttempt = pexConfig.Field(
115 doc=
"Number of points to try for creating a shape. This value should " 116 "be greater than or equal to numPointsForShape. Besides " 117 "loosening the signal to noise cut in the matcherSourceSelector, " 118 "increasing this number will solve CCDs where no match was found.",
122 minMatchDistPixels = pexConfig.RangeField(
123 doc=
"Distance in units of pixels to always consider a source-" 124 "reference pair a match. This prevents the astrometric fitter " 125 "from over-fitting and removing stars that should be matched and " 126 "allows for inclusion of new matches as the wcs improves.",
132 numPatternConsensus = pexConfig.Field(
133 doc=
"Number of implied shift/rotations from patterns that must agree " 134 "before it a given shift/rotation is accepted. This is only used " 135 "after the first softening iteration fails and if both the " 136 "number of reference and source objects is greater than " 141 sourceSelector = sourceSelectorRegistry.makeField(
142 doc=
"How to select sources for cross-matching. The default " 143 "matcherSourceSelector removes objects with low S/N, bad " 144 "saturated objects, edge objects, and interpolated objects.",
145 default=
"matcherPessimistic" 150 sourceSelector.setDefaults()
153 pexConfig.Config.validate(self)
155 raise ValueError(
"numPointsForShapeAttempt must be greater than " 156 "or equal to numPointsForShape.")
158 raise ValueError(
"numBrightStars must be greater than " 159 "numPointsForShape.")
172 """Match sources to reference objects. 175 ConfigClass = MatchPessimisticBConfig
176 _DefaultName =
"matchObjectsToSources" 179 pipeBase.Task.__init__(self, **kwargs)
180 self.makeSubtask(
"sourceSelector")
184 match_tolerance=None):
185 """Match sources to position reference stars 187 refCat : `lsst.afw.table.SimpleCatalog` 188 catalog of reference objects that overlap the exposure; reads 192 - the specified flux field 194 sourceCat : `lsst.afw.table.SourceCatalog` 195 catalog of sources found on an exposure; Please check the required 196 fields of your specified source selector that the correct flags are present. 197 wcs : `lsst.afw.geom.SkyWcs` 200 field of refCat to use for flux 201 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 202 is a MatchTolerance class object or `None`. This this class is used 203 to communicate state between AstrometryTask and MatcherTask. 204 AstrometryTask will also set the MatchTolerance class variable 205 maxMatchDist based on the scatter AstrometryTask has found after 210 result : `lsst.pipe.base.Struct` 211 Result struct with components: 213 - ``matches`` : source to reference matches found (`list` of 214 `lsst.afw.table.ReferenceMatch`) 215 - ``usableSourcCat`` : a catalog of sources potentially usable for 216 matching and WCS fitting (`lsst.afw.table.SourceCatalog`). 217 - ``match_tolerance`` : a MatchTolerance object containing the 218 resulting state variables from the match 219 (`lsst.meas.astrom.MatchTolerancePessimistic`). 226 if match_tolerance
is None:
230 numSources = len(sourceCat)
231 selectedSources = self.sourceSelector.run(sourceCat)
232 goodSourceCat = selectedSources.sourceCat
233 numUsableSources = len(goodSourceCat)
234 self.log.info(
"Purged %d sources, leaving %d good sources" %
235 (numSources - numUsableSources, numUsableSources))
237 if len(goodSourceCat) == 0:
238 raise pipeBase.TaskError(
"No sources are good")
243 minMatchedPairs = min(self.config.minMatchedPairs,
244 int(self.config.minFracMatchedPairs *
245 min([len(refCat), len(goodSourceCat)])))
249 sourceCat=goodSourceCat,
251 refFluxField=refFluxField,
252 numUsableSources=numUsableSources,
253 minMatchedPairs=minMatchedPairs,
254 match_tolerance=match_tolerance,
255 sourceFluxField=self.sourceSelector.fluxField,
256 verbose=debug.verbose,
258 matches = doMatchReturn.matches
259 match_tolerance = doMatchReturn.match_tolerance
261 if len(matches) == 0:
262 raise RuntimeError(
"Unable to match sources")
264 self.log.info(
"Matched %d sources" % len(matches))
265 if len(matches) < minMatchedPairs:
266 self.log.warn(
"Number of matches is smaller than request")
268 return pipeBase.Struct(
270 usableSourceCat=goodSourceCat,
271 match_tolerance=match_tolerance,
275 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources,
276 minMatchedPairs, match_tolerance, sourceFluxField, verbose):
277 """Implementation of matching sources to position reference stars 279 Unlike matchObjectsToSources, this method does not check if the sources 284 refCat : `lsst.afw.table.SimpleCatalog` 285 catalog of position reference stars that overlap an exposure 286 sourceCat : `lsst.afw.table.SourceCatalog` 287 catalog of sources found on the exposure 288 wcs : `lsst.afw.geom.SkyWcs` 289 estimated WCS of exposure 291 field of refCat to use for flux 292 numUsableSources : `int` 293 number of usable sources (sources with known centroid that are not 294 near the edge, but may be saturated) 295 minMatchedPairs : `int` 296 minimum number of matches 297 match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` 298 a MatchTolerance object containing variables specifying matcher 299 tolerances and state from possible previous runs. 300 sourceFluxField : `str` 301 Name of the flux field in the source catalog. 303 Set true to print diagnostic information to std::cout 308 Results struct with components: 310 - ``matches`` : a list the matches found 311 (`list` of `lsst.afw.table.ReferenceMatch`). 312 - ``match_tolerance`` : MatchTolerance containing updated values from 313 this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`) 322 src_array = np.empty((len(sourceCat), 4), dtype=np.float64)
323 for src_idx, srcObj
in enumerate(sourceCat):
324 coord = wcs.pixelToSky(srcObj.getCentroid())
325 theta = np.pi / 2 - coord.getLatitude().asRadians()
326 phi = coord.getLongitude().asRadians()
327 flux = srcObj.getPsfInstFlux()
328 src_array[src_idx, :] = \
331 if match_tolerance.PPMbObj
is None or \
332 match_tolerance.autoMaxMatchDist
is None:
336 ref_array = np.empty((len(refCat), 4), dtype=np.float64)
337 for ref_idx, refObj
in enumerate(refCat):
338 theta = np.pi / 2 - refObj.getDec().asRadians()
339 phi = refObj.getRa().asRadians()
340 flux = refObj[refFluxField]
341 ref_array[ref_idx, :] = \
345 ref_array[:, :3], self.log)
346 self.log.debug(
"Computing source statistics...")
349 self.log.debug(
"Computing reference statistics...")
352 maxMatchDistArcSec = np.min((maxMatchDistArcSecSrc,
353 maxMatchDistArcSecRef))
354 match_tolerance.autoMaxMatchDist = afwgeom.Angle(
355 maxMatchDistArcSec, afwgeom.arcseconds)
359 if match_tolerance.maxShift
is None:
360 maxShiftArcseconds = (self.config.maxOffsetPix *
361 wcs.getPixelScale().asArcseconds())
365 maxShiftArcseconds = np.max(
366 (match_tolerance.maxShift.asArcseconds(),
367 self.config.minMatchDistPixels *
368 wcs.getPixelScale().asArcseconds()))
374 if match_tolerance.maxMatchDist
is None:
375 match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist
377 maxMatchDistArcSec = np.max(
378 (self.config.minMatchDistPixels *
379 wcs.getPixelScale().asArcseconds(),
380 np.min((match_tolerance.maxMatchDist.asArcseconds(),
381 match_tolerance.autoMaxMatchDist.asArcseconds()))))
386 numConsensus = self.config.numPatternConsensus
387 minObjectsForConsensus = \
388 self.config.numBrightStars + self.config.numPointsForShapeAttempt
389 if len(refCat) < minObjectsForConsensus
or \
390 len(sourceCat) < minObjectsForConsensus:
393 self.log.debug(
"Current tol maxDist: %.4f arcsec" %
395 self.log.debug(
"Current shift: %.4f arcsec" %
400 for soften_dist
in range(self.config.matcherIterations):
401 for soften_pattern
in range(self.config.matcherIterations):
402 if soften_pattern == 0
and soften_dist == 0
and \
403 match_tolerance.lastMatchedPattern
is not None:
412 run_n_consent = numConsensus
415 matcher_struct = match_tolerance.PPMbObj.match(
416 source_array=src_array,
417 n_check=self.config.numPointsForShapeAttempt + soften_pattern,
418 n_match=self.config.numPointsForShape,
419 n_agree=run_n_consent,
420 max_n_patterns=self.config.numBrightStars,
421 max_shift=maxShiftArcseconds,
422 max_rotation=self.config.maxRotationDeg,
423 max_dist=maxMatchDistArcSec * 2. ** soften_dist,
424 min_matches=minMatchedPairs,
425 pattern_skip_array=np.array(
426 match_tolerance.failedPatternList)
429 if soften_pattern == 0
and soften_dist == 0
and \
430 len(matcher_struct.match_ids) == 0
and \
431 match_tolerance.lastMatchedPattern
is not None:
438 match_tolerance.failedPatternList.append(
439 match_tolerance.lastMatchedPattern)
440 match_tolerance.lastMatchedPattern =
None 441 maxShiftArcseconds = \
442 self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()
443 elif len(matcher_struct.match_ids) > 0:
446 match_tolerance.maxShift = \
447 matcher_struct.shift * afwgeom.arcseconds
448 match_tolerance.lastMatchedPattern = \
449 matcher_struct.pattern_idx
457 return pipeBase.Struct(
459 match_tolerance=match_tolerance,
471 distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600
472 clip_max_dist = np.max(
473 (sigmaclip(distances_arcsec, low=100, high=2)[-1],
474 self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds())
479 if not np.isfinite(clip_max_dist):
480 clip_max_dist = maxMatchDistArcSec
482 if clip_max_dist < maxMatchDistArcSec
and \
483 len(distances_arcsec[distances_arcsec < clip_max_dist]) < \
485 dist_cut_arcsec = maxMatchDistArcSec
487 dist_cut_arcsec = np.min((clip_max_dist, maxMatchDistArcSec))
492 for match_id_pair, dist_arcsec
in zip(matcher_struct.match_ids,
494 if dist_arcsec < dist_cut_arcsec:
496 match.first = refCat[match_id_pair[1]]
497 match.second = sourceCat[match_id_pair[0]]
501 match.distance = match.first.getCoord().separation(
502 match.second.getCoord()).asArcseconds()
503 matches.append(match)
505 return pipeBase.Struct(
507 match_tolerance=match_tolerance,
510 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
511 """Convert angles theta and phi and a flux into unit sphere 512 x, y, z, and a relative magnitude. 514 Takes in a afw catalog object and converts the catalog object RA, DECs 515 to points on the unit sphere. Also converts the flux into a simple, 516 non-zero-pointed magnitude for relative sorting. 521 Angle from the north pole (z axis) of the sphere 523 Rotation around the sphere 527 output_array : `numpy.ndarray`, (N, 4) 528 Spherical unit vector x, y, z with flux. 530 output_array = np.empty(4, dtype=np.float64)
531 output_array[0] = np.sin(theta)*np.cos(phi)
532 output_array[1] = np.sin(theta)*np.sin(phi)
533 output_array[2] = np.cos(theta)
535 output_array[3] = -2.5 * np.log10(flux)
539 output_array[3] = 99.
543 def _get_pair_pattern_statistics(self, cat_array):
544 """ Compute the tolerances for the matcher automatically by comparing 545 pinwheel patterns as we would in the matcher. 547 We test how similar the patterns we can create from a given set of 548 objects by computing the spoke lengths for each pattern and sorting 549 them from smallest to largest. The match tolerance is the average 550 distance per spoke between the closest two patterns in the sorted 555 cat_array : `numpy.ndarray`, (N, 3) 556 array of 3 vectors representing the x, y, z position of catalog 557 objects on the unit sphere. 562 Suggested max match tolerance distance calculated from comparisons 563 between pinwheel patterns used in optimistic/pessimistic pattern 567 self.log.debug(
"Starting automated tolerance calculation...")
571 pattern_array = np.empty(
572 (cat_array.shape[0] - self.config.numPointsForShape,
573 self.config.numPointsForShape - 1))
574 flux_args_array = np.argsort(cat_array[:, -1])
577 tmp_sort_array = cat_array[flux_args_array]
580 for start_idx
in range(cat_array.shape[0] -
581 self.config.numPointsForShape):
582 pattern_points = tmp_sort_array[start_idx:start_idx +
583 self.config.numPointsForShape, :-1]
584 pattern_delta = pattern_points[1:, :] - pattern_points[0, :]
585 pattern_array[start_idx, :] = np.sqrt(
586 pattern_delta[:, 0] ** 2 +
587 pattern_delta[:, 1] ** 2 +
588 pattern_delta[:, 2] ** 2)
593 pattern_array[start_idx, :] = pattern_array[
594 start_idx, np.argsort(pattern_array[start_idx, :])]
600 pattern_array[:, :(self.config.numPointsForShape - 1)])
601 dist_nearest_array, ids = dist_tree.query(
602 pattern_array[:, :(self.config.numPointsForShape - 1)], k=2)
603 dist_nearest_array = dist_nearest_array[:, 1]
604 dist_nearest_array.sort()
608 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600. /
609 (self.config.numPointsForShape - 1.))
611 self.log.debug(
"Automated tolerance")
612 self.log.debug(
"\tdistance/match tol: %.4f [arcsec]" % dist_tol)
def __init__(self, kwargs)
def __init__(self, maxMatchDist=None, autoMaxMatchDist=None, maxShift=None, lastMatchedPattern=None, failedPatternList=None, PPMbObj=None)
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose)
def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField, match_tolerance=None)
def _get_pair_pattern_statistics(self, cat_array)
def _latlong_flux_to_xyz_mag(self, theta, phi, flux)