424 image_ap_corr_map=None,
425 sources_is_astropy=False,
427 """Compute all summary-statistic fields that depend on the PSF model.
431 summary : `lsst.afw.image.ExposureSummaryStats`
432 Summary object to update in-place.
433 psf : `lsst.afw.detection.Psf` or `None`
434 Point spread function model. If `None`, all fields that depend on
435 the PSF will be reset (generally to NaN).
436 bbox : `lsst.geom.Box2I`
437 Bounding box of the image for which summary stats are being
439 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
440 Catalog for quantities that are computed from source table columns.
441 If `None`, these quantities will be reset (generally to NaN).
442 The type of this table must correspond to the
443 ``sources_is_astropy`` argument.
444 image_mask : `lsst.afw.image.Mask`, optional
445 Mask image that may be used to compute distance-to-nearest-star
447 sources_is_astropy : `bool`, optional
448 Whether ``sources`` is an `astropy.table.Table` instance instead
449 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
453 summary.psfSigma = nan
457 summary.psfArea = nan
459 summary.psfStarDeltaE1Median = nan
460 summary.psfStarDeltaE2Median = nan
461 summary.psfStarDeltaE1Scatter = nan
462 summary.psfStarDeltaE2Scatter = nan
463 summary.psfStarDeltaSizeMedian = nan
464 summary.psfStarDeltaSizeScatter = nan
465 summary.psfStarScaledDeltaSizeScatter = nan
466 summary.maxDistToNearestPsf = nan
467 summary.psfTraceRadiusDelta = nan
468 summary.psfApFluxDelta = nan
469 summary.psfApCorrSigmaScaledDelta = nan
470 summary.starEMedian = nan
471 summary.starUnNormalizedEMedian = nan
472 summary.starComa1Median = nan
473 summary.starComa2Median = nan
474 summary.starTrefoil1Median = nan
475 summary.starTrefoil2Median = nan
476 summary.starKurtosisMedian = nan
477 summary.starE41Median = nan
478 summary.starE42Median = nan
482 shape = psf.computeShape(bbox.getCenter())
483 summary.psfSigma = shape.getDeterminantRadius()
484 summary.psfIxx = shape.getIxx()
485 summary.psfIyy = shape.getIyy()
486 summary.psfIxy = shape.getIxy()
487 im = psf.computeKernelImage(bbox.getCenter())
490 summary.psfArea = float(np.sum(im.array)**2./np.sum(im.array**2.))
492 if image_mask
is not None:
493 psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
494 self.log.debug(
"Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
498 sampling=self.config.psfGridSampling,
499 ap_radius_pix=psfApRadius,
500 bad_mask_bits=self.config.psfBadMaskPlanes
502 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
503 summary.psfApFluxDelta = float(psfApFluxDelta)
504 if image_ap_corr_map
is not None:
505 if self.config.psfApCorrFieldName
not in image_ap_corr_map.keys():
506 self.log.warning(f
"{self.config.psfApCorrFieldName} not found in "
507 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
508 psfApCorrSigmaScaledDelta = nan
510 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
515 sampling=self.config.psfGridSampling,
516 bad_mask_bits=self.config.psfBadMaskPlanes,
518 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
527 nPsfStar = sources[self.config.starSelection].sum()
528 summary.nPsfStar = int(nPsfStar)
530 psf_mask = self.starSelector.run(sources).selected
531 nPsfStarsUsedInStats = psf_mask.sum()
533 if nPsfStarsUsedInStats == 0:
538 if sources_is_astropy:
539 psf_cat = sources[psf_mask]
541 psf_cat = sources[psf_mask].copy(deep=
True)
543 starXX = psf_cat[self.config.starShape +
"_xx"]
544 starYY = psf_cat[self.config.starShape +
"_yy"]
545 starXY = psf_cat[self.config.starShape +
"_xy"]
546 psfXX = psf_cat[self.config.psfShape +
"_xx"]
547 psfYY = psf_cat[self.config.psfShape +
"_yy"]
548 psfXY = psf_cat[self.config.psfShape +
"_xy"]
551 starSize = np.sqrt(starXX/2. + starYY/2.)
553 starE1 = (starXX - starYY)/(starXX + starYY)
554 starE2 = 2*starXY/(starXX + starYY)
555 starSizeMedian = np.median(starSize)
557 starE = np.sqrt(starE1**2.0 + starE2**2.0)
558 starUnNormalizedE = np.sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)
559 starEMedian = np.median(starE)
560 starUnNormalizedEMedian = np.median(starUnNormalizedE)
561 summary.starEMedian = float(starEMedian)
562 summary.starUnNormalizedEMedian = float(starUnNormalizedEMedian)
565 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
566 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
567 psfE2 = 2*psfXY/(psfXX + psfYY)
569 e1Residuals = starE1 - psfE1
570 e2Residuals = starE2 - psfE2
572 psfStarDeltaE1Median = np.median(e1Residuals)
573 psfStarDeltaE1Scatter = sigmaMad(e1Residuals, scale=
'normal')
574 psfStarDeltaE2Median = np.median(e2Residuals)
575 psfStarDeltaE2Scatter = sigmaMad(e2Residuals, scale=
'normal')
577 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
578 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
"normal")
579 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
581 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
582 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
583 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
584 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
585 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
586 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
587 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
590 if sources_is_astropy:
591 columnNames = psf_cat.colnames
593 columnNames = psf_cat.schema.getNames()
594 if self.config.starHigherOrderMomentBase +
"_03" in columnNames:
595 starM03 = psf_cat[self.config.starHigherOrderMomentBase +
"_03"]
596 starM12 = psf_cat[self.config.starHigherOrderMomentBase +
"_12"]
597 starM21 = psf_cat[self.config.starHigherOrderMomentBase +
"_21"]
598 starM30 = psf_cat[self.config.starHigherOrderMomentBase +
"_30"]
599 starM04 = psf_cat[self.config.starHigherOrderMomentBase +
"_04"]
600 starM13 = psf_cat[self.config.starHigherOrderMomentBase +
"_13"]
601 starM22 = psf_cat[self.config.starHigherOrderMomentBase +
"_22"]
602 starM31 = psf_cat[self.config.starHigherOrderMomentBase +
"_31"]
603 starM40 = psf_cat[self.config.starHigherOrderMomentBase +
"_40"]
605 starComa1Median = np.nanmedian(starM30 + starM12)
606 starComa2Median = np.nanmedian(starM21 + starM03)
607 starTrefoil1Median = np.nanmedian(starM30 - 3.0*starM12)
608 starTrefoil2Median = np.nanmedian(3.0*starM21 - starM03)
609 starKurtosisMedian = np.nanmedian(starM40 + 2.0*starM22 + starM04)
610 starE41Median = np.nanmedian(starM40 - starM04)
611 starE42Median = np.nanmedian(2.0*(starM31 + starM13))
613 summary.starComa1Median = float(starComa1Median)
614 summary.starComa2Median = float(starComa2Median)
615 summary.starTrefoil1Median = float(starTrefoil1Median)
616 summary.starTrefoil2Median = float(starTrefoil2Median)
617 summary.starKurtosisMedian = float(starKurtosisMedian)
618 summary.starE41Median = float(starE41Median)
619 summary.starE42Median = float(starE42Median)
621 self.log.warning(
"Higher-order moments with base column name %s not found in source "
622 "catalog. Setting the derived metrics (i.e. coma1[2], trefoil1[2], "
623 "kurtosis, e4_1, and e4_2) to nan.", self.config.starHigherOrderMomentBase)
625 if image_mask
is not None:
629 sampling=self.config.psfSampling,
630 bad_mask_bits=self.config.psfBadMaskPlanes
632 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
635 """Compute all summary-statistic fields at visit level for TEx metric of PSF.
639 summary : `lsst.afw.image.ExposureSummaryStats`
640 Summary object to update in-place.
641 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
642 Catalog for quantities that are computed from source table columns.
643 If `None`, these quantities will be reset (generally to NaN).
644 The type of this table must correspond to the
645 ``sources_is_astropy`` argument.
646 sources_is_astropy : `bool`, optional
647 Whether ``sources`` is an `astropy.table.Table` instance instead
648 of an `lsst.afw.table.Catalog` instance.
684 psf_mask = self.starSelector.run(sources).selected
686 nPsfStarsUsedInStats = psf_mask.sum()
688 if nPsfStarsUsedInStats == 0:
693 if sources_is_astropy:
694 psf_cat = sources[psf_mask]
696 psf_cat = sources[psf_mask].copy(deep=
True)
698 starXX = psf_cat[self.config.starShape +
'_xx']
699 starYY = psf_cat[self.config.starShape +
'_yy']
700 starXY = psf_cat[self.config.starShape +
'_xy']
701 psfXX = psf_cat[self.config.psfShape +
'_xx']
702 psfYY = psf_cat[self.config.psfShape +
'_yy']
703 psfXY = psf_cat[self.config.psfShape +
'_xy']
705 starE1 = (starXX - starYY)/(starXX + starYY)
706 starE2 = 2*starXY/(starXX + starYY)
708 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
709 psfE2 = 2*psfXY/(psfXX + psfYY)
711 e1Residuals = starE1 - psfE1
712 e2Residuals = starE2 - psfE2
715 ra = psf_cat[
"coord_ra"].to(units.deg)
716 dec = psf_cat[
"coord_dec"].to(units.deg)
724 "TE1": {
"E1": np.nan,
"E2": np.nan,
"Ex": np.nan, },
725 "TE2": {
"E1": np.nan,
"E2": np.nan,
"Ex": np.nan, },
728 isNotNan = np.array([
True] * len(ra))
729 isNotNan &= np.isfinite(ra)
730 isNotNan &= np.isfinite(dec)
731 isNotNan &= np.isfinite(e1Residuals)
732 isNotNan &= np.isfinite(e2Residuals)
734 if np.sum(isNotNan) >= 2:
736 for TEX
in [
"TE1",
"TE2"]:
738 output = psfTEx[TEX].run(
739 e1Residuals[isNotNan], e2Residuals[isNotNan],
740 ra[isNotNan], dec[isNotNan],
744 gatherE12Stat[TEX][
"E1"] = output.metric_E1
745 gatherE12Stat[TEX][
"E2"] = output.metric_E2
746 gatherE12Stat[TEX][
"Ex"] = output.metric_Ex
756 "TE3": {
"E1": [],
"E2": [],
"Ex": [], },
757 "TE4": {
"E1": [],
"E2": [],
"Ex": [], },
761 if "detector" in psf_cat.colnames:
762 detectorIds = list(set(psf_cat[
"detector"]))
763 for TEX
in [
"TE3",
"TE4"]:
764 for ccdId
in detectorIds:
765 isccdId = (ccdId == psf_cat[
"detector"])
766 mask = (isccdId & isNotNan)
767 if np.sum(mask) >= 2:
768 output = psfTEx[TEX].run(
769 e1Residuals[mask], e2Residuals[mask],
773 gatherE34Stat[TEX][
"E1"].append(output.metric_E1)
774 gatherE34Stat[TEX][
"E2"].append(output.metric_E2)
775 gatherE34Stat[TEX][
"Ex"].append(output.metric_Ex)
777 self.
psfTE1e1 = summary.psfTE1e1 = gatherE12Stat[
"TE1"][
"E1"]
778 self.
psfTE1e2 = summary.psfTE1e2 = gatherE12Stat[
"TE1"][
"E2"]
779 self.
psfTE1ex = summary.psfTE1ex = gatherE12Stat[
"TE1"][
"Ex"]
780 self.
psfTE2e1 = summary.psfTE2e1 = gatherE12Stat[
"TE2"][
"E1"]
781 self.
psfTE2e2 = summary.psfTE2e2 = gatherE12Stat[
"TE2"][
"E2"]
782 self.
psfTE2ex = summary.psfTE2ex = gatherE12Stat[
"TE2"][
"Ex"]
783 self.
psfTE3e1 = summary.psfTE3e1 = np.nanmedian(gatherE34Stat[
"TE3"][
"E1"])
784 self.
psfTE3e2 = summary.psfTE3e2 = np.nanmedian(gatherE34Stat[
"TE3"][
"E2"])
785 self.
psfTE3ex = summary.psfTE3ex = np.nanmedian(gatherE34Stat[
"TE3"][
"Ex"])
786 self.
psfTE4e1 = summary.psfTE4e1 = np.nanmedian(gatherE34Stat[
"TE4"][
"E1"])
787 self.
psfTE4e2 = summary.psfTE4e2 = np.nanmedian(gatherE34Stat[
"TE4"][
"E2"])
788 self.
psfTE4ex = summary.psfTE4ex = np.nanmedian(gatherE34Stat[
"TE4"][
"Ex"])
791 """Compute all summary-statistic fields that depend on the WCS model.
795 summary : `lsst.afw.image.ExposureSummaryStats`
796 Summary object to update in-place.
797 wcs : `lsst.afw.geom.SkyWcs` or `None`
798 Astrometric calibration model. If `None`, all fields that depend
799 on the WCS will be reset (generally to NaN).
800 bbox : `lsst.geom.Box2I`
801 Bounding box of the image for which summary stats are being
803 visitInfo : `lsst.afw.image.VisitInfo`
804 Observation information used in together with ``wcs`` to compute
808 summary.raCorners = [nan]*4
809 summary.decCorners = [nan]*4
812 summary.pixelScale = nan
813 summary.zenithDistance = nan
818 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
819 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
820 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
822 sph_pt = wcs.pixelToSky(bbox.getCenter())
823 summary.ra = sph_pt.getRa().asDegrees()
824 summary.dec = sph_pt.getDec().asDegrees()
825 summary.pixelScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds()
827 date = visitInfo.getDate()
834 observatory = visitInfo.getObservatory()
835 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
836 lon=observatory.getLongitude().asDegrees()*units.deg,
837 height=observatory.getElevation()*units.m)
838 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
839 location=loc, format=
"mjd")
841 summary.ra*units.degree,
842 summary.dec*units.degree,
846 with warnings.catch_warnings():
847 warnings.simplefilter(
"ignore")
848 altaz = coord.transform_to(AltAz)
850 summary.zenithDistance = float(90.0 - altaz.alt.degree)
928 """Compute effective exposure time statistics to estimate depth.
930 The effective exposure time is the equivalent shutter open
931 time that would be needed under nominal conditions to give the
932 same signal-to-noise for a point source as what is achieved by
933 the observation of interest. This metric combines measurements
934 of the point-spread function, the sky brightness, and the
935 transparency. It assumes that the observation is
936 sky-background dominated.
938 .. _teff_definitions:
940 The effective exposure time and its subcomponents are defined in [1]_.
945 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
946 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
947 https://www.osti.gov/biblio/1250877/
951 summary : `lsst.afw.image.ExposureSummaryStats`
952 Summary object to update in-place.
953 exposure : `lsst.afw.image.ExposureF`
954 Exposure to grab band and exposure time metadata
958 summary.effTime = nan
959 summary.effTimePsfSigmaScale = nan
960 summary.effTimeSkyBgScale = nan
961 summary.effTimeZeroPointScale = nan
963 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
964 filterLabel = exposure.getFilter()
965 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
968 band = filterLabel.bandLabel
971 self.log.warning(
"No band associated with exposure; effTime not calculated.")
975 if np.isnan(summary.psfSigma):
976 self.log.debug(
"PSF sigma is NaN")
978 elif band
not in self.config.fiducialPsfSigma:
979 self.log.debug(f
"Fiducial PSF value not found for {band}")
982 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
983 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
987 if np.isnan(summary.zeroPoint):
988 self.log.debug(
"Zero point is NaN")
990 elif band
not in self.config.fiducialZeroPoint:
991 self.log.debug(f
"Fiducial zero point value not found for {band}")
994 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
995 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
996 c_eff = min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
999 if np.isnan(summary.skyBg):
1000 self.log.debug(
"Sky background is NaN")
1002 elif band
not in self.config.fiducialSkyBackground:
1003 self.log.debug(f
"Fiducial sky background value not found for {band}")
1006 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
1007 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
1013 if band
not in self.config.fiducialMagLim:
1014 self.log.debug(f
"Fiducial magnitude limit not found for {band}")
1016 elif band
not in self.config.fiducialExpTime:
1017 self.log.debug(f
"Fiducial exposure time not found for {band}")
1021 self.config.fiducialMagLim[band],
1022 self.config.fiducialExpTime[band])
1025 summary.effTime = float(effectiveTime)
1026 summary.effTimePsfSigmaScale = float(f_eff)
1027 summary.effTimeSkyBgScale = float(b_eff)
1028 summary.effTimeZeroPointScale = float(c_eff)