LSST Applications g00d0e8bbd7+edbf708997,g03191d30f7+6b31559d11,g118115db7c+ac820e85d2,g199a45376c+5137f08352,g1fd858c14a+90100aa1a7,g262e1987ae+64df5f6984,g29ae962dfc+1eb4aece83,g2cef7863aa+73c82f25e4,g3541666cd7+1e37cdad5c,g35bb328faa+edbf708997,g3fd5ace14f+fb4e2866cc,g47891489e3+19fcc35de2,g53246c7159+edbf708997,g5b326b94bb+d622351b67,g64539dfbff+dfe1dff262,g67b6fd64d1+19fcc35de2,g74acd417e5+cfdc02aca8,g786e29fd12+af89c03590,g7aefaa3e3d+dc1a598170,g87389fa792+a4172ec7da,g88cb488625+60ba2c3075,g89139ef638+19fcc35de2,g8d4809ba88+dfe1dff262,g8d7436a09f+db94b797be,g8ea07a8fe4+79658f16ab,g90f42f885a+6577634e1f,g9722cb1a7f+d8f85438e7,g98df359435+7fdd888faa,ga2180abaac+edbf708997,ga9e74d7ce9+128cc68277,gbf99507273+edbf708997,gca7fc764a6+19fcc35de2,gd7ef33dd92+19fcc35de2,gdab6d2f7ff+cfdc02aca8,gdbb4c4dda9+dfe1dff262,ge410e46f29+19fcc35de2,ge41e95a9f2+dfe1dff262,geaed405ab2+062dfc8cdc,w.2025.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
astrometry.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__ = ["AstrometryConfig", "AstrometryTask"]
23
24import numpy as np
25from astropy import units
26import scipy.stats
27
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30from lsst.utils.timer import timeMethod
31from . import exceptions
32from .ref_match import RefMatchTask, RefMatchConfig
33from .fitTanSipWcs import FitTanSipWcsTask
34from .display import displayAstrometry
35
36
38 """Config for AstrometryTask.
39 """
40 wcsFitter = pexConfig.ConfigurableField(
41 target=FitTanSipWcsTask,
42 doc="WCS fitter",
43 )
44 forceKnownWcs = pexConfig.Field(
45 dtype=bool,
46 doc="If True then load reference objects and match sources but do not fit a WCS; "
47 "this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
48 default=False,
49 )
50 maxIter = pexConfig.RangeField(
51 doc="maximum number of iterations of match sources and fit WCS"
52 "ignored if not fitting a WCS",
53 dtype=int,
54 default=3,
55 min=1,
56 )
57 minMatchDistanceArcSec = pexConfig.RangeField(
58 doc="the match distance below which further iteration is pointless (arcsec); "
59 "ignored if not fitting a WCS",
60 dtype=float,
61 default=0.001,
62 min=0,
63 )
64 maxMeanDistanceArcsec = pexConfig.RangeField(
65 doc="Maximum mean on-sky distance (in arcsec) between matched source and reference "
66 "objects post-fit. A mean distance greater than this threshold raises BadAstrometryFit "
67 "and the WCS fit is considered a failure. The default is set to the maximum tolerated "
68 "by the external global calibration (e.g. jointcal) step for conceivable recovery; "
69 "the appropriate value will be dataset and workflow dependent.",
70 dtype=float,
71 default=0.5,
72 min=0,
73 )
74 doMagnitudeOutlierRejection = pexConfig.Field(
75 dtype=bool,
76 doc=("If True then a rough zeropoint will be computed from matched sources "
77 "and outliers will be rejected in the iterations."),
78 default=True,
79 )
80 magnitudeOutlierRejectionNSigma = pexConfig.Field(
81 dtype=float,
82 doc=("Number of sigma (measured from the distribution) in magnitude "
83 "for a potential reference/source match to be rejected during "
84 "iteration."),
85 default=3.0,
86 )
87 fiducialZeroPoint = pexConfig.DictField(
88 keytype=str,
89 itemtype=float,
90 doc="Fiducial zeropoint values, keyed by band.",
91 default=None,
92 optional=True,
93 )
94 doFiducialZeroPointCull = pexConfig.Field(
95 dtype=bool,
96 doc="If True, use the obs_package-defined fiducial zeropoint values to compute the "
97 "expected zeropoint for the current exposure. This is for use in culling reference "
98 "objects down to the approximate magnitude range of the detection source catalog "
99 "used for astrometric calibration.",
100 default=False,
101 )
102 cullMagBuffer = pexConfig.Field(
103 dtype=float,
104 doc="Generous buffer on the fiducial zero point culling to account for observing "
105 "condition variations and uncertainty of the fiducial values.",
106 default=0.3,
107 optional=True,
108 )
109 maxRefToSourceRatio = pexConfig.Field(
110 dtype=float,
111 doc="Maximum ratio of loaded reference objects to detected sources in play. If exceded "
112 "the source catalog will be trimmed to the minimum (i.e. brightest) mag of the "
113 "reference catalog.",
114 default=20.0,
115 )
116 filterMap = pexConfig.DictField(
117 doc="Mapping from physical filter label to reference filter name.",
118 keytype=str,
119 itemtype=str,
120 default={},
121 )
122 refColorDeltaDefaults = pexConfig.DictField(
123 doc="Fallback values for color differences between the reference band and the "
124 "physical filter of the observation (note that these values apply to LSSTCam "
125 "filters and may not be appropriate for others).",
126 keytype=str,
127 itemtype=float,
128 default={"u": -1.5, "g": -0.6, "r": 0.0, "i": 0.5, "z": 0.6},
129 )
130
131 def setDefaults(self):
132 super().setDefaults()
133 # Astrometry needs sources to be isolated, too.
134 self.sourceSelector["science"].doRequirePrimary = True
135 self.sourceSelector["science"].doIsolated = True
136 self.sourceSelector["science"].doCentroidErrorLimit = True
137 self.referenceSelector.doCullFromMaskedRegion = True
138
139 def validate(self):
140 super().validate()
141 if self.doFiducialZeroPointCull and self.fiducialZeroPoint is None:
142 msg = "doFiducialZeroPointCull=True requires `fiducialZeroPoint`, a dict of the "
143 "fiducial zeropoints measured for the camera/filter, be set."
144 raise pexConfig.FieldValidationError(AstrometryConfig.fiducialZeroPoint, self, msg)
145
146
148 """Match an input source catalog with objects from a reference catalog and
149 solve for the WCS.
150
151 This task is broken into two main subasks: matching and WCS fitting which
152 are very interactive. The matching here can be considered in part a first
153 pass WCS fitter due to the fitter's sensitivity to outliers.
154
155 Parameters
156 ----------
157 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
158 A reference object loader object; gen3 pipeline tasks will pass `None`
159 and call `setRefObjLoader` in `runQuantum`.
160 schema : `lsst.afw.table.Schema`
161 Used to set "calib_astrometry_used" flag in output source catalog.
162 **kwargs
163 Additional keyword arguments for pipe_base
164 `lsst.pipe.base.Task.__init__`.
165 """
166 ConfigClass = AstrometryConfig
167 _DefaultName = "astrometricSolver"
168
169 def __init__(self, refObjLoader=None, schema=None, **kwargs):
170 RefMatchTask.__init__(self, refObjLoader=refObjLoader, **kwargs)
171
172 if schema is not None:
173 self.usedKey = schema.addField("calib_astrometry_used", type="Flag",
174 doc="set if source was used in astrometric calibration")
175 else:
176 self.usedKey = None
177
178 self.makeSubtask("wcsFitter")
179
180 @timeMethod
181 def run(self, sourceCat, exposure):
182 """Load reference objects, match sources and optionally fit a WCS.
183
184 This is a thin layer around solve or loadAndMatch, depending on
185 config.forceKnownWcs.
186
187 Parameters
188 ----------
189 exposure : `lsst.afw.image.Exposure`
190 exposure whose WCS is to be fit
191 The following are read only:
192
193 - bbox
194 - filter (may be unset)
195 - detector (if wcs is pure tangent; may be absent)
196
197 The following are updated:
198
199 - wcs (the initial value is used as an initial guess, and is
200 required)
201
202 sourceCat : `lsst.afw.table.SourceCatalog`
203 catalog of sources detected on the exposure
204
205 Returns
206 -------
207 result : `lsst.pipe.base.Struct`
208 with these fields:
209
210 - ``refCat`` : reference object catalog of objects that overlap the
211 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
212 - ``matches`` : astrometric matches
213 (`list` of `lsst.afw.table.ReferenceMatch`).
214 - ``scatterOnSky`` : median on-sky separation between reference
215 objects and sources in "matches"
216 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
217 - ``matchMeta`` : metadata needed to unpersist matches
218 (`lsst.daf.base.PropertyList`)
219 """
220 if self.refObjLoader is None:
221 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
222 if self.config.forceKnownWcs:
223 res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
224 res.scatterOnSky = None
225 else:
226 res = self.solve(exposure=exposure, sourceCat=sourceCat)
227 return res
228
229 @timeMethod
230 def solve(self, exposure, sourceCat):
231 """Load reference objects overlapping an exposure, match to sources and
232 fit a WCS
233
234 Returns
235 -------
236 result : `lsst.pipe.base.Struct`
237 Result struct with components:
238
239 - ``refCat`` : reference object catalog of objects that overlap the
240 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
241 - ``matches`` : astrometric matches
242 (`list` of `lsst.afw.table.ReferenceMatch`).
243 - ``scatterOnSky`` : median on-sky separation between reference
244 objects and sources in "matches" (`lsst.geom.Angle`)
245 - ``matchMeta`` : metadata needed to unpersist matches
246 (`lsst.daf.base.PropertyList`)
247
248 Raises
249 ------
250 BadAstrometryFit
251 If the measured mean on-sky distance between the matched source and
252 reference objects is greater than
253 ``self.config.maxMeanDistanceArcsec``.
254
255 Notes
256 -----
257 ignores config.forceKnownWcs
258 """
259 if self.refObjLoader is None:
260 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
261 import lsstDebug
262 debug = lsstDebug.Info(__name__)
263
264 epoch = exposure.visitInfo.date.toAstropy()
265 band = exposure.filter.bandLabel
266
267 sourceSelection = self.sourceSelector.run(sourceCat)
268
269 self.log.info("Purged %d sources, leaving %d good sources",
270 len(sourceCat) - len(sourceSelection.sourceCat),
271 len(sourceSelection.sourceCat))
272 if len(sourceSelection.sourceCat) == 0:
274 "No good sources selected for astrometry.",
275 lenSourceSelectionCat=len(sourceSelection.sourceCat)
276 )
277
278 loadResult = self.refObjLoader.loadPixelBox(
279 bbox=exposure.getBBox(),
280 wcs=exposure.wcs,
281 filterName=band,
282 epoch=epoch,
283 )
284
285 refSelection = self.referenceSelector.run(loadResult.refCat, exposure=exposure)
286 refCat = refSelection.sourceCat
287
288 if self.config.doFiducialZeroPointCull:
289 refCat, sourceSelection.sourceCat = self._do_fiducial_zeropoint_culling(
290 band,
291 loadResult.fluxField,
292 refCat, sourceSelection.sourceCat,
293 exposure.visitInfo.getExposureTime()
294 )
295
296 # Some operations below require catalog contiguity, which is not
297 # guaranteed from the source selector.
298 if not refCat.isContiguous():
299 refCat = refCat.copy(deep=True)
300
301 if debug.display:
302 frame = int(debug.frame)
304 refCat=refCat,
305 sourceCat=sourceSelection.sourceCat,
306 exposure=exposure,
307 bbox=exposure.getBBox(),
308 frame=frame,
309 title="Reference catalog",
310 )
311
312 result = pipeBase.Struct(matchTolerance=None)
313 maxMatchDistance = np.inf
314 i = 0
315 while (maxMatchDistance > self.config.minMatchDistanceArcSec and i < self.config.maxIter):
316 i += 1
317 try:
318 result = self._matchAndFitWcs(
319 refCat=refCat,
320 sourceCat=sourceCat,
321 goodSourceCat=sourceSelection.sourceCat,
322 refFluxField=loadResult.fluxField,
323 bbox=exposure.getBBox(),
324 wcs=exposure.wcs,
325 exposure=exposure,
326 matchTolerance=result.matchTolerance,
327 )
328 exposure.setWcs(result.wcs)
329 except exceptions.AstrometryError as e:
330 e._metadata['iterations'] = i
331 sourceCat["coord_ra"] = np.nan
332 sourceCat["coord_dec"] = np.nan
333 exposure.setWcs(None)
334 self.log.error("Failure fitting astrometry. %s: %s", type(e).__name__, e)
335 raise
336
337 result.stats = self._computeMatchStatsOnSky(result.matches)
338 maxMatchDistance = result.stats.maxMatchDist.asArcseconds()
339 distMean = result.stats.distMean.asArcseconds()
340 distStdDev = result.stats.distStdDev.asArcseconds()
341 self.log.info("Astrometric fit iteration %d: found %d matches with mean separation "
342 "= %0.3f +- %0.3f arcsec; max match distance = %0.3f arcsec.",
343 i, len(result.matches), distMean, distStdDev, maxMatchDistance)
344
345 # If fitter converged, record the scatter in the exposure metadata
346 # even if the fit was deemed a failure according to the value of
347 # the maxMeanDistanceArcsec config.
348 md = exposure.getMetadata()
349 md['SFM_ASTROM_OFFSET_MEAN'] = distMean
350 md['SFM_ASTROM_OFFSET_STD'] = distStdDev
351 md['SFM_ASTROM_OFFSET_MEDIAN'] = result.scatterOnSky.asArcseconds()
352
353 if self.usedKey:
354 for m in result.matches:
355 m.second.set(self.usedKey, True)
356
357 matchMeta = self.refObjLoader.getMetadataBox(
358 bbox=exposure.getBBox(),
359 wcs=exposure.wcs,
360 filterName=band,
361 epoch=epoch,
362 )
363
364 return pipeBase.Struct(
365 refCat=refCat,
366 matches=result.matches,
367 scatterOnSky=result.scatterOnSky,
368 matchMeta=matchMeta,
369 )
370
371 def check(self, exposure, sourceCat, nMatches):
372 """Validate the astrometric fit against the maxMeanDistance threshold.
373
374 If the distMean metric does not satisfy the requirement of being less
375 than the value set in config.maxMeanDistanceArcsec, the WCS on the
376 exposure will be set to None and the coordinate values in the
377 source catalog will be set to NaN to reflect a failed astrometric fit.
378
379 Parameters
380 ----------
381 exposure : `lsst.afw.image.Exposure`
382 The exposure whose astrometric fit is being evaluated.
383 sourceCat : `lsst.afw.table.SourceCatalog`
384 The catalog of sources associated with the exposure.
385 nMatches : `int`
386 The number of matches that were found and used during
387 the astrometric fit (for logging purposes only).
388
389 Raises
390 ------
391 BadAstrometryFit
392 If the measured mean on-sky distance between the matched source and
393 reference objects is greater than
394 ``self.config.maxMeanDistanceArcsec``.
395 """
396 # Poor quality fits are a failure.
397 md = exposure.getMetadata()
398 distMean = md['SFM_ASTROM_OFFSET_MEAN']
399 distMedian = md['SFM_ASTROM_OFFSET_MEDIAN']
400 if distMean > self.config.maxMeanDistanceArcsec:
401 exception = exceptions.BadAstrometryFit(nMatches=nMatches, distMean=distMean,
402 maxMeanDist=self.config.maxMeanDistanceArcsec,
403 distMedian=distMedian)
404 exposure.setWcs(None)
405 sourceCat["coord_ra"] = np.nan
406 sourceCat["coord_dec"] = np.nan
407 self.log.error(exception)
408 raise exception
409 return
410
411 @timeMethod
412 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance,
413 exposure=None):
414 """Match sources to reference objects and fit a WCS.
415
416 Parameters
417 ----------
418 refCat : `lsst.afw.table.SimpleCatalog`
419 catalog of reference objects
420 sourceCat : `lsst.afw.table.SourceCatalog`
421 catalog of sources detected on the exposure
422 goodSourceCat : `lsst.afw.table.SourceCatalog`
423 catalog of down-selected good sources detected on the exposure
424 refFluxField : 'str'
425 field of refCat to use for flux
426 bbox : `lsst.geom.Box2I`
427 bounding box of exposure
428 wcs : `lsst.afw.geom.SkyWcs`
429 initial guess for WCS of exposure
430 matchTolerance : `lsst.meas.astrom.MatchTolerance`
431 a MatchTolerance object (or None) specifying
432 internal tolerances to the matcher. See the MatchTolerance
433 definition in the respective matcher for the class definition.
434 exposure : `lsst.afw.image.Exposure`, optional
435 exposure whose WCS is to be fit, or None; used only for the debug
436 display.
437
438 Returns
439 -------
440 result : `lsst.pipe.base.Struct`
441 Result struct with components:
442
443 - ``matches``: astrometric matches
444 (`list` of `lsst.afw.table.ReferenceMatch`).
445 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
446 - ``scatterOnSky`` : median on-sky separation between reference
447 objects and sources in "matches" (`lsst.afw.geom.Angle`).
448 """
449 import lsstDebug
450 debug = lsstDebug.Info(__name__)
451
452 sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
453
454 matchRes = self.matcher.matchObjectsToSources(
455 refCat=refCat,
456 sourceCat=goodSourceCat,
457 wcs=wcs,
458 sourceFluxField=sourceFluxField,
459 refFluxField=refFluxField,
460 matchTolerance=matchTolerance,
461 bbox=bbox,
462 )
463 self.log.debug("Found %s matches", len(matchRes.matches))
464 if debug.display:
465 frame = int(debug.frame)
467 refCat=refCat,
468 sourceCat=matchRes.usableSourceCat,
469 matches=matchRes.matches,
470 exposure=exposure,
471 bbox=bbox,
472 frame=frame + 1,
473 title="Initial WCS",
474 )
475
476 if self.config.doMagnitudeOutlierRejection:
477 matches = self._removeMagnitudeOutliers(sourceFluxField, refFluxField, matchRes.matches)
478 else:
479 matches = matchRes.matches
480
481 self.log.debug("Fitting WCS")
482 fitRes = self.wcsFitter.fitWcs(
483 matches=matches,
484 initWcs=wcs,
485 bbox=bbox,
486 refCat=refCat,
487 sourceCat=sourceCat,
488 exposure=exposure,
489 )
490 fitWcs = fitRes.wcs
491 scatterOnSky = fitRes.scatterOnSky
492 if debug.display:
493 frame = int(debug.frame)
495 refCat=refCat,
496 sourceCat=matchRes.usableSourceCat,
497 matches=matches,
498 exposure=exposure,
499 bbox=bbox,
500 frame=frame + 2,
501 title=f"Fitter: {self.wcsFitter._DefaultName}",
502 )
503
504 return pipeBase.Struct(
505 matches=matches,
506 wcs=fitWcs,
507 scatterOnSky=scatterOnSky,
508 matchTolerance=matchRes.matchTolerance,
509 )
510
511 def _removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn):
512 """Remove magnitude outliers, computing a simple zeropoint.
513
514 Parameters
515 ----------
516 sourceFluxField : `str`
517 Field in source catalog for instrumental fluxes.
518 refFluxField : `str`
519 Field in reference catalog for fluxes (nJy).
520 matchesIn : `list` [`lsst.afw.table.ReferenceMatch`]
521 List of source/reference matches input
522
523 Returns
524 -------
525 matchesOut : `list` [`lsst.afw.table.ReferenceMatch`]
526 List of source/reference matches with magnitude
527 outliers removed.
528 """
529 nMatch = len(matchesIn)
530 sourceMag = np.zeros(nMatch)
531 refMag = np.zeros(nMatch)
532 for i, match in enumerate(matchesIn):
533 sourceMag[i] = -2.5*np.log10(match[1][sourceFluxField])
534 refMag[i] = (match[0][refFluxField]*units.nJy).to_value(units.ABmag)
535
536 deltaMag = refMag - sourceMag
537 # Protect against negative fluxes and nans in the reference catalog.
538 goodDelta, = np.where(np.isfinite(deltaMag))
539 zp = np.median(deltaMag[goodDelta])
540 # Use median absolute deviation (MAD) for zpSigma.
541 # Also require a minimum scatter to prevent floating-point errors from
542 # rejecting objects in zero-noise tests.
543 zpSigma = np.clip(scipy.stats.median_abs_deviation(deltaMag[goodDelta], scale='normal'),
544 1e-3,
545 None)
546
547 self.log.info("Rough zeropoint from astrometry matches is %.4f +/- %.4f.",
548 zp, zpSigma)
549
550 goodStars = goodDelta[(np.abs(deltaMag[goodDelta] - zp)
551 <= self.config.magnitudeOutlierRejectionNSigma*zpSigma)]
552
553 nOutlier = nMatch - goodStars.size
554 self.log.info("Removed %d magnitude outliers out of %d total astrometry matches.",
555 nOutlier, nMatch)
556
557 matchesOut = [matchesIn[idx] for idx in goodStars]
558
559 return matchesOut
560
561 def _compute_ref_src_filter_diff(self, band, refFluxField, refCat):
562 """Compute the median ref flux - source filter color difference.
563
564 The median difference in color between the flux field used for
565 selection and that of the observations being calibrated is computed
566 from the values for each in the reference catalog.
567
568 Parameters
569 ----------
570 band : `str`
571 Bandpass of observed data.
572 refFluxField : `str`
573 Name of the flux field used in the reference catalog.
574 refCat : `lsst.afw.table.SimpleCatalog`
575 Catalog of reference objects from which to compute the color
576 offset.
577
578 Returns
579 -------
580 refColorDelta : `float`
581 The median color difference.
582 """
583 if band in self.config.filterMap:
584 refFilterNameForBand = self.config.filterMap.get(band) + "_flux"
585 refCatTemp = refCat[(np.isfinite(refCat[refFluxField])
586 & np.isfinite(refCat[refFilterNameForBand]))].copy(deep=True)
587 if len(refCatTemp) < 3:
588 refColorDeltaDefaults = self.config.refColorDeltaDefaults
589 if band in refColorDeltaDefaults:
590 refColorDelta = refColorDeltaDefaults[band]
591 self.log.warning("Not enough reference sources with finite fluxes in %s and %s, "
592 "so can't compute color shift; a default vaulue of %.2f will "
593 "be applied.", refFluxField, refFilterNameForBand, refColorDelta)
594 else:
595 refMag = (refCatTemp[refFluxField]*units.nJy).to_value(units.ABmag)
596 refMagSrcBand = (refCatTemp[refFilterNameForBand]*units.nJy).to_value(units.ABmag)
597 refColorDelta = np.nanmedian(refMag - refMagSrcBand)
598 self.log.info("Adjusting refCat cutoffs for color shift: %s - %s = %.2f.",
599 refFluxField, refFilterNameForBand, refColorDelta)
600 else:
601 refColorDelta = 0.0
602 self.log.warning("Band %s not found in filterMap: %s. No adjustment for filter "
603 "differences between reference and source catalogs attempted.",
604 band, self.config.filterMap)
605 return refColorDelta
606
607 def _do_fiducial_zeropoint_culling(self, band, refFluxField, refCat, sourceCat, expTime):
608 """Perform a culling of the catalogs to attempt to match their
609 effective magnitude ranges.
610
611 This uses a fiducial zeropoint along with the exposure time for
612 the observations to compute our best-guess magnitudes. Also,
613 accommodation is made for the median difference in color between
614 the flux field used for selection and that of the observations being
615 calibrated, which is computed from the values for each in the reference
616 catalog.
617
618 Parameters
619 ----------
620 band : `str`
621 Bandpass of observed data.
622 refFluxField : `str`
623 Name of the flux field used in the reference catalog.
624 refCat : `lsst.afw.table.SimpleCatalog`
625 Catalog of reference objects to be passed to the matcher. Modified
626 in place.
627 sourceCat : `lsst.afw.table.SourceCatalog`
628 Catalog of observed sources to be passed to the matcher. Modified
629 in place.
630 expTime : `float`
631 Exposure time of the observation being calibrated.
632
633 Returns
634 -------
635 refColorDelta : `float`
636 The median color difference.
637 """
638 nRefCatPreCull = len(refCat)
639 nSelectedSourceCat = len(sourceCat)
640 # Compute rough limiting magnitudes of selected sources
641 psfFlux = sourceCat["base_PsfFlux_instFlux"]
642 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
643 psfMag = -2.5*np.log10(psfFlux) + fiducialZeroPoint + 2.5*np.log10(expTime)
644 sourceMagMin = min(psfMag) - self.config.cullMagBuffer
645 sourceMagMax = max(psfMag) + self.config.cullMagBuffer
646
647 # Try to account for median ref flux - source band color difference.
648 refColorDelta = 0.0
649 refColorDelta = self._compute_ref_src_filter_diff(band, refFluxField, refCat)
650 if refColorDelta > self.config.cullMagBuffer:
651 # Start shifting by just half of the median color difference.
652 sourceMagMin += 0.5*refColorDelta
653 sourceMagMax += 0.5*refColorDelta
654 if refColorDelta > 3.0*self.config.cullMagBuffer:
655 # Shift even further if color difference is large compared
656 # with the cullMagBuffer.
657 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
658 sourceMagMax += 0.5*refColorDelta
659 if refColorDelta < -1.0*self.config.cullMagBuffer:
660 # If the color difference is negative (i.e. sources are fainter
661 # in the observed filter), allow full bright end, and shift to
662 # fainter limit.
663 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
664 sourceMagMax += refColorDelta
665 self.log.debug("Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
666 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
667
668 # Include full bright end of reference catalog if there are very
669 # few sources compared to references loaded (this often occurs when
670 # there is a large amount of exctinction and/or scattering from
671 # from bright stars).
672 if nRefCatPreCull/nSelectedSourceCat > self.config.maxRefToSourceRatio:
673 sourceMagMin = np.nanmin((refCat[refFluxField]*units.nJy).to_value(units.ABmag))
674
675 self.log.info("Source selection: sourceMag (min, max) = (%.3f, %.3f)", sourceMagMin, sourceMagMax)
676 refCat = refCat[np.isfinite(refCat[refFluxField])]
677 refMag = (refCat[refFluxField]*units.nJy).to_value(units.ABmag)
678 refCat = refCat[(refMag < sourceMagMax) & (refMag > sourceMagMin)]
679 refMagMin = np.nanmin(refMag)
680 refMagMax = np.nanmax(refMag)
681 # Now make sure source cat doesn't extend beyond refCat limits.
682 goodSources = ((psfMag < refMagMax - refColorDelta) & (psfMag > refMagMin - refColorDelta))
683 if len(goodSources) < np.sum(goodSources):
684 sourceCat = sourceCat[goodSources].copy(deep=True)
685 nSelectedSourceCat = len(sourceCat)
686 self.log.debug("Number of sources = %d; Number of refs = %d; refs/source = %.2f.",
687 nSelectedSourceCat, nRefCatPreCull, nRefCatPreCull/nSelectedSourceCat)
688
689 self.log.info("Final: Selected %d/%d reference sources based on fiducial zeropoint culling. "
690 "Mag range in %s = (%.2f, %.2f)", len(refCat), nRefCatPreCull,
691 refFluxField, refMagMin, refMagMax)
692 return refCat, sourceCat
_do_fiducial_zeropoint_culling(self, band, refFluxField, refCat, sourceCat, expTime)
_compute_ref_src_filter_diff(self, band, refFluxField, refCat)
check(self, exposure, sourceCat, nMatches)
_matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, matchTolerance, exposure=None)
__init__(self, refObjLoader=None, schema=None, **kwargs)
_removeMagnitudeOutliers(self, sourceFluxField, refFluxField, matchesIn)
loadAndMatch(self, exposure, sourceCat)
Definition ref_match.py:120
displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition display.py:34