296 def run(self, exposure, sources, background):
297 """Measure exposure statistics from the exposure, sources, and
302 exposure : `lsst.afw.image.ExposureF`
303 sources : `lsst.afw.table.SourceCatalog`
304 background : `lsst.afw.math.BackgroundList`
308 summary : `lsst.afw.image.ExposureSummary`
310 self.log.info(
"Measuring exposure statistics")
312 summary = afwImage.ExposureSummaryStats()
315 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
316 summary.expTime = exposureTime
318 bbox = exposure.getBBox()
320 psf = exposure.getPsf()
322 summary, psf, bbox, sources, image_mask=exposure.mask, image_ap_corr_map=exposure.apCorrMap
325 wcs = exposure.getWcs()
326 visitInfo = exposure.getInfo().getVisitInfo()
329 photoCalib = exposure.getPhotoCalib()
340 md = exposure.getMetadata()
341 if 'SFM_ASTROM_OFFSET_MEAN' in md:
342 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
343 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
354 image_ap_corr_map=None,
355 sources_is_astropy=False,
357 """Compute all summary-statistic fields that depend on the PSF model.
361 summary : `lsst.afw.image.ExposureSummaryStats`
362 Summary object to update in-place.
363 psf : `lsst.afw.detection.Psf` or `None`
364 Point spread function model. If `None`, all fields that depend on
365 the PSF will be reset (generally to NaN).
366 bbox : `lsst.geom.Box2I`
367 Bounding box of the image for which summary stats are being
369 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
370 Catalog for quantities that are computed from source table columns.
371 If `None`, these quantities will be reset (generally to NaN).
372 The type of this table must correspond to the
373 ``sources_is_astropy`` argument.
374 image_mask : `lsst.afw.image.Mask`, optional
375 Mask image that may be used to compute distance-to-nearest-star
377 sources_is_astropy : `bool`, optional
378 Whether ``sources`` is an `astropy.table.Table` instance instead
379 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
383 summary.psfSigma = nan
387 summary.psfArea = nan
389 summary.psfStarDeltaE1Median = nan
390 summary.psfStarDeltaE2Median = nan
391 summary.psfStarDeltaE1Scatter = nan
392 summary.psfStarDeltaE2Scatter = nan
393 summary.psfStarDeltaSizeMedian = nan
394 summary.psfStarDeltaSizeScatter = nan
395 summary.psfStarScaledDeltaSizeScatter = nan
396 summary.maxDistToNearestPsf = nan
397 summary.psfTraceRadiusDelta = nan
398 summary.psfApFluxDelta = nan
399 summary.psfApCorrSigmaScaledDelta = nan
400 summary.starEMedian = nan
401 summary.starUnNormalizedEMedian = nan
405 shape = psf.computeShape(bbox.getCenter())
406 summary.psfSigma = shape.getDeterminantRadius()
407 summary.psfIxx = shape.getIxx()
408 summary.psfIyy = shape.getIyy()
409 summary.psfIxy = shape.getIxy()
410 im = psf.computeKernelImage(bbox.getCenter())
413 summary.psfArea = float(np.sum(im.array)**2./np.sum(im.array**2.))
415 if image_mask
is not None:
416 psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
417 self.log.debug(
"Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
421 sampling=self.config.psfGridSampling,
422 ap_radius_pix=psfApRadius,
423 bad_mask_bits=self.config.psfBadMaskPlanes
425 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
426 summary.psfApFluxDelta = float(psfApFluxDelta)
427 if image_ap_corr_map
is not None:
428 if self.config.psfApCorrFieldName
not in image_ap_corr_map.keys():
429 self.log.warning(f
"{self.config.psfApCorrFieldName} not found in "
430 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
431 psfApCorrSigmaScaledDelta = nan
433 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
438 sampling=self.config.psfGridSampling,
439 bad_mask_bits=self.config.psfBadMaskPlanes,
441 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
450 nPsfStar = sources[self.config.starSelection].sum()
451 summary.nPsfStar = int(nPsfStar)
453 psf_mask = self.starSelector.run(sources).selected
454 nPsfStarsUsedInStats = psf_mask.sum()
456 if nPsfStarsUsedInStats == 0:
461 if sources_is_astropy:
462 psf_cat = sources[psf_mask]
464 psf_cat = sources[psf_mask].copy(deep=
True)
466 starXX = psf_cat[self.config.starShape +
'_xx']
467 starYY = psf_cat[self.config.starShape +
'_yy']
468 starXY = psf_cat[self.config.starShape +
'_xy']
469 psfXX = psf_cat[self.config.psfShape +
'_xx']
470 psfYY = psf_cat[self.config.psfShape +
'_yy']
471 psfXY = psf_cat[self.config.psfShape +
'_xy']
474 starSize = np.sqrt(starXX/2. + starYY/2.)
476 starE1 = (starXX - starYY)/(starXX + starYY)
477 starE2 = 2*starXY/(starXX + starYY)
478 starSizeMedian = np.median(starSize)
480 starE = np.sqrt(starE1**2.0 + starE2**2.0)
481 starUnNormalizedE = np.sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)
482 starEMedian = np.median(starE)
483 starUnNormalizedEMedian = np.median(starUnNormalizedE)
484 summary.starEMedian = float(starEMedian)
485 summary.starUnNormalizedEMedian = float(starUnNormalizedEMedian)
488 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
489 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
490 psfE2 = 2*psfXY/(psfXX + psfYY)
492 psfStarDeltaE1Median = np.median(starE1 - psfE1)
493 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
494 psfStarDeltaE2Median = np.median(starE2 - psfE2)
495 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
497 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
498 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
499 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
501 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
502 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
503 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
504 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
505 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
506 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
507 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
509 if image_mask
is not None:
513 sampling=self.config.psfSampling,
514 bad_mask_bits=self.config.psfBadMaskPlanes
516 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
519 """Compute all summary-statistic fields that depend on the WCS model.
523 summary : `lsst.afw.image.ExposureSummaryStats`
524 Summary object to update in-place.
525 wcs : `lsst.afw.geom.SkyWcs` or `None`
526 Astrometric calibration model. If `None`, all fields that depend
527 on the WCS will be reset (generally to NaN).
528 bbox : `lsst.geom.Box2I`
529 Bounding box of the image for which summary stats are being
531 visitInfo : `lsst.afw.image.VisitInfo`
532 Observation information used in together with ``wcs`` to compute
536 summary.raCorners = [nan]*4
537 summary.decCorners = [nan]*4
540 summary.pixelScale = nan
541 summary.zenithDistance = nan
546 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
547 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
548 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
550 sph_pt = wcs.pixelToSky(bbox.getCenter())
551 summary.ra = sph_pt.getRa().asDegrees()
552 summary.dec = sph_pt.getDec().asDegrees()
553 summary.pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
555 date = visitInfo.getDate()
562 observatory = visitInfo.getObservatory()
563 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
564 lon=observatory.getLongitude().asDegrees()*units.deg,
565 height=observatory.getElevation()*units.m)
566 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
567 location=loc, format=
'mjd')
569 summary.ra*units.degree,
570 summary.dec*units.degree,
574 with warnings.catch_warnings():
575 warnings.simplefilter(
'ignore')
576 altaz = coord.transform_to(AltAz)
578 summary.zenithDistance = float(90.0 - altaz.alt.degree)
624 """Compute summary-statistic fields that depend on the masked image
629 summary : `lsst.afw.image.ExposureSummaryStats`
630 Summary object to update in-place.
631 masked_image : `lsst.afw.image.MaskedImage` or `None`
632 Masked image. If `None`, all fields that depend
633 on the masked image will be reset (generally to NaN).
636 if masked_image
is None:
637 summary.skyNoise = nan
638 summary.meanVar = nan
640 statsCtrl = afwMath.StatisticsControl()
641 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
642 statsCtrl.setNumIter(self.config.clipIter)
643 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
644 statsCtrl.setNanSafe(
True)
646 statObj = afwMath.makeStatistics(masked_image, afwMath.STDEVCLIP, statsCtrl)
647 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
648 summary.skyNoise = skyNoise
650 statObj = afwMath.makeStatistics(masked_image.variance, masked_image.mask, afwMath.MEANCLIP,
652 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
653 summary.meanVar = meanVar
656 """Compute effective exposure time statistics to estimate depth.
658 The effective exposure time is the equivalent shutter open
659 time that would be needed under nominal conditions to give the
660 same signal-to-noise for a point source as what is achieved by
661 the observation of interest. This metric combines measurements
662 of the point-spread function, the sky brightness, and the
663 transparency. It assumes that the observation is
664 sky-background dominated.
666 .. _teff_definitions:
668 The effective exposure time and its subcomponents are defined in [1]_.
673 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
674 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
675 https://www.osti.gov/biblio/1250877/
679 summary : `lsst.afw.image.ExposureSummaryStats`
680 Summary object to update in-place.
681 exposure : `lsst.afw.image.ExposureF`
682 Exposure to grab band and exposure time metadata
686 summary.effTime = nan
687 summary.effTimePsfSigmaScale = nan
688 summary.effTimeSkyBgScale = nan
689 summary.effTimeZeroPointScale = nan
691 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
692 filterLabel = exposure.getFilter()
693 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
696 band = filterLabel.bandLabel
699 self.log.warning(
"No band associated with exposure; effTime not calculated.")
703 if np.isnan(summary.psfSigma):
704 self.log.debug(
"PSF sigma is NaN")
706 elif band
not in self.config.fiducialPsfSigma:
707 self.log.debug(f
"Fiducial PSF value not found for {band}")
710 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
711 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
715 if np.isnan(summary.zeroPoint):
716 self.log.debug(
"Zero point is NaN")
718 elif band
not in self.config.fiducialZeroPoint:
719 self.log.debug(f
"Fiducial zero point value not found for {band}")
722 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
723 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
724 c_eff = min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
727 if np.isnan(summary.skyBg):
728 self.log.debug(
"Sky background is NaN")
730 elif band
not in self.config.fiducialSkyBackground:
731 self.log.debug(f
"Fiducial sky background value not found for {band}")
734 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
735 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
741 if band
not in self.config.fiducialMagLim:
742 self.log.debug(f
"Fiducial magnitude limit not found for {band}")
744 elif band
not in self.config.fiducialExpTime:
745 self.log.debug(f
"Fiducial exposure time not found for {band}")
749 self.config.fiducialMagLim[band],
750 self.config.fiducialExpTime[band])
753 summary.effTime = float(effectiveTime)
754 summary.effTimePsfSigmaScale = float(f_eff)
755 summary.effTimeSkyBgScale = float(b_eff)
756 summary.effTimeZeroPointScale = float(c_eff)