LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
matchPessimisticB.py
Go to the documentation of this file.
1# This file is part of meas_astrom.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22__all__ = ["MatchPessimisticBTask", "MatchPessimisticBConfig",
23 "MatchTolerancePessimistic"]
24
25import numpy as np
26from scipy.spatial import cKDTree
27
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30import lsst.geom as geom
31import lsst.afw.table as afwTable
32from lsst.utils.timer import timeMethod
33
34from . import exceptions
35from .matchOptimisticBTask import MatchTolerance
36from .pessimistic_pattern_matcher_b_3D import PessimisticPatternMatcherB
37
38
40 """Stores match tolerances for use in AstrometryTask and later
41 iterations of the matcher.
42
43 MatchPessimisticBTask relies on several state variables to be
44 preserved over different iterations in the
45 AstrometryTask.matchAndFitWcs loop of AstrometryTask.
46
47 Parameters
48 ----------
49 maxMatchDist : `lsst.geom.Angle`
50 Maximum distance to consider a match from the previous match/fit
51 iteration.
52 autoMaxMatchDist : `lsst.geom.Angle`
53 Automated estimation of the maxMatchDist from the sky statistics of the
54 source and reference catalogs.
55 maxShift : `lsst.geom.Angle`
56 Maximum shift found in the previous match/fit cycle.
57 lastMatchedPattern : `int`
58 Index of the last source pattern that was matched into the reference
59 data.
60 failedPatternList : `list` of `int`
61 Previous matches were found to be false positives.
62 PPMbObj : `lsst.meas.astrom.PessimisticPatternMatcherB`
63 Initialized Pessimistic pattern matcher object. Storing this prevents
64 the need for recalculation of the searchable distances in the PPMB.
65 """
66
67 def __init__(self, maxMatchDist=None, autoMaxMatchDist=None,
68 maxShift=None, lastMatchedPattern=None,
69 failedPatternList=None, PPMbObj=None):
70 self.maxMatchDistmaxMatchDist = maxMatchDist
71 self.autoMaxMatchDist = autoMaxMatchDist
72 self.maxShift = maxShift
73 self.lastMatchedPattern = lastMatchedPattern
74 self.PPMbObj = PPMbObj
75 if failedPatternList is None:
77 else:
78 self.failedPatternList = failedPatternList
79
80
81class MatchPessimisticBConfig(pexConfig.Config):
82 """Configuration for MatchPessimisticBTask
83 """
84 numBrightStars = pexConfig.RangeField(
85 doc="Maximum number of bright stars to use. Sets the max number of patterns "
86 "that can be tested.",
87 dtype=int,
88 default=150,
89 min=2,
90 )
91 minMatchedPairs = pexConfig.RangeField(
92 doc="Minimum number of matched pairs; see also minFracMatchedPairs.",
93 dtype=int,
94 default=30,
95 min=2,
96 )
97 minFracMatchedPairs = pexConfig.RangeField(
98 doc="Minimum number of matched pairs as a fraction of the smaller of "
99 "the number of reference stars or the number of good sources; "
100 "the actual minimum is the smaller of this value or "
101 "minMatchedPairs.",
102 dtype=float,
103 default=0.3,
104 min=0,
105 max=1,
106 )
107 matcherIterations = pexConfig.RangeField(
108 doc="Number of softening iterations in matcher.",
109 dtype=int,
110 default=5,
111 min=1,
112 )
113 maxOffsetPix = pexConfig.RangeField(
114 doc="Maximum allowed shift of WCS, due to matching (pixel). "
115 "When changing this value, the "
116 "LoadReferenceObjectsConfig.pixelMargin should also be updated.",
117 dtype=int,
118 default=250,
119 max=4000,
120 )
121 maxRotationDeg = pexConfig.RangeField(
122 doc="Rotation angle allowed between sources and position reference "
123 "objects (degrees).",
124 dtype=float,
125 default=1.0,
126 max=6.0,
127 )
128 numPointsForShape = pexConfig.Field(
129 doc="Number of points to define a shape for matching.",
130 dtype=int,
131 default=6,
132 )
133 numPointsForShapeAttempt = pexConfig.Field(
134 doc="Number of points to try for creating a shape. This value should "
135 "be greater than or equal to numPointsForShape. Besides "
136 "loosening the signal to noise cut in the 'matcher' SourceSelector, "
137 "increasing this number will solve CCDs where no match was found.",
138 dtype=int,
139 default=6,
140 )
141 minMatchDistPixels = pexConfig.RangeField(
142 doc="Distance in units of pixels to always consider a source-"
143 "reference pair a match. This prevents the astrometric fitter "
144 "from over-fitting and removing stars that should be matched and "
145 "allows for inclusion of new matches as the wcs improves.",
146 dtype=float,
147 default=1.0,
148 min=0.0,
149 max=6.0,
150 )
151 numPatternConsensus = pexConfig.Field(
152 doc="Number of implied shift/rotations from patterns that must agree "
153 "before it a given shift/rotation is accepted. This is only used "
154 "after the first softening iteration fails and if both the "
155 "number of reference and source objects is greater than "
156 "numBrightStars.",
157 dtype=int,
158 default=3,
159 )
160 numRefRequireConsensus = pexConfig.Field(
161 doc="If the available reference objects exceeds this number, "
162 "consensus/pessimistic mode will enforced regardless of the "
163 "number of available sources. Below this optimistic mode ("
164 "exit at first match rather than requiring numPatternConsensus to "
165 "be matched) can be used. If more sources are required to match, "
166 "decrease the signal to noise cut in the sourceSelector.",
167 dtype=int,
168 default=1000,
169 )
170 maxRefObjects = pexConfig.RangeField(
171 doc="Maximum number of reference objects to use for the matcher. The "
172 "absolute maximum allowed for is 2 ** 16 for memory reasons.",
173 dtype=int,
174 default=2**16,
175 min=0,
176 max=2**16 + 1,
177 )
178
179 def validate(self):
180 pexConfig.Config.validate(self)
182 raise ValueError("numPointsForShapeAttempt must be greater than "
183 "or equal to numPointsForShape.")
184 if self.numPointsForShape > self.numBrightStars:
185 raise ValueError("numBrightStars must be greater than "
186 "numPointsForShape.")
187
188
189# The following block adds links to this task from the Task Documentation page.
190# \addtogroup LSST_task_documentation
191# \{
192# \page measAstrom_MatchPessimisticBTask
193# \ref MatchPessimisticBTask "MatchPessimisticBTask"
194# Match sources to reference objects
195# \}
196
197
198class MatchPessimisticBTask(pipeBase.Task):
199 """Match sources to reference objects.
200 """
201
202 ConfigClass = MatchPessimisticBConfig
203 _DefaultName = "matchPessimisticB"
204
205 def __init__(self, **kwargs):
206 pipeBase.Task.__init__(self, **kwargs)
207
208 @timeMethod
209 def matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField,
210 matchTolerance=None, bbox=None):
211 """Match sources to position reference stars
212
213 refCat : `lsst.afw.table.SimpleCatalog`
214 catalog of reference objects that overlap the exposure; reads
215 fields for:
216
217 - coord
218 - the specified flux field
219
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`
224 estimated WCS
225 sourceFluxField: `str`
226 field of sourceCat to use for flux
227 refFluxField : `str`
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
234 fitting for the wcs.
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``
238 is `None`).
239
240 Returns
241 -------
242 result : `lsst.pipe.base.Struct`
243 Result struct with components:
244
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`).
252 """
253 import lsstDebug
254 debug = lsstDebug.Info(__name__)
255
256 # If we get an empty tolerance struct create the variables we need for
257 # this matcher.
258 if matchTolerance is None:
259 matchTolerance = MatchTolerancePessimistic()
260
261 # Make a name alias here for consistency with older code, and to make
262 # it clear that this is a good/usable (cleaned) source catalog.
263 goodSourceCat = sourceCat
264
265 if (numUsableSources := len(goodSourceCat)) == 0:
266 raise exceptions.MatcherFailure("No sources are good")
267
268 minMatchedPairs = min(self.config.minMatchedPairs,
269 int(self.config.minFracMatchedPairs
270 * min([len(refCat), len(goodSourceCat)])))
271
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}).")
280
281 if len(refCat) > self.config.maxRefObjects:
282 self.log.warning(
283 "WARNING: Reference catalog larger than maximum allowed. "
284 "Trimming to %i", self.config.maxRefObjects)
285 trimmedRefCat = self._filterRefCat(refCat, refFluxField)
286 else:
287 trimmedRefCat = refCat
288
289 doMatchReturn = self._doMatch(
290 refCat=trimmedRefCat,
291 sourceCat=goodSourceCat,
292 wcs=wcs,
293 refFluxField=refFluxField,
294 numUsableSources=numUsableSources,
295 minMatchedPairs=minMatchedPairs,
296 matchTolerance=matchTolerance,
297 sourceFluxField=sourceFluxField,
298 verbose=debug.verbose,
299 bbox=bbox,
300 )
301 matches = doMatchReturn.matches
302 matchTolerance = doMatchReturn.matchTolerance
303
304 if (nMatches := len(matches)) == 0:
305 raise exceptions.MatcherFailure("No matches found")
306
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)
311
312 return pipeBase.Struct(
313 matches=matches,
314 usableSourceCat=goodSourceCat,
315 matchTolerance=matchTolerance,
316 )
317
318 def _filterRefCat(self, refCat, refFluxField):
319 """Sub-select a number of reference objects starting from the brightest
320 and maxing out at the number specified by maxRefObjects in the config.
321
322 No trimming is done if len(refCat) > config.maxRefObjects.
323
324 Parameters
325 ----------
326 refCat : `lsst.afw.table.SimpleCatalog`
327 Catalog of reference objects to trim.
328 refFluxField : `str`
329 field of refCat to use for flux
330 Returns
331 -------
332 outCat : `lsst.afw.table.SimpleCatalog`
333 Catalog trimmed to the number set in the task config from the
334 brightest flux down.
335 """
336 # Find the flux cut that gives us the desired number of objects.
337 if len(refCat) <= self.config.maxRefObjects:
338 return refCat
339 fluxArray = refCat.get(refFluxField)
340 sortedFluxArray = fluxArray[fluxArray.argsort()]
341 minFlux = sortedFluxArray[-(self.config.maxRefObjects + 1)]
342
343 selected = (refCat.get(refFluxField) > minFlux)
344
345 outCat = afwTable.SimpleCatalog(refCat.schema)
346 outCat.reserve(self.config.maxRefObjects)
347 outCat.extend(refCat[selected])
348
349 return outCat
350
351 @timeMethod
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
355
356 Unlike matchObjectsToSources, this method does not check if the sources
357 are suitable.
358
359 Parameters
360 ----------
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
367 refFluxField : `str`
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.
379 verbose : `bool`
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``
384 is `None`).
385
386 Returns
387 -------
388 result : `lsst.pipe.base.Struct`
389 Results struct with components:
390
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`)
395 """
396 if bbox is not None:
397 pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
398 else:
399 pixelScale = wcs.getPixelScale().asArcseconds()
400
401 # Load the source and reference catalog as spherical points
402 # in numpy array. We do this rather than relying on internal
403 # lsst C objects for simplicity and because we require
404 # objects contiguous in memory. We need to do these slightly
405 # differently for the reference and source cats as they are
406 # different catalog objects with different fields.
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, :] = \
414 self._latlong_flux_to_xyz_mag(theta, phi, flux)
415
416 if matchTolerance.PPMbObj is None or \
417 matchTolerance.autoMaxMatchDist is None:
418 # The reference catalog is fixed per AstrometryTask so we only
419 # create the data needed if this is the first step in the match
420 # fit cycle.
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, :] = \
427 self._latlong_flux_to_xyz_mag(theta, phi, flux)
428 # Create our matcher object.
429 matchTolerance.PPMbObj = PessimisticPatternMatcherB(
430 ref_array[:, :3], self.log)
431 self.log.debug("Computing source statistics...")
432 maxMatchDistArcSecSrc = self._get_pair_pattern_statistics(
433 src_array)
434 self.log.debug("Computing reference statistics...")
435 maxMatchDistArcSecRef = self._get_pair_pattern_statistics(
436 ref_array)
437 maxMatchDistArcSec = np.max((
438 self.config.minMatchDistPixels * pixelScale,
439 np.min((maxMatchDistArcSecSrc,
440 maxMatchDistArcSecRef))))
441 matchTolerance.autoMaxMatchDist = geom.Angle(
442 maxMatchDistArcSec, geom.arcseconds)
443
444 # Set configurable defaults when we encounter None type or set
445 # state based on previous run of AstrometryTask._matchAndFitWcs.
446 if matchTolerance.maxShift is None:
447 maxShiftArcseconds = self.config.maxOffsetPix * pixelScale
448 else:
449 # We don't want to clamp down too hard on the allowed shift so
450 # we test that the smallest we ever allow is the pixel scale.
451 maxShiftArcseconds = np.max(
452 (matchTolerance.maxShift.asArcseconds(),
453 self.config.minMatchDistPixels * pixelScale))
454
455 # If our tolerances are not set from a previous run, estimate a
456 # starting tolerance guess from the statistics of patterns we can
457 # create on both the source and reference catalog. We use the smaller
458 # of the two.
459 if matchTolerance.maxMatchDist is None:
460 matchTolerance.maxMatchDist = matchTolerance.autoMaxMatchDist
461 else:
462 maxMatchDistArcSec = np.max(
463 (self.config.minMatchDistPixels * pixelScale,
464 np.min((matchTolerance.maxMatchDist.asArcseconds(),
465 matchTolerance.autoMaxMatchDist.asArcseconds()))))
466
467 # Make sure the data we are considering is dense enough to require
468 # the consensus mode of the matcher. If not default to Optimistic
469 # pattern matcher behavior. We enforce pessimistic mode if the
470 # reference cat is sufficiently large, avoiding false positives.
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:
478 numConsensus = 1
479
480 self.log.debug("Current tol maxDist: %.4f arcsec",
481 maxMatchDistArcSec)
482 self.log.debug("Current shift: %.4f arcsec",
483 maxShiftArcseconds)
484
485 match_found = False
486 # Start the iteration over our tolerances.
487 for soften_dist in range(self.config.matcherIterations):
488 if soften_dist == 0 and \
489 matchTolerance.lastMatchedPattern is not None:
490 # If we are on the first, most stringent tolerance,
491 # and have already found a match, the matcher should behave
492 # like an optimistic pattern matcher. Exiting at the first
493 # match.
494 run_n_consent = 1
495 else:
496 # If we fail or this is the first match attempt, set the
497 # pattern consensus to the specified config value.
498 run_n_consent = numConsensus
499 # We double the match dist tolerance each round and add 1 to the
500 # to the number of candidate spokes to check.
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)
513 )
514
515 if soften_dist == 0 and \
516 len(matcher_struct.match_ids) == 0 and \
517 matchTolerance.lastMatchedPattern is not None:
518 # If we found a pattern on a previous match-fit iteration and
519 # can't find an optimistic match on our first try with the
520 # tolerances as found in the previous match-fit,
521 # the match we found in the last iteration was likely bad. We
522 # append the bad match's index to the a list of
523 # patterns/matches to skip on subsequent iterations.
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:
529 # Match found, save a bit a state regarding this pattern
530 # in the match tolerance class object and exit.
531 matchTolerance.maxShift = \
532 matcher_struct.shift * geom.arcseconds
533 matchTolerance.lastMatchedPattern = \
534 matcher_struct.pattern_idx
535 match_found = True
536 break
537
538 # If we didn't find a match, exit early.
539 if not match_found:
540 return pipeBase.Struct(
541 matches=[],
542 matchTolerance=matchTolerance,
543 )
544
545 # The matcher returns all the nearest neighbors that agree between
546 # the reference and source catalog. For the current astrometric solver
547 # we need to remove as many false positives as possible before sending
548 # the matches off to the solver. The low value of 100 and high value of
549 # 2 are the low number of sigma and high respectively. The exact values
550 # were found after testing on data of various reference/source
551 # densities and astrometric distortion quality, specifically the
552 # visits: HSC (3358), DECam (406285, 410827),
553 # CFHT (793169, 896070, 980526).
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))
558
559 # A match has been found, return our list of matches and
560 # return.
561 matches = []
562 for match_id_pair, dist_arcsec in zip(matcher_struct.match_ids,
563 distances_arcsec):
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])]
568 # We compute the true distance along and sphere. This isn't
569 # used in the WCS fitter however it is used in the unittest
570 # to confirm the matches computed.
571 match.distance = match.first.getCoord().separation(
572 match.second.getCoord()).asArcseconds()
573 matches.append(match)
574
575 return pipeBase.Struct(
576 matches=matches,
577 matchTolerance=matchTolerance,
578 )
579
580 def _latlong_flux_to_xyz_mag(self, theta, phi, flux):
581 """Convert angles theta and phi and a flux into unit sphere
582 x, y, z, and a relative magnitude.
583
584 Takes in a afw catalog object and converts the catalog object RA, DECs
585 to points on the unit sphere. Also converts the flux into a simple,
586 non-zero-pointed magnitude for relative sorting.
587
588 Parameters
589 ----------
590 theta : `float`
591 Angle from the north pole (z axis) of the sphere
592 phi : `float`
593 Rotation around the sphere
594
595 Return
596 ------
597 output_array : `numpy.ndarray`, (N, 4)
598 Spherical unit vector x, y, z with flux.
599 """
600 output_array = np.empty(4, dtype=np.float64)
601 output_array[0] = np.sin(theta)*np.cos(phi)
602 output_array[1] = np.sin(theta)*np.sin(phi)
603 output_array[2] = np.cos(theta)
604 if flux > 0:
605 output_array[3] = -2.5 * np.log10(flux)
606 else:
607 # Set flux to a very faint mag if its for some reason it
608 # does not exist
609 output_array[3] = 99.
610
611 return output_array
612
613 def _get_pair_pattern_statistics(self, cat_array):
614 """ Compute the tolerances for the matcher automatically by comparing
615 pinwheel patterns as we would in the matcher.
616
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
621 spoke length space.
622
623 Parameters
624 ----------
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.
628
629 Returns
630 -------
631 dist_tol : `float`
632 Suggested max match tolerance distance calculated from comparisons
633 between pinwheel patterns used in optimistic/pessimistic pattern
634 matcher.
635 """
636
637 self.log.debug("Starting automated tolerance calculation...")
638
639 # Create an empty array of all the patterns we possibly make
640 # sorting from brightest to faintest.
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])
645
646 # Sort our input array.
647 tmp_sort_array = cat_array[flux_args_array]
648
649 # Start making patterns.
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)
659
660 # When we store the length of each spoke in our pattern we
661 # sort from shortest to longest so we have a defined space
662 # to compare them in.
663 pattern_array[start_idx, :] = pattern_array[
664 start_idx, np.argsort(pattern_array[start_idx, :])]
665
666 # Create a searchable tree object of the patterns and find
667 # for any given pattern the closest pattern in the sorted
668 # spoke length space.
669 dist_tree = cKDTree(
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()
675
676 # We use the two closest patterns to set our tolerance.
677 dist_idx = 0
678 dist_tol = (np.degrees(dist_nearest_array[dist_idx]) * 3600.
679 / (self.config.numPointsForShape - 1.))
680
681 self.log.debug("Automated tolerance")
682 self.log.debug("\tdistance/match tol: %.4f [arcsec]", dist_tol)
683
684 return dist_tol
int min
Tag types used to declare specialized field types.
Definition misc.h:31
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
A class representing an angle.
Definition Angle.h:128
matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField, matchTolerance=None, bbox=None)
_doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, matchTolerance, sourceFluxField, verbose, bbox=None)
__init__(self, maxMatchDist=None, autoMaxMatchDist=None, maxShift=None, lastMatchedPattern=None, failedPatternList=None, PPMbObj=None)