224 def run(self, exposure, sources, background):
225 """Measure exposure statistics from the exposure, sources, and
230 exposure : `lsst.afw.image.ExposureF`
231 sources : `lsst.afw.table.SourceCatalog`
232 background : `lsst.afw.math.BackgroundList`
236 summary : `lsst.afw.image.ExposureSummary`
238 self.log.info(
"Measuring exposure statistics")
243 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
244 summary.expTime = exposureTime
246 bbox = exposure.getBBox()
248 psf = exposure.getPsf()
250 summary, psf, bbox, sources, image_mask=exposure.mask, image_ap_corr_map=exposure.apCorrMap
253 wcs = exposure.getWcs()
254 visitInfo = exposure.getInfo().getVisitInfo()
257 photoCalib = exposure.getPhotoCalib()
268 md = exposure.getMetadata()
269 if 'SFM_ASTROM_OFFSET_MEAN' in md:
270 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
271 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
282 image_ap_corr_map=None,
283 sources_is_astropy=False,
285 """Compute all summary-statistic fields that depend on the PSF model.
289 summary : `lsst.afw.image.ExposureSummaryStats`
290 Summary object to update in-place.
291 psf : `lsst.afw.detection.Psf` or `None`
292 Point spread function model. If `None`, all fields that depend on
293 the PSF will be reset (generally to NaN).
294 bbox : `lsst.geom.Box2I`
295 Bounding box of the image for which summary stats are being
297 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
298 Catalog for quantities that are computed from source table columns.
299 If `None`, these quantities will be reset (generally to NaN).
300 The type of this table must correspond to the
301 ``sources_is_astropy`` argument.
302 image_mask : `lsst.afw.image.Mask`, optional
303 Mask image that may be used to compute distance-to-nearest-star
305 sources_is_astropy : `bool`, optional
306 Whether ``sources`` is an `astropy.table.Table` instance instead
307 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
311 summary.psfSigma = nan
315 summary.psfArea = nan
317 summary.psfStarDeltaE1Median = nan
318 summary.psfStarDeltaE2Median = nan
319 summary.psfStarDeltaE1Scatter = nan
320 summary.psfStarDeltaE2Scatter = nan
321 summary.psfStarDeltaSizeMedian = nan
322 summary.psfStarDeltaSizeScatter = nan
323 summary.psfStarScaledDeltaSizeScatter = nan
324 summary.maxDistToNearestPsf = nan
325 summary.psfTraceRadiusDelta = nan
326 summary.psfApFluxDelta = nan
327 summary.psfApCorrSigmaScaledDelta = nan
331 shape = psf.computeShape(bbox.getCenter())
332 summary.psfSigma = shape.getDeterminantRadius()
333 summary.psfIxx = shape.getIxx()
334 summary.psfIyy = shape.getIyy()
335 summary.psfIxy = shape.getIxy()
336 im = psf.computeKernelImage(bbox.getCenter())
339 summary.psfArea = float(np.sum(im.array)**2./np.sum(im.array**2.))
341 if image_mask
is not None:
342 psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
343 self.log.debug(
"Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
347 sampling=self.config.psfGridSampling,
348 ap_radius_pix=psfApRadius,
349 bad_mask_bits=self.config.psfBadMaskPlanes
351 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
352 summary.psfApFluxDelta = float(psfApFluxDelta)
353 if image_ap_corr_map
is not None:
354 if self.config.psfApCorrFieldName
not in image_ap_corr_map.keys():
355 self.log.warning(f
"{self.config.psfApCorrFieldName} not found in "
356 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
357 psfApCorrSigmaScaledDelta = nan
359 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
364 sampling=self.config.psfGridSampling,
365 bad_mask_bits=self.config.psfBadMaskPlanes,
367 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
376 nPsfStar = sources[self.config.starSelection].sum()
377 summary.nPsfStar = int(nPsfStar)
379 psf_mask = self.starSelector.run(sources).selected
380 nPsfStarsUsedInStats = psf_mask.sum()
382 if nPsfStarsUsedInStats == 0:
387 if sources_is_astropy:
388 psf_cat = sources[psf_mask]
390 psf_cat = sources[psf_mask].copy(deep=
True)
392 starXX = psf_cat[self.config.starShape +
'_xx']
393 starYY = psf_cat[self.config.starShape +
'_yy']
394 starXY = psf_cat[self.config.starShape +
'_xy']
395 psfXX = psf_cat[self.config.psfShape +
'_xx']
396 psfYY = psf_cat[self.config.psfShape +
'_yy']
397 psfXY = psf_cat[self.config.psfShape +
'_xy']
400 starSize = np.sqrt(starXX/2. + starYY/2.)
402 starE1 = (starXX - starYY)/(starXX + starYY)
403 starE2 = 2*starXY/(starXX + starYY)
404 starSizeMedian = np.median(starSize)
407 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
408 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
409 psfE2 = 2*psfXY/(psfXX + psfYY)
411 psfStarDeltaE1Median = np.median(starE1 - psfE1)
412 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
413 psfStarDeltaE2Median = np.median(starE2 - psfE2)
414 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
416 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
417 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
418 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
420 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
421 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
422 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
423 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
424 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
425 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
426 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
428 if image_mask
is not None:
432 sampling=self.config.psfSampling,
433 bad_mask_bits=self.config.psfBadMaskPlanes
435 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
438 """Compute all summary-statistic fields that depend on the WCS model.
442 summary : `lsst.afw.image.ExposureSummaryStats`
443 Summary object to update in-place.
444 wcs : `lsst.afw.geom.SkyWcs` or `None`
445 Astrometric calibration model. If `None`, all fields that depend
446 on the WCS will be reset (generally to NaN).
447 bbox : `lsst.geom.Box2I`
448 Bounding box of the image for which summary stats are being
450 visitInfo : `lsst.afw.image.VisitInfo`
451 Observation information used in together with ``wcs`` to compute
455 summary.raCorners = [nan]*4
456 summary.decCorners = [nan]*4
459 summary.pixelScale = nan
460 summary.zenithDistance = nan
465 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
466 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
467 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
469 sph_pt = wcs.pixelToSky(bbox.getCenter())
470 summary.ra = sph_pt.getRa().asDegrees()
471 summary.dec = sph_pt.getDec().asDegrees()
472 summary.pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
474 date = visitInfo.getDate()
481 observatory = visitInfo.getObservatory()
482 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
483 lon=observatory.getLongitude().asDegrees()*units.deg,
484 height=observatory.getElevation()*units.m)
485 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
486 location=loc, format=
'mjd')
488 summary.ra*units.degree,
489 summary.dec*units.degree,
493 with warnings.catch_warnings():
494 warnings.simplefilter(
'ignore')
495 altaz = coord.transform_to(AltAz)
497 summary.zenithDistance = float(90.0 - altaz.alt.degree)
543 """Compute summary-statistic fields that depend on the masked image
548 summary : `lsst.afw.image.ExposureSummaryStats`
549 Summary object to update in-place.
550 masked_image : `lsst.afw.image.MaskedImage` or `None`
551 Masked image. If `None`, all fields that depend
552 on the masked image will be reset (generally to NaN).
555 if masked_image
is None:
556 summary.skyNoise = nan
557 summary.meanVar = nan
560 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
561 statsCtrl.setNumIter(self.config.clipIter)
562 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
563 statsCtrl.setNanSafe(
True)
566 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
567 summary.skyNoise = skyNoise
571 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
572 summary.meanVar = meanVar
575 """Compute effective exposure time statistics to estimate depth.
577 The effective exposure time is the equivalent shutter open
578 time that would be needed under nominal conditions to give the
579 same signal-to-noise for a point source as what is achieved by
580 the observation of interest. This metric combines measurements
581 of the point-spread function, the sky brightness, and the
582 transparency. It assumes that the observation is
583 sky-background dominated.
585 .. _teff_definitions:
587 The effective exposure time and its subcomponents are defined in [1]_.
592 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
593 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
594 https://www.osti.gov/biblio/1250877/
598 summary : `lsst.afw.image.ExposureSummaryStats`
599 Summary object to update in-place.
600 exposure : `lsst.afw.image.ExposureF`
601 Exposure to grab band and exposure time metadata
605 summary.effTime = nan
606 summary.effTimePsfSigmaScale = nan
607 summary.effTimeSkyBgScale = nan
608 summary.effTimeZeroPointScale = nan
610 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
611 filterLabel = exposure.getFilter()
612 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
615 band = filterLabel.bandLabel
618 self.log.warning(
"No band associated with exposure; effTime not calculated.")
622 if np.isnan(summary.psfSigma):
623 self.log.debug(
"PSF sigma is NaN")
625 elif band
not in self.config.fiducialPsfSigma:
626 self.log.debug(f
"Fiducial PSF value not found for {band}")
629 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
630 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
634 if np.isnan(summary.zeroPoint):
635 self.log.debug(
"Zero point is NaN")
637 elif band
not in self.config.fiducialZeroPoint:
638 self.log.debug(f
"Fiducial zero point value not found for {band}")
641 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
642 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
643 c_eff = min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
646 if np.isnan(summary.skyBg):
647 self.log.debug(
"Sky background is NaN")
649 elif band
not in self.config.fiducialSkyBackground:
650 self.log.debug(f
"Fiducial sky background value not found for {band}")
653 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
654 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
657 t_eff = f_eff * c_eff * b_eff
660 effectiveTime = t_eff * exposureTime
663 summary.effTime = float(effectiveTime)
664 summary.effTimePsfSigmaScale = float(f_eff)
665 summary.effTimeSkyBgScale = float(b_eff)
666 summary.effTimeZeroPointScale = float(c_eff)
669 """Compute a summary statistic for the point-source magnitude limit at
670 a given signal-to-noise ratio using exposure-level metadata.
672 The magnitude limit is calculated at a given SNR from the PSF,
673 sky, zeropoint, and readnoise in accordance with SMTN-002 [1]_,
674 LSE-40 [2]_, and DMTN-296 [3]_.
679 .. [1] "Calculating LSST limiting magnitudes and SNR" (SMTN-002)
680 .. [2] "Photon Rates and SNR Calculations" (LSE-40)
681 .. [3] "Calculations of Image and Catalog Depth" (DMTN-296)
686 summary : `lsst.afw.image.ExposureSummaryStats`
687 Summary object to update in-place.
688 exposure : `lsst.afw.image.ExposureF`
689 Exposure to grab band and exposure time metadata
691 if exposure.getDetector()
is None:
692 summary.magLim = float(
"nan")
696 readNoiseList = list(ipIsr.getExposureReadNoises(exposure).values())
697 readNoise = np.nanmean(readNoiseList)
698 if np.isnan(readNoise):
700 self.log.warning(
"Read noise set to NaN! Setting readNoise to 0.0 to proceed.")
703 gainList = list(ipIsr.getExposureGains(exposure).values())
704 gain = np.nanmean(gainList)
706 self.log.warning(
"Gain set to NaN! Setting magLim to NaN.")
707 summary.magLim = float(
"nan")
711 md = exposure.getMetadata()
712 if md.get(
"LSST ISR UNITS",
"adu") ==
"electron":
722 summary.zeroPoint, readNoise,
723 gain, self.config.magLimSnr)
725 summary.magLim = float(magLim)