LSST Applications g034a557a3c+dd8dd8f11d,g0afe43252f+b86e4b8053,g11f7dcd041+017865fdd3,g1cd03abf6b+8446defddb,g1ce3e0751c+f991eae79d,g28da252d5a+ca8a1a9fb3,g2bbee38e9b+b6588ad223,g2bc492864f+b6588ad223,g2cdde0e794+8523d0dbb4,g347aa1857d+b6588ad223,g35bb328faa+b86e4b8053,g3a166c0a6a+b6588ad223,g461a3dce89+b86e4b8053,g52b1c1532d+b86e4b8053,g7f3b0d46df+ad13c1b82d,g80478fca09+f29c5d6c70,g858d7b2824+293f439f82,g8cd86fa7b1+af721d2595,g965a9036f2+293f439f82,g979bb04a14+51ed57f74c,g9ddcbc5298+f24b38b85a,gae0086650b+b86e4b8053,gbb886bcc26+b97e247655,gc28159a63d+b6588ad223,gc30aee3386+a2f0f6cab9,gcaf7e4fdec+293f439f82,gcd45df26be+293f439f82,gcdd4ae20e8+70b5def7e6,gce08ada175+da9c58a417,gcf0d15dbbd+70b5def7e6,gdaeeff99f8+006e14e809,gdbce86181e+6a170ce272,ge3d4d395c2+224150c836,ge5f7162a3a+bb2241c923,ge6cb8fbbf7+d119aed356,ge79ae78c31+b6588ad223,gf048a9a2f4+40ffced2b8,gf0baf85859+b4cca3d10f,w.2024.30
LSST Data Management Base Package
Loading...
Searching...
No Matches
computeExposureSummaryStats.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
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__ = ["ComputeExposureSummaryStatsTask", "ComputeExposureSummaryStatsConfig"]
23
24import warnings
25import numpy as np
26from scipy.stats import median_abs_deviation as sigmaMad
27import astropy.units as units
28from astropy.time import Time
29from astropy.coordinates import AltAz, SkyCoord, EarthLocation
30from lsst.daf.base import DateTime
31
32import lsst.pipe.base as pipeBase
33import lsst.pex.config as pexConfig
34import lsst.afw.math as afwMath
35import lsst.afw.image as afwImage
36import lsst.geom as geom
37from lsst.meas.algorithms import ScienceSourceSelectorTask
38from lsst.utils.timer import timeMethod
39
40
41class ComputeExposureSummaryStatsConfig(pexConfig.Config):
42 """Config for ComputeExposureSummaryTask"""
43 sigmaClip = pexConfig.Field(
44 dtype=float,
45 doc="Sigma for outlier rejection for sky noise.",
46 default=3.0,
47 )
48 clipIter = pexConfig.Field(
49 dtype=int,
50 doc="Number of iterations of outlier rejection for sky noise.",
51 default=2,
52 )
53 badMaskPlanes = pexConfig.ListField(
54 dtype=str,
55 doc="Mask planes that, if set, the associated pixel should not be included sky noise calculation.",
56 default=("NO_DATA", "SUSPECT"),
57 )
58 starSelection = pexConfig.Field(
59 doc="Field to select full list of sources used for PSF modeling.",
60 dtype=str,
61 default="calib_psf_used",
62 )
63 starSelector = pexConfig.ConfigurableField(
64 target=ScienceSourceSelectorTask,
65 doc="Selection of sources to compute PSF star statistics.",
66 )
67 starShape = pexConfig.Field(
68 doc="Base name of columns to use for the source shape in the PSF statistics computation.",
69 dtype=str,
70 default="slot_Shape"
71 )
72 psfShape = pexConfig.Field(
73 doc="Base name of columns to use for the PSF shape in the PSF statistics computation.",
74 dtype=str,
75 default="slot_PsfShape"
76 )
77 psfSampling = pexConfig.Field(
78 dtype=int,
79 doc="Sampling rate in pixels in each dimension for the maxDistToNearestPsf metric "
80 "caclulation grid (the tradeoff is between adequate sampling versus speed).",
81 default=8,
82 )
83 psfGridSampling = pexConfig.Field(
84 dtype=int,
85 doc="Sampling rate in pixels in each dimension for PSF model robustness metric "
86 "caclulations grid (the tradeoff is between adequate sampling versus speed).",
87 default=96,
88 )
89 minPsfApRadiusPix = pexConfig.Field(
90 dtype=float,
91 doc="Minimum radius in pixels of the aperture within which to measure the flux of "
92 "the PSF model for the psfApFluxDelta metric calculation (the radius is computed as "
93 "max(``minPsfApRadius``, 3*psfSigma)).",
94 default=2.0,
95 )
96 psfApCorrFieldName = pexConfig.Field(
97 doc="Name of the flux column associated with the aperture correction of the PSF model "
98 "to use for the psfApCorrSigmaScaledDelta metric calculation.",
99 dtype=str,
100 default="base_PsfFlux_instFlux"
101 )
102 psfBadMaskPlanes = pexConfig.ListField(
103 dtype=str,
104 doc="Mask planes that, if set, the associated pixel should not be included in the PSF model "
105 "robutsness metric calculations (namely, maxDistToNearestPsf and psfTraceRadiusDelta).",
106 default=("BAD", "CR", "EDGE", "INTRP", "NO_DATA", "SAT", "SUSPECT"),
107 )
108 fiducialSkyBackground = pexConfig.DictField(
109 keytype=str,
110 itemtype=float,
111 doc="Fiducial sky background level (ADU/s) assumed when calculating effective exposure time. "
112 "Keyed by band.",
113 default={'u': 1.0, 'g': 1.0, 'r': 1.0, 'i': 1.0, 'z': 1.0, 'y': 1.0},
114 )
115 fiducialPsfSigma = pexConfig.DictField(
116 keytype=str,
117 itemtype=float,
118 doc="Fiducial PSF sigma (pixels) assumed when calculating effective exposure time. "
119 "Keyed by band.",
120 default={'u': 1.0, 'g': 1.0, 'r': 1.0, 'i': 1.0, 'z': 1.0, 'y': 1.0},
121 )
122 fiducialZeroPoint = pexConfig.DictField(
123 keytype=str,
124 itemtype=float,
125 doc="Fiducial zero point assumed when calculating effective exposure time. "
126 "Keyed by band.",
127 default={'u': 25.0, 'g': 25.0, 'r': 25.0, 'i': 25.0, 'z': 25.0, 'y': 25.0},
128 )
129 maxEffectiveTransparency = pexConfig.Field(
130 dtype=float,
131 doc="Maximum value allowed for effective transparency scale factor (often inf or 1.0).",
132 default=float('inf')
133 )
134
135 def setDefaults(self):
136 super().setDefaults()
137
139 self.starSelector.doFlags = True
140 self.starSelector.doSignalToNoise = True
141 self.starSelector.doUnresolved = False
142 self.starSelector.doIsolated = False
143 self.starSelector.doRequireFiniteRaDec = False
144 self.starSelector.doRequirePrimary = False
145
146 self.starSelector.signalToNoise.minimum = 50.0
147 self.starSelector.signalToNoise.maximum = 1000.0
148
149 self.starSelector.flags.bad = ["slot_Shape_flag", "slot_PsfFlux_flag"]
150 # Select stars used for PSF modeling.
151 self.starSelector.flags.good = ["calib_psf_used"]
152
153 self.starSelector.signalToNoise.fluxField = "slot_PsfFlux_instFlux"
154 self.starSelector.signalToNoise.errField = "slot_PsfFlux_instFluxErr"
155
156
158 """Task to compute exposure summary statistics.
159
160 This task computes various quantities suitable for DPDD and other
161 downstream processing at the detector centers, including:
162 - expTime
163 - psfSigma
164 - psfArea
165 - psfIxx
166 - psfIyy
167 - psfIxy
168 - ra
169 - dec
170 - pixelScale (arcsec/pixel)
171 - zenithDistance
172 - zeroPoint
173 - skyBg
174 - skyNoise
175 - meanVar
176 - raCorners
177 - decCorners
178 - astromOffsetMean
179 - astromOffsetStd
180
181 These additional quantities are computed from the stars in the detector:
182 - psfStarDeltaE1Median
183 - psfStarDeltaE2Median
184 - psfStarDeltaE1Scatter
185 - psfStarDeltaE2Scatter
186 - psfStarDeltaSizeMedian
187 - psfStarDeltaSizeScatter
188 - psfStarScaledDeltaSizeScatter
189
190 These quantities are computed based on the PSF model and image mask
191 to assess the robustness of the PSF model across a given detector
192 (against, e.g., extrapolation instability):
193 - maxDistToNearestPsf
194 - psfTraceRadiusDelta
195 - psfApFluxDelta
196
197 This quantity is computed based on the aperture correction map, the
198 psfSigma, and the image mask to assess the robustness of the aperture
199 corrections across a given detector:
200 - psfApCorrSigmaScaledDelta
201
202 These quantities are computed to assess depth:
203 - effTime
204 - effTimePsfSigmaScale
205 - effTimeSkyBgScale
206 - effTimeZeroPointScale
207 """
208 ConfigClass = ComputeExposureSummaryStatsConfig
209 _DefaultName = "computeExposureSummaryStats"
210
211 def __init__(self, **kwargs):
212 super().__init__(**kwargs)
213
214 self.makeSubtask("starSelector")
215
216 @timeMethod
217 def run(self, exposure, sources, background):
218 """Measure exposure statistics from the exposure, sources, and
219 background.
220
221 Parameters
222 ----------
223 exposure : `lsst.afw.image.ExposureF`
224 sources : `lsst.afw.table.SourceCatalog`
225 background : `lsst.afw.math.BackgroundList`
226
227 Returns
228 -------
229 summary : `lsst.afw.image.ExposureSummary`
230 """
231 self.log.info("Measuring exposure statistics")
232
234
235 # Set exposure time.
236 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
237 summary.expTime = exposureTime
238
239 bbox = exposure.getBBox()
240
241 psf = exposure.getPsf()
242 self.update_psf_stats(
243 summary, psf, bbox, sources, image_mask=exposure.mask, image_ap_corr_map=exposure.apCorrMap
244 )
245
246 wcs = exposure.getWcs()
247 visitInfo = exposure.getInfo().getVisitInfo()
248 self.update_wcs_stats(summary, wcs, bbox, visitInfo)
249
250 photoCalib = exposure.getPhotoCalib()
251 self.update_photo_calib_stats(summary, photoCalib)
252
253 self.update_background_stats(summary, background)
254
255 self.update_masked_image_stats(summary, exposure.getMaskedImage())
256
257 self.update_effective_time_stats(summary, exposure)
258
259 md = exposure.getMetadata()
260 if 'SFM_ASTROM_OFFSET_MEAN' in md:
261 summary.astromOffsetMean = md['SFM_ASTROM_OFFSET_MEAN']
262 summary.astromOffsetStd = md['SFM_ASTROM_OFFSET_STD']
263
264 return summary
265
267 self,
268 summary,
269 psf,
270 bbox,
271 sources=None,
272 image_mask=None,
273 image_ap_corr_map=None,
274 sources_is_astropy=False,
275 ):
276 """Compute all summary-statistic fields that depend on the PSF model.
277
278 Parameters
279 ----------
280 summary : `lsst.afw.image.ExposureSummaryStats`
281 Summary object to update in-place.
282 psf : `lsst.afw.detection.Psf` or `None`
283 Point spread function model. If `None`, all fields that depend on
284 the PSF will be reset (generally to NaN).
285 bbox : `lsst.geom.Box2I`
286 Bounding box of the image for which summary stats are being
287 computed.
288 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
289 Catalog for quantities that are computed from source table columns.
290 If `None`, these quantities will be reset (generally to NaN).
291 The type of this table must correspond to the
292 ``sources_is_astropy`` argument.
293 image_mask : `lsst.afw.image.Mask`, optional
294 Mask image that may be used to compute distance-to-nearest-star
295 metrics.
296 sources_is_astropy : `bool`, optional
297 Whether ``sources`` is an `astropy.table.Table` instance instead
298 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
299 latter).
300 """
301 nan = float("nan")
302 summary.psfSigma = nan
303 summary.psfIxx = nan
304 summary.psfIyy = nan
305 summary.psfIxy = nan
306 summary.psfArea = nan
307 summary.nPsfStar = 0
308 summary.psfStarDeltaE1Median = nan
309 summary.psfStarDeltaE2Median = nan
310 summary.psfStarDeltaE1Scatter = nan
311 summary.psfStarDeltaE2Scatter = nan
312 summary.psfStarDeltaSizeMedian = nan
313 summary.psfStarDeltaSizeScatter = nan
314 summary.psfStarScaledDeltaSizeScatter = nan
315 summary.maxDistToNearestPsf = nan
316 summary.psfTraceRadiusDelta = nan
317 summary.psfApFluxDelta = nan
318 summary.psfApCorrSigmaScaledDelta = nan
319
320 if psf is None:
321 return
322 shape = psf.computeShape(bbox.getCenter())
323 summary.psfSigma = shape.getDeterminantRadius()
324 summary.psfIxx = shape.getIxx()
325 summary.psfIyy = shape.getIyy()
326 summary.psfIxy = shape.getIxy()
327 im = psf.computeKernelImage(bbox.getCenter())
328 # The calculation of effective psf area is taken from
329 # meas_base/src/PsfFlux.cc#L112. See
330 # https://github.com/lsst/meas_base/blob/
331 # 750bffe6620e565bda731add1509507f5c40c8bb/src/PsfFlux.cc#L112
332 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.))
333
334 if image_mask is not None:
335 psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
336 self.log.debug("Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
337 psfTraceRadiusDelta, psfApFluxDelta = compute_psf_image_deltas(
338 image_mask,
339 psf,
340 sampling=self.config.psfGridSampling,
341 ap_radius_pix=psfApRadius,
342 bad_mask_bits=self.config.psfBadMaskPlanes
343 )
344 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
345 summary.psfApFluxDelta = float(psfApFluxDelta)
346 if image_ap_corr_map is not None:
347 if self.config.psfApCorrFieldName not in image_ap_corr_map.keys():
348 self.log.warn(f"{self.config.psfApCorrFieldName} not found in "
349 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
350 psfApCorrSigmaScaledDelta = nan
351 else:
352 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
353 psfApCorrSigmaScaledDelta = compute_ap_corr_sigma_scaled_delta(
354 image_mask,
355 image_ap_corr_field,
356 summary.psfSigma,
357 sampling=self.config.psfGridSampling,
358 bad_mask_bits=self.config.psfBadMaskPlanes,
359 )
360 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
361
362 if sources is None:
363 # No sources are available (as in some tests and rare cases where
364 # the selection criteria in finalizeCharacterization lead to no
365 # good sources).
366 return
367
368 # Count the total number of psf stars used (prior to stats selection).
369 nPsfStar = sources[self.config.starSelection].sum()
370 summary.nPsfStar = int(nPsfStar)
371
372 psf_mask = self.starSelector.run(sources).selected
373 nPsfStarsUsedInStats = psf_mask.sum()
374
375 if nPsfStarsUsedInStats == 0:
376 # No stars to measure statistics, so we must return the defaults
377 # of 0 stars and NaN values.
378 return
379
380 if sources_is_astropy:
381 psf_cat = sources[psf_mask]
382 else:
383 psf_cat = sources[psf_mask].copy(deep=True)
384
385 starXX = psf_cat[self.config.starShape + '_xx']
386 starYY = psf_cat[self.config.starShape + '_yy']
387 starXY = psf_cat[self.config.starShape + '_xy']
388 psfXX = psf_cat[self.config.psfShape + '_xx']
389 psfYY = psf_cat[self.config.psfShape + '_yy']
390 psfXY = psf_cat[self.config.psfShape + '_xy']
391
392 # Use the trace radius for the star size.
393 starSize = np.sqrt(starXX/2. + starYY/2.)
394
395 starE1 = (starXX - starYY)/(starXX + starYY)
396 starE2 = 2*starXY/(starXX + starYY)
397 starSizeMedian = np.median(starSize)
398
399 # Use the trace radius for the psf size.
400 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
401 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
402 psfE2 = 2*psfXY/(psfXX + psfYY)
403
404 psfStarDeltaE1Median = np.median(starE1 - psfE1)
405 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale='normal')
406 psfStarDeltaE2Median = np.median(starE2 - psfE2)
407 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale='normal')
408
409 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
410 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale='normal')
411 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
412
413 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
414 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
415 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
416 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
417 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
418 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
419 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
420
421 if image_mask is not None:
422 maxDistToNearestPsf = maximum_nearest_psf_distance(
423 image_mask,
424 psf_cat,
425 sampling=self.config.psfSampling,
426 bad_mask_bits=self.config.psfBadMaskPlanes
427 )
428 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
429
430 def update_wcs_stats(self, summary, wcs, bbox, visitInfo):
431 """Compute all summary-statistic fields that depend on the WCS model.
432
433 Parameters
434 ----------
435 summary : `lsst.afw.image.ExposureSummaryStats`
436 Summary object to update in-place.
437 wcs : `lsst.afw.geom.SkyWcs` or `None`
438 Astrometric calibration model. If `None`, all fields that depend
439 on the WCS will be reset (generally to NaN).
440 bbox : `lsst.geom.Box2I`
441 Bounding box of the image for which summary stats are being
442 computed.
443 visitInfo : `lsst.afw.image.VisitInfo`
444 Observation information used in together with ``wcs`` to compute
445 the zenith distance.
446 """
447 nan = float("nan")
448 summary.raCorners = [nan]*4
449 summary.decCorners = [nan]*4
450 summary.ra = nan
451 summary.dec = nan
452 summary.pixelScale = nan
453 summary.zenithDistance = nan
454
455 if wcs is None:
456 return
457
458 sph_pts = wcs.pixelToSky(geom.Box2D(bbox).getCorners())
459 summary.raCorners = [float(sph.getRa().asDegrees()) for sph in sph_pts]
460 summary.decCorners = [float(sph.getDec().asDegrees()) for sph in sph_pts]
461
462 sph_pt = wcs.pixelToSky(bbox.getCenter())
463 summary.ra = sph_pt.getRa().asDegrees()
464 summary.dec = sph_pt.getDec().asDegrees()
465 summary.pixelScale = wcs.getPixelScale().asArcseconds()
466
467 date = visitInfo.getDate()
468
469 if date.isValid():
470 # We compute the zenithDistance at the center of the detector
471 # rather than use the boresight value available via the visitInfo,
472 # because the zenithDistance may vary significantly over a large
473 # field of view.
474 observatory = visitInfo.getObservatory()
475 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
476 lon=observatory.getLongitude().asDegrees()*units.deg,
477 height=observatory.getElevation()*units.m)
478 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
479 location=loc, format='mjd')
480 coord = SkyCoord(
481 summary.ra*units.degree,
482 summary.dec*units.degree,
483 obstime=obstime,
484 location=loc,
485 )
486 with warnings.catch_warnings():
487 warnings.simplefilter('ignore')
488 altaz = coord.transform_to(AltAz)
489
490 summary.zenithDistance = float(90.0 - altaz.alt.degree)
491
492 def update_photo_calib_stats(self, summary, photo_calib):
493 """Compute all summary-statistic fields that depend on the photometric
494 calibration model.
495
496 Parameters
497 ----------
498 summary : `lsst.afw.image.ExposureSummaryStats`
499 Summary object to update in-place.
500 photo_calib : `lsst.afw.image.PhotoCalib` or `None`
501 Photometric calibration model. If `None`, all fields that depend
502 on the photometric calibration will be reset (generally to NaN).
503 """
504 if photo_calib is not None:
505 summary.zeroPoint = float(2.5*np.log10(photo_calib.getInstFluxAtZeroMagnitude()))
506 else:
507 summary.zeroPoint = float("nan")
508
509 def update_background_stats(self, summary, background):
510 """Compute summary-statistic fields that depend only on the
511 background model.
512
513 Parameters
514 ----------
515 summary : `lsst.afw.image.ExposureSummaryStats`
516 Summary object to update in-place.
517 background : `lsst.afw.math.BackgroundList` or `None`
518 Background model. If `None`, all fields that depend on the
519 background will be reset (generally to NaN).
520
521 Notes
522 -----
523 This does not include fields that depend on the background-subtracted
524 masked image; when the background changes, it should generally be
525 applied to the image and `update_masked_image_stats` should be called
526 as well.
527 """
528 if background is not None:
529 bgStats = (bg[0].getStatsImage().getImage().array
530 for bg in background)
531 summary.skyBg = float(sum(np.median(bg[np.isfinite(bg)]) for bg in bgStats))
532 else:
533 summary.skyBg = float("nan")
534
535 def update_masked_image_stats(self, summary, masked_image):
536 """Compute summary-statistic fields that depend on the masked image
537 itself.
538
539 Parameters
540 ----------
541 summary : `lsst.afw.image.ExposureSummaryStats`
542 Summary object to update in-place.
543 masked_image : `lsst.afw.image.MaskedImage` or `None`
544 Masked image. If `None`, all fields that depend
545 on the masked image will be reset (generally to NaN).
546 """
547 nan = float("nan")
548 if masked_image is None:
549 summary.skyNoise = nan
550 summary.meanVar = nan
551 return
552 statsCtrl = afwMath.StatisticsControl()
553 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
554 statsCtrl.setNumIter(self.config.clipIter)
555 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
556 statsCtrl.setNanSafe(True)
557
558 statObj = afwMath.makeStatistics(masked_image, afwMath.STDEVCLIP, statsCtrl)
559 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
560 summary.skyNoise = skyNoise
561
562 statObj = afwMath.makeStatistics(masked_image.variance, masked_image.mask, afwMath.MEANCLIP,
563 statsCtrl)
564 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
565 summary.meanVar = meanVar
566
567 def update_effective_time_stats(self, summary, exposure):
568 """Compute effective exposure time statistics to estimate depth.
569
570 The effective exposure time is the equivalent shutter open
571 time that would be needed under nominal conditions to give the
572 same signal-to-noise for a point source as what is achieved by
573 the observation of interest. This metric combines measurements
574 of the point-spread function, the sky brightness, and the
575 transparency.
576
577 .. _teff_definitions:
578
579 The effective exposure time and its subcomponents are defined in [1]_
580
581 References
582 ----------
583
584 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
585 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
586 https://www.osti.gov/biblio/1250877/
587
588
589 Parameters
590 ----------
591 summary : `lsst.afw.image.ExposureSummaryStats`
592 Summary object to update in-place.
593 exposure : `lsst.afw.image.ExposureF`
594 Exposure to grab band and exposure time metadata
595
596 """
597 self.log.info("Updating effective exposure time")
598
599 nan = float("nan")
600 summary.effTime = nan
601 summary.effTimePsfSigmaScale = nan
602 summary.effTimeSkyBgScale = nan
603 summary.effTimeZeroPointScale = nan
604
605 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
606 filterLabel = exposure.getFilter()
607 if (filterLabel is None) or (not filterLabel.hasBandLabel):
608 band = None
609 else:
610 band = filterLabel.bandLabel
611
612 if band is None:
613 self.log.warn("No band associated with exposure; effTime not calculated.")
614 return
615
616 # PSF component
617 if np.isnan(summary.psfSigma):
618 self.log.debug("PSF sigma is NaN")
619 f_eff = nan
620 elif band not in self.config.fiducialPsfSigma:
621 self.log.debug(f"Fiducial PSF value not found for {band}")
622 f_eff = nan
623 else:
624 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
625 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
626
627 # Transparency component (note that exposure time may be removed from zeropoint)
628 if np.isnan(summary.zeroPoint):
629 self.log.debug("Zero point is NaN")
630 c_eff = nan
631 elif band not in self.config.fiducialZeroPoint:
632 self.log.debug(f"Fiducial zero point value not found for {band}")
633 c_eff = nan
634 else:
635 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
636 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
637 c_eff = min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
638
639 # Sky brightness component (convert to cts/s)
640 if np.isnan(summary.skyBg):
641 self.log.debug("Sky background is NaN")
642 b_eff = nan
643 elif band not in self.config.fiducialSkyBackground:
644 self.log.debug(f"Fiducial sky background value not found for {band}")
645 b_eff = nan
646 else:
647 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
648 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
649
650 # Effective exposure time scale factor
651 t_eff = f_eff * c_eff * b_eff
652
653 # Effective exposure time (seconds)
654 effectiveTime = t_eff * exposureTime
655
656 # Output quantities
657 summary.effTime = float(effectiveTime)
658 summary.effTimePsfSigmaScale = float(f_eff)
659 summary.effTimeSkyBgScale = float(b_eff)
660 summary.effTimeZeroPointScale = float(c_eff)
661
662
664 image_mask,
665 psf_cat,
666 sampling=8,
667 bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"],
668):
669 """Compute the maximum distance of an unmasked pixel to its nearest PSF.
670
671 Parameters
672 ----------
673 image_mask : `lsst.afw.image.Mask`
674 The mask plane associated with the exposure.
675 psf_cat : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
676 Catalog containing only the stars used in the PSF modeling.
677 sampling : `int`
678 Sampling rate in each dimension to create the grid of points on which
679 to evaluate the distance to the nearest PSF star. The tradeoff is
680 between adequate sampling versus speed.
681 bad_mask_bits : `list` [`str`]
682 Mask bits required to be absent for a pixel to be considered
683 "unmasked".
684
685 Returns
686 -------
687 max_dist_to_nearest_psf : `float`
688 The maximum distance (in pixels) of an unmasked pixel to its nearest
689 PSF model star.
690 """
691 mask_arr = image_mask.array[::sampling, ::sampling]
692 bitmask = image_mask.getPlaneBitMask(bad_mask_bits)
693 good = ((mask_arr & bitmask) == 0)
694
695 x = np.arange(good.shape[1]) * sampling
696 y = np.arange(good.shape[0]) * sampling
697 xx, yy = np.meshgrid(x, y)
698
699 dist_to_nearest_psf = np.full(good.shape, np.inf)
700 for psf in psf_cat:
701 x_psf = psf["slot_Centroid_x"]
702 y_psf = psf["slot_Centroid_y"]
703 dist_to_nearest_psf = np.minimum(dist_to_nearest_psf, np.hypot(xx - x_psf, yy - y_psf))
704 unmasked_dists = dist_to_nearest_psf * good
705 max_dist_to_nearest_psf = np.max(unmasked_dists)
706
707 return max_dist_to_nearest_psf
708
709
711 image_mask,
712 image_psf,
713 sampling=96,
714 ap_radius_pix=3.0,
715 bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"],
716):
717 """Compute the delta between the maximum and minimum model PSF trace radius
718 values evaluated on a grid of points lying in the unmasked region of the
719 image.
720
721 Parameters
722 ----------
723 image_mask : `lsst.afw.image.Mask`
724 The mask plane associated with the exposure.
725 image_psf : `lsst.afw.detection.Psf`
726 The PSF model associated with the exposure.
727 sampling : `int`, optional
728 Sampling rate in each dimension to create the grid of points at which
729 to evaluate ``image_psf``s trace radius value. The tradeoff is between
730 adequate sampling versus speed.
731 ap_radius_pix : `float`, optional
732 Radius in pixels of the aperture on which to measure the flux of the
733 PSF model.
734 bad_mask_bits : `list` [`str`], optional
735 Mask bits required to be absent for a pixel to be considered
736 "unmasked".
737
738 Returns
739 -------
740 psf_trace_radius_delta, psf_ap_flux_delta : `float`
741 The delta (in pixels) between the maximum and minimum model PSF trace
742 radius values and the PSF aperture fluxes (with aperture radius of
743 max(2, 3*psfSigma)) evaluated on the x,y-grid subsampled on the
744 unmasked detector pixels by a factor of ``sampling``. If both the
745 model PSF trace radius value and aperture flux value on the grid
746 evaluate to NaN, then NaNs are returned immediately.
747 """
748 psf_trace_radius_list = []
749 psf_ap_flux_list = []
750 mask_arr = image_mask.array[::sampling, ::sampling]
751 bitmask = image_mask.getPlaneBitMask(bad_mask_bits)
752 good = ((mask_arr & bitmask) == 0)
753
754 x = np.arange(good.shape[1]) * sampling
755 y = np.arange(good.shape[0]) * sampling
756 xx, yy = np.meshgrid(x, y)
757
758 for x_mesh, y_mesh, good_mesh in zip(xx, yy, good):
759 for x_point, y_point, is_good in zip(x_mesh, y_mesh, good_mesh):
760 if is_good:
761 position = geom.Point2D(x_point, y_point)
762 psf_trace_radius = image_psf.computeShape(position).getTraceRadius()
763 psf_ap_flux = image_psf.computeApertureFlux(ap_radius_pix, position)
764 if ~np.isfinite(psf_trace_radius) and ~np.isfinite(psf_ap_flux):
765 return float("nan"), float("nan")
766 psf_trace_radius_list.append(psf_trace_radius)
767 psf_ap_flux_list.append(psf_ap_flux)
768
769 psf_trace_radius_delta = np.max(psf_trace_radius_list) - np.min(psf_trace_radius_list)
770 if np.any(np.asarray(psf_ap_flux_list) < 0.0): # Consider any -ve flux entry as "bad".
771 psf_ap_flux_delta = float("nan")
772 else:
773 psf_ap_flux_delta = np.max(psf_ap_flux_list) - np.min(psf_ap_flux_list)
774
775 return psf_trace_radius_delta, psf_ap_flux_delta
776
777
779 image_mask,
780 image_ap_corr_field,
781 psfSigma,
782 sampling=96,
783 bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"],
784):
785 """Compute the delta between the maximum and minimum aperture correction
786 values scaled (divided) by ``psfSigma`` for the given field representation,
787 ``image_ap_corr_field`` evaluated on a grid of points lying in the
788 unmasked region of the image.
789
790 Parameters
791 ----------
792 image_mask : `lsst.afw.image.Mask`
793 The mask plane associated with the exposure.
794 image_ap_corr_field : `lsst.afw.math.ChebyshevBoundedField`
795 The ChebyshevBoundedField representation of the aperture correction
796 of interest for the exposure.
797 psfSigma : `float`
798 The PSF model second-moments determinant radius (center of chip)
799 in pixels.
800 sampling : `int`, optional
801 Sampling rate in each dimension to create the grid of points at which
802 to evaluate ``image_psf``s trace radius value. The tradeoff is between
803 adequate sampling versus speed.
804 bad_mask_bits : `list` [`str`], optional
805 Mask bits required to be absent for a pixel to be considered
806 "unmasked".
807
808 Returns
809 -------
810 ap_corr_sigma_scaled_delta : `float`
811 The delta between the maximum and minimum of the (multiplicative)
812 aperture correction values scaled (divided) by ``psfSigma`` evaluated
813 on the x,y-grid subsampled on the unmasked detector pixels by a factor
814 of ``sampling``. If the aperture correction evaluates to NaN on any
815 of the grid points, this is set to NaN.
816 """
817 mask_arr = image_mask.array[::sampling, ::sampling]
818 bitmask = image_mask.getPlaneBitMask(bad_mask_bits)
819 good = ((mask_arr & bitmask) == 0)
820
821 x = np.arange(good.shape[1], dtype=np.float64) * sampling
822 y = np.arange(good.shape[0], dtype=np.float64) * sampling
823 xx, yy = np.meshgrid(x, y)
824
825 ap_corr = image_ap_corr_field.evaluate(xx.ravel(), yy.ravel()).reshape(xx.shape)
826 ap_corr_good = ap_corr[good]
827 if ~np.isfinite(ap_corr_good).all():
828 ap_corr_sigma_scaled_delta = float("nan")
829 else:
830 ap_corr_sigma_scaled_delta = (np.max(ap_corr_good) - np.min(ap_corr_good))/psfSigma
831
832 return ap_corr_sigma_scaled_delta
int min
int max
Pass parameters to a Statistics object.
Definition Statistics.h:83
A floating-point coordinate rectangle geometry.
Definition Box.h:413
update_psf_stats(self, summary, psf, bbox, sources=None, image_mask=None, image_ap_corr_map=None, sources_is_astropy=False)
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
maximum_nearest_psf_distance(image_mask, psf_cat, sampling=8, bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"])
compute_ap_corr_sigma_scaled_delta(image_mask, image_ap_corr_field, psfSigma, sampling=96, bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"])
compute_psf_image_deltas(image_mask, image_psf, sampling=96, ap_radius_pix=3.0, bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"])