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())
341 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.))
343 if image_mask
is not None:
344 psfApRadius =
max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
345 self.log.debug(
"Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
349 sampling=self.config.psfGridSampling,
350 ap_radius_pix=psfApRadius,
351 bad_mask_bits=self.config.psfBadMaskPlanes
353 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
354 summary.psfApFluxDelta = float(psfApFluxDelta)
355 if image_ap_corr_map
is not None:
356 if self.config.psfApCorrFieldName
not in image_ap_corr_map.keys():
357 self.log.warn(f
"{self.config.psfApCorrFieldName} not found in "
358 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
359 psfApCorrSigmaScaledDelta = nan
361 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
366 sampling=self.config.psfGridSampling,
367 bad_mask_bits=self.config.psfBadMaskPlanes,
369 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
378 nPsfStar = sources[self.config.starSelection].sum()
379 summary.nPsfStar = int(nPsfStar)
381 psf_mask = self.starSelector.run(sources).selected
382 nPsfStarsUsedInStats = psf_mask.sum()
384 if nPsfStarsUsedInStats == 0:
389 if sources_is_astropy:
390 psf_cat = sources[psf_mask]
392 psf_cat = sources[psf_mask].copy(deep=
True)
394 starXX = psf_cat[self.config.starShape +
'_xx']
395 starYY = psf_cat[self.config.starShape +
'_yy']
396 starXY = psf_cat[self.config.starShape +
'_xy']
397 psfXX = psf_cat[self.config.psfShape +
'_xx']
398 psfYY = psf_cat[self.config.psfShape +
'_yy']
399 psfXY = psf_cat[self.config.psfShape +
'_xy']
402 starSize = np.sqrt(starXX/2. + starYY/2.)
404 starE1 = (starXX - starYY)/(starXX + starYY)
405 starE2 = 2*starXY/(starXX + starYY)
406 starSizeMedian = np.median(starSize)
409 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
410 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
411 psfE2 = 2*psfXY/(psfXX + psfYY)
413 psfStarDeltaE1Median = np.median(starE1 - psfE1)
414 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
415 psfStarDeltaE2Median = np.median(starE2 - psfE2)
416 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
418 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
419 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
420 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
422 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
423 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
424 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
425 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
426 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
427 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
428 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
430 if image_mask
is not None:
434 sampling=self.config.psfSampling,
435 bad_mask_bits=self.config.psfBadMaskPlanes
437 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
440 """Compute all summary-statistic fields that depend on the WCS model.
444 summary : `lsst.afw.image.ExposureSummaryStats`
445 Summary object to update in-place.
446 wcs : `lsst.afw.geom.SkyWcs` or `None`
447 Astrometric calibration model. If `None`, all fields that depend
448 on the WCS will be reset (generally to NaN).
449 bbox : `lsst.geom.Box2I`
450 Bounding box of the image for which summary stats are being
452 visitInfo : `lsst.afw.image.VisitInfo`
453 Observation information used in together with ``wcs`` to compute
457 summary.raCorners = [nan]*4
458 summary.decCorners = [nan]*4
461 summary.pixelScale = nan
462 summary.zenithDistance = nan
467 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
468 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
469 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
471 sph_pt = wcs.pixelToSky(bbox.getCenter())
472 summary.ra = sph_pt.getRa().asDegrees()
473 summary.dec = sph_pt.getDec().asDegrees()
474 summary.pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
476 date = visitInfo.getDate()
483 observatory = visitInfo.getObservatory()
484 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
485 lon=observatory.getLongitude().asDegrees()*units.deg,
486 height=observatory.getElevation()*units.m)
487 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
488 location=loc, format=
'mjd')
490 summary.ra*units.degree,
491 summary.dec*units.degree,
495 with warnings.catch_warnings():
496 warnings.simplefilter(
'ignore')
497 altaz = coord.transform_to(AltAz)
499 summary.zenithDistance = float(90.0 - altaz.alt.degree)
545 """Compute summary-statistic fields that depend on the masked image
550 summary : `lsst.afw.image.ExposureSummaryStats`
551 Summary object to update in-place.
552 masked_image : `lsst.afw.image.MaskedImage` or `None`
553 Masked image. If `None`, all fields that depend
554 on the masked image will be reset (generally to NaN).
557 if masked_image
is None:
558 summary.skyNoise = nan
559 summary.meanVar = nan
562 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
563 statsCtrl.setNumIter(self.config.clipIter)
564 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
565 statsCtrl.setNanSafe(
True)
568 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
569 summary.skyNoise = skyNoise
573 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
574 summary.meanVar = meanVar
577 """Compute effective exposure time statistics to estimate depth.
579 The effective exposure time is the equivalent shutter open
580 time that would be needed under nominal conditions to give the
581 same signal-to-noise for a point source as what is achieved by
582 the observation of interest. This metric combines measurements
583 of the point-spread function, the sky brightness, and the
584 transparency. It assumes that the observation is
585 sky-background dominated.
587 .. _teff_definitions:
589 The effective exposure time and its subcomponents are defined in [1]_.
594 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
595 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
596 https://www.osti.gov/biblio/1250877/
600 summary : `lsst.afw.image.ExposureSummaryStats`
601 Summary object to update in-place.
602 exposure : `lsst.afw.image.ExposureF`
603 Exposure to grab band and exposure time metadata
607 summary.effTime = nan
608 summary.effTimePsfSigmaScale = nan
609 summary.effTimeSkyBgScale = nan
610 summary.effTimeZeroPointScale = nan
612 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
613 filterLabel = exposure.getFilter()
614 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
617 band = filterLabel.bandLabel
620 self.log.warn(
"No band associated with exposure; effTime not calculated.")
624 if np.isnan(summary.psfSigma):
625 self.log.debug(
"PSF sigma is NaN")
627 elif band
not in self.config.fiducialPsfSigma:
628 self.log.debug(f
"Fiducial PSF value not found for {band}")
631 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
632 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
636 if np.isnan(summary.zeroPoint):
637 self.log.debug(
"Zero point is NaN")
639 elif band
not in self.config.fiducialZeroPoint:
640 self.log.debug(f
"Fiducial zero point value not found for {band}")
643 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
644 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
645 c_eff =
min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
648 if np.isnan(summary.skyBg):
649 self.log.debug(
"Sky background is NaN")
651 elif band
not in self.config.fiducialSkyBackground:
652 self.log.debug(f
"Fiducial sky background value not found for {band}")
655 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
656 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
659 t_eff = f_eff * c_eff * b_eff
662 effectiveTime = t_eff * exposureTime
665 summary.effTime = float(effectiveTime)
666 summary.effTimePsfSigmaScale = float(f_eff)
667 summary.effTimeSkyBgScale = float(b_eff)
668 summary.effTimeZeroPointScale = float(c_eff)
671 """Compute a summary statistic for the point-source magnitude limit at
672 a given signal-to-noise ratio using exposure-level metadata.
674 The magnitude limit is calculated at a given SNR from the PSF,
675 sky, zeropoint, and readnoise in accordance with SMTN-002 [1]_,
676 LSE-40 [2]_, and DMTN-296 [3]_.
681 .. [1] "Calculating LSST limiting magnitudes and SNR" (SMTN-002)
682 .. [2] "Photon Rates and SNR Calculations" (LSE-40)
683 .. [3] "Calculations of Image and Catalog Depth" (DMTN-296)
688 summary : `lsst.afw.image.ExposureSummaryStats`
689 Summary object to update in-place.
690 exposure : `lsst.afw.image.ExposureF`
691 Exposure to grab band and exposure time metadata
693 if exposure.getDetector()
is None:
694 summary.magLim = float(
"nan")
698 readNoiseList = list(ipIsr.getExposureReadNoises(exposure).values())
699 readNoise = np.nanmean(readNoiseList)
700 if np.isnan(readNoise):
702 self.log.warn(
"Read noise set to NaN! Setting readNoise to 0.0 to proceed.")
705 gainList = list(ipIsr.getExposureGains(exposure).values())
706 gain = np.nanmean(gainList)
708 self.log.warn(
"Gain set to NaN! Setting magLim to NaN.")
709 summary.magLim = float(
"nan")
713 md = exposure.getMetadata()
714 if md.get(
"LSST ISR UNIT",
"adu") ==
"electron":
724 summary.zeroPoint, readNoise,
725 gain, self.config.magLimSnr)
727 summary.magLim = float(magLim)