Coverage for python/lsst/pipe/tasks/computeExposureSummaryStats.py : 21%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21import warnings
22import numpy as np
23import astropy.units as units
24from astropy.time import Time
25from astropy.coordinates import AltAz, SkyCoord, EarthLocation
26from lsst.daf.base import DateTime
28import lsst.pipe.base as pipeBase
29import lsst.pex.config as pexConfig
30import lsst.afw.math as afwMath
31import lsst.afw.image as afwImage
32import lsst.geom
33from lsst.utils.timer import timeMethod
36__all__ = ("ComputeExposureSummaryStatsTask", "ComputeExposureSummaryStatsConfig")
39class ComputeExposureSummaryStatsConfig(pexConfig.Config):
40 """Config for ComputeExposureSummaryTask"""
41 sigmaClip = pexConfig.Field(
42 dtype=float,
43 doc="Sigma for outlier rejection for sky noise.",
44 default=3.0,
45 )
46 clipIter = pexConfig.Field(
47 dtype=int,
48 doc="Number of iterations of outlier rejection for sky noise.",
49 default=2,
50 )
51 badMaskPlanes = pexConfig.ListField(
52 dtype=str,
53 doc="Mask planes that, if set, the associated pixel should not be included sky noise calculation.",
54 default=("NO_DATA", "SUSPECT"),
55 )
58class ComputeExposureSummaryStatsTask(pipeBase.Task):
59 """Task to compute exposure summary statistics.
61 This task computes various quantities suitable for DPDD and other
62 downstream processing at the detector centers, including:
63 - psfSigma
64 - psfArea
65 - psfIxx
66 - psfIyy
67 - psfIxy
68 - ra
69 - decl
70 - zenithDistance
71 - zeroPoint
72 - skyBg
73 - skyNoise
74 - meanVar
75 - raCorners
76 - decCorners
77 - astromOffsetMean
78 - astromOffsetStd
79 """
80 ConfigClass = ComputeExposureSummaryStatsConfig
81 _DefaultName = "computeExposureSummaryStats"
83 @timeMethod
84 def run(self, exposure, sources, background):
85 """Measure exposure statistics from the exposure, sources, and background.
87 Parameters
88 ----------
89 exposure : `lsst.afw.image.ExposureF`
90 sources : `lsst.afw.table.SourceCatalog`
91 background : `lsst.afw.math.BackgroundList`
93 Returns
94 -------
95 summary : `lsst.afw.image.ExposureSummary`
96 """
97 self.log.info("Measuring exposure statistics")
99 bbox = exposure.getBBox()
101 psf = exposure.getPsf()
102 if psf is not None:
103 shape = psf.computeShape(bbox.getCenter())
104 psfSigma = shape.getDeterminantRadius()
105 psfIxx = shape.getIxx()
106 psfIyy = shape.getIyy()
107 psfIxy = shape.getIxy()
108 im = psf.computeKernelImage(bbox.getCenter())
109 # The calculation of effective psf area is taken from
110 # meas_base/src/PsfFlux.cc#L112. See
111 # https://github.com/lsst/meas_base/blob/
112 # 750bffe6620e565bda731add1509507f5c40c8bb/src/PsfFlux.cc#L112
113 psfArea = np.sum(im.array)/np.sum(im.array**2.)
114 else:
115 psfSigma = np.nan
116 psfIxx = np.nan
117 psfIyy = np.nan
118 psfIxy = np.nan
119 psfArea = np.nan
121 wcs = exposure.getWcs()
122 if wcs is not None:
123 sph_pts = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners())
124 raCorners = [float(sph.getRa().asDegrees()) for sph in sph_pts]
125 decCorners = [float(sph.getDec().asDegrees()) for sph in sph_pts]
127 sph_pt = wcs.pixelToSky(bbox.getCenter())
128 ra = sph_pt.getRa().asDegrees()
129 decl = sph_pt.getDec().asDegrees()
130 else:
131 raCorners = [float(np.nan)]*4
132 decCorners = [float(np.nan)]*4
133 ra = np.nan
134 decl = np.nan
136 photoCalib = exposure.getPhotoCalib()
137 if photoCalib is not None:
138 zeroPoint = 2.5*np.log10(photoCalib.getInstFluxAtZeroMagnitude())
139 else:
140 zeroPoint = np.nan
142 visitInfo = exposure.getInfo().getVisitInfo()
143 date = visitInfo.getDate()
145 if date.isValid():
146 # We compute the zenithDistance at the center of the detector rather
147 # than use the boresight value available via the visitInfo, because
148 # the zenithDistance may vary significantly over a large field of view.
149 observatory = visitInfo.getObservatory()
150 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
151 lon=observatory.getLongitude().asDegrees()*units.deg,
152 height=observatory.getElevation()*units.m)
153 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
154 location=loc, format='mjd')
155 coord = SkyCoord(ra*units.degree, decl*units.degree, obstime=obstime, location=loc)
156 with warnings.catch_warnings():
157 warnings.simplefilter('ignore')
158 altaz = coord.transform_to(AltAz)
160 zenithDistance = 90.0 - altaz.alt.degree
161 else:
162 zenithDistance = np.nan
164 if background is not None:
165 bgStats = (bg[0].getStatsImage().getImage().array
166 for bg in background)
167 skyBg = sum(np.median(bg[np.isfinite(bg)]) for bg in bgStats)
168 else:
169 skyBg = np.nan
171 statsCtrl = afwMath.StatisticsControl()
172 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
173 statsCtrl.setNumIter(self.config.clipIter)
174 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
175 statsCtrl.setNanSafe(True)
177 statObj = afwMath.makeStatistics(exposure.getMaskedImage(), afwMath.STDEVCLIP,
178 statsCtrl)
179 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
181 statObj = afwMath.makeStatistics(exposure.getMaskedImage().getVariance(),
182 exposure.getMaskedImage().getMask(),
183 afwMath.MEANCLIP, statsCtrl)
184 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
186 md = exposure.getMetadata()
187 if 'SFM_ASTROM_OFFSET_MEAN' in md:
188 astromOffsetMean = md['SFM_ASTROM_OFFSET_MEAN']
189 astromOffsetStd = md['SFM_ASTROM_OFFSET_STD']
190 else:
191 astromOffsetMean = np.nan
192 astromOffsetStd = np.nan
194 summary = afwImage.ExposureSummaryStats(psfSigma=float(psfSigma),
195 psfArea=float(psfArea),
196 psfIxx=float(psfIxx),
197 psfIyy=float(psfIyy),
198 psfIxy=float(psfIxy),
199 ra=float(ra),
200 decl=float(decl),
201 zenithDistance=float(zenithDistance),
202 zeroPoint=float(zeroPoint),
203 skyBg=float(skyBg),
204 skyNoise=float(skyNoise),
205 meanVar=float(meanVar),
206 raCorners=raCorners,
207 decCorners=decCorners,
208 astromOffsetMean=astromOffsetMean,
209 astromOffsetStd=astromOffsetStd)
211 return summary