230 def run(self, exposure, sources, background):
231 """Measure exposure statistics from the exposure, sources, and
236 exposure : `lsst.afw.image.ExposureF`
237 sources : `lsst.afw.table.SourceCatalog`
238 background : `lsst.afw.math.BackgroundList`
242 summary : `lsst.afw.image.ExposureSummary`
244 self.log.info(
"Measuring exposure statistics")
246 summary = afwImage.ExposureSummaryStats()
249 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
250 summary.expTime = exposureTime
252 bbox = exposure.getBBox()
254 psf = exposure.getPsf()
256 summary, psf, bbox, sources, image_mask=exposure.mask, image_ap_corr_map=exposure.apCorrMap
259 wcs = exposure.getWcs()
260 visitInfo = exposure.getInfo().getVisitInfo()
263 photoCalib = exposure.getPhotoCalib()
274 md = exposure.getMetadata()
275 if 'SFM_ASTROM_OFFSET_MEAN' in md:
276 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
277 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
288 image_ap_corr_map=None,
289 sources_is_astropy=False,
291 """Compute all summary-statistic fields that depend on the PSF model.
295 summary : `lsst.afw.image.ExposureSummaryStats`
296 Summary object to update in-place.
297 psf : `lsst.afw.detection.Psf` or `None`
298 Point spread function model. If `None`, all fields that depend on
299 the PSF will be reset (generally to NaN).
300 bbox : `lsst.geom.Box2I`
301 Bounding box of the image for which summary stats are being
303 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
304 Catalog for quantities that are computed from source table columns.
305 If `None`, these quantities will be reset (generally to NaN).
306 The type of this table must correspond to the
307 ``sources_is_astropy`` argument.
308 image_mask : `lsst.afw.image.Mask`, optional
309 Mask image that may be used to compute distance-to-nearest-star
311 sources_is_astropy : `bool`, optional
312 Whether ``sources`` is an `astropy.table.Table` instance instead
313 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
317 summary.psfSigma = nan
321 summary.psfArea = nan
323 summary.psfStarDeltaE1Median = nan
324 summary.psfStarDeltaE2Median = nan
325 summary.psfStarDeltaE1Scatter = nan
326 summary.psfStarDeltaE2Scatter = nan
327 summary.psfStarDeltaSizeMedian = nan
328 summary.psfStarDeltaSizeScatter = nan
329 summary.psfStarScaledDeltaSizeScatter = nan
330 summary.maxDistToNearestPsf = nan
331 summary.psfTraceRadiusDelta = nan
332 summary.psfApFluxDelta = nan
333 summary.psfApCorrSigmaScaledDelta = nan
334 summary.starEMedian = nan
335 summary.starUnNormalizedEMedian = nan
339 shape = psf.computeShape(bbox.getCenter())
340 summary.psfSigma = shape.getDeterminantRadius()
341 summary.psfIxx = shape.getIxx()
342 summary.psfIyy = shape.getIyy()
343 summary.psfIxy = shape.getIxy()
344 im = psf.computeKernelImage(bbox.getCenter())
347 summary.psfArea = float(np.sum(im.array)**2./np.sum(im.array**2.))
349 if image_mask
is not None:
350 psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
351 self.log.debug(
"Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
355 sampling=self.config.psfGridSampling,
356 ap_radius_pix=psfApRadius,
357 bad_mask_bits=self.config.psfBadMaskPlanes
359 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
360 summary.psfApFluxDelta = float(psfApFluxDelta)
361 if image_ap_corr_map
is not None:
362 if self.config.psfApCorrFieldName
not in image_ap_corr_map.keys():
363 self.log.warning(f
"{self.config.psfApCorrFieldName} not found in "
364 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
365 psfApCorrSigmaScaledDelta = nan
367 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
372 sampling=self.config.psfGridSampling,
373 bad_mask_bits=self.config.psfBadMaskPlanes,
375 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
384 nPsfStar = sources[self.config.starSelection].sum()
385 summary.nPsfStar = int(nPsfStar)
387 psf_mask = self.starSelector.run(sources).selected
388 nPsfStarsUsedInStats = psf_mask.sum()
390 if nPsfStarsUsedInStats == 0:
395 if sources_is_astropy:
396 psf_cat = sources[psf_mask]
398 psf_cat = sources[psf_mask].copy(deep=
True)
400 starXX = psf_cat[self.config.starShape +
'_xx']
401 starYY = psf_cat[self.config.starShape +
'_yy']
402 starXY = psf_cat[self.config.starShape +
'_xy']
403 psfXX = psf_cat[self.config.psfShape +
'_xx']
404 psfYY = psf_cat[self.config.psfShape +
'_yy']
405 psfXY = psf_cat[self.config.psfShape +
'_xy']
408 starSize = np.sqrt(starXX/2. + starYY/2.)
410 starE1 = (starXX - starYY)/(starXX + starYY)
411 starE2 = 2*starXY/(starXX + starYY)
412 starSizeMedian = np.median(starSize)
414 starE = np.sqrt(starE1**2.0 + starE2**2.0)
415 starUnNormalizedE = np.sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)
416 starEMedian = np.median(starE)
417 starUnNormalizedEMedian = np.median(starUnNormalizedE)
418 summary.starEMedian = float(starEMedian)
419 summary.starUnNormalizedEMedian = float(starUnNormalizedEMedian)
422 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
423 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
424 psfE2 = 2*psfXY/(psfXX + psfYY)
426 psfStarDeltaE1Median = np.median(starE1 - psfE1)
427 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
428 psfStarDeltaE2Median = np.median(starE2 - psfE2)
429 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
431 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
432 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
433 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
435 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
436 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
437 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
438 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
439 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
440 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
441 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
443 if image_mask
is not None:
447 sampling=self.config.psfSampling,
448 bad_mask_bits=self.config.psfBadMaskPlanes
450 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
453 """Compute all summary-statistic fields that depend on the WCS model.
457 summary : `lsst.afw.image.ExposureSummaryStats`
458 Summary object to update in-place.
459 wcs : `lsst.afw.geom.SkyWcs` or `None`
460 Astrometric calibration model. If `None`, all fields that depend
461 on the WCS will be reset (generally to NaN).
462 bbox : `lsst.geom.Box2I`
463 Bounding box of the image for which summary stats are being
465 visitInfo : `lsst.afw.image.VisitInfo`
466 Observation information used in together with ``wcs`` to compute
470 summary.raCorners = [nan]*4
471 summary.decCorners = [nan]*4
474 summary.pixelScale = nan
475 summary.zenithDistance = nan
480 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
481 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
482 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
484 sph_pt = wcs.pixelToSky(bbox.getCenter())
485 summary.ra = sph_pt.getRa().asDegrees()
486 summary.dec = sph_pt.getDec().asDegrees()
487 summary.pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
489 date = visitInfo.getDate()
496 observatory = visitInfo.getObservatory()
497 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
498 lon=observatory.getLongitude().asDegrees()*units.deg,
499 height=observatory.getElevation()*units.m)
500 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
501 location=loc, format=
'mjd')
503 summary.ra*units.degree,
504 summary.dec*units.degree,
508 with warnings.catch_warnings():
509 warnings.simplefilter(
'ignore')
510 altaz = coord.transform_to(AltAz)
512 summary.zenithDistance = float(90.0 - altaz.alt.degree)
558 """Compute summary-statistic fields that depend on the masked image
563 summary : `lsst.afw.image.ExposureSummaryStats`
564 Summary object to update in-place.
565 masked_image : `lsst.afw.image.MaskedImage` or `None`
566 Masked image. If `None`, all fields that depend
567 on the masked image will be reset (generally to NaN).
570 if masked_image
is None:
571 summary.skyNoise = nan
572 summary.meanVar = nan
574 statsCtrl = afwMath.StatisticsControl()
575 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
576 statsCtrl.setNumIter(self.config.clipIter)
577 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
578 statsCtrl.setNanSafe(
True)
580 statObj = afwMath.makeStatistics(masked_image, afwMath.STDEVCLIP, statsCtrl)
581 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
582 summary.skyNoise = skyNoise
584 statObj = afwMath.makeStatistics(masked_image.variance, masked_image.mask, afwMath.MEANCLIP,
586 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
587 summary.meanVar = meanVar
590 """Compute effective exposure time statistics to estimate depth.
592 The effective exposure time is the equivalent shutter open
593 time that would be needed under nominal conditions to give the
594 same signal-to-noise for a point source as what is achieved by
595 the observation of interest. This metric combines measurements
596 of the point-spread function, the sky brightness, and the
597 transparency. It assumes that the observation is
598 sky-background dominated.
600 .. _teff_definitions:
602 The effective exposure time and its subcomponents are defined in [1]_.
607 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
608 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
609 https://www.osti.gov/biblio/1250877/
613 summary : `lsst.afw.image.ExposureSummaryStats`
614 Summary object to update in-place.
615 exposure : `lsst.afw.image.ExposureF`
616 Exposure to grab band and exposure time metadata
620 summary.effTime = nan
621 summary.effTimePsfSigmaScale = nan
622 summary.effTimeSkyBgScale = nan
623 summary.effTimeZeroPointScale = nan
625 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
626 filterLabel = exposure.getFilter()
627 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
630 band = filterLabel.bandLabel
633 self.log.warning(
"No band associated with exposure; effTime not calculated.")
637 if np.isnan(summary.psfSigma):
638 self.log.debug(
"PSF sigma is NaN")
640 elif band
not in self.config.fiducialPsfSigma:
641 self.log.debug(f
"Fiducial PSF value not found for {band}")
644 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
645 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
649 if np.isnan(summary.zeroPoint):
650 self.log.debug(
"Zero point is NaN")
652 elif band
not in self.config.fiducialZeroPoint:
653 self.log.debug(f
"Fiducial zero point value not found for {band}")
656 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
657 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
658 c_eff = min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
661 if np.isnan(summary.skyBg):
662 self.log.debug(
"Sky background is NaN")
664 elif band
not in self.config.fiducialSkyBackground:
665 self.log.debug(f
"Fiducial sky background value not found for {band}")
668 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
669 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
672 t_eff = f_eff * c_eff * b_eff
675 effectiveTime = t_eff * exposureTime
678 summary.effTime = float(effectiveTime)
679 summary.effTimePsfSigmaScale = float(f_eff)
680 summary.effTimeSkyBgScale = float(b_eff)
681 summary.effTimeZeroPointScale = float(c_eff)