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

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
35__all__ = ("ComputeExposureSummaryStatsTask", "ComputeExposureSummaryStatsConfig")
38class ComputeExposureSummaryStatsConfig(pexConfig.Config):
39 """Config for ComputeExposureSummaryTask"""
40 sigmaClip = pexConfig.Field(
41 dtype=float,
42 doc="Sigma for outlier rejection for sky noise.",
43 default=3.0,
44 )
45 clipIter = pexConfig.Field(
46 dtype=int,
47 doc="Number of iterations of outlier rejection for sky noise.",
48 default=2,
49 )
50 badMaskPlanes = pexConfig.ListField(
51 dtype=str,
52 doc="Mask planes that, if set, the associated pixel should not be included sky noise calculation.",
53 default=("NO_DATA", "SUSPECT"),
54 )
57class ComputeExposureSummaryStatsTask(pipeBase.Task):
58 """Task to compute exposure summary statistics.
60 This task computes various quantities suitable for DPDD and other
61 downstream processing at the detector centers, including:
62 - psfSigma
63 - psfArea
64 - psfIxx
65 - psfIyy
66 - psfIxy
67 - ra
68 - decl
69 - zenithDistance
70 - zeroPoint
71 - skyBg
72 - skyNoise
73 - meanVar
74 - raCorners
75 - decCorners
76 - astromOffsetMean
77 - astromOffsetStd
78 """
79 ConfigClass = ComputeExposureSummaryStatsConfig
80 _DefaultName = "computeExposureSummaryStats"
82 @pipeBase.timeMethod
83 def run(self, exposure, sources, background):
84 """Measure exposure statistics from the exposure, sources, and background.
86 Parameters
87 ----------
88 exposure : `lsst.afw.image.ExposureF`
89 sources : `lsst.afw.table.SourceCatalog`
90 background : `lsst.afw.math.BackgroundList`
92 Returns
93 -------
94 summary : `lsst.afw.image.ExposureSummary`
95 """
96 self.log.info("Measuring exposure statistics")
98 bbox = exposure.getBBox()
100 psf = exposure.getPsf()
101 if psf is not None:
102 shape = psf.computeShape(bbox.getCenter())
103 psfSigma = shape.getDeterminantRadius()
104 psfIxx = shape.getIxx()
105 psfIyy = shape.getIyy()
106 psfIxy = shape.getIxy()
107 im = psf.computeKernelImage(bbox.getCenter())
108 # The calculation of effective psf area is taken from
109 # meas_base/src/PsfFlux.cc#L112. See
110 # https://github.com/lsst/meas_base/blob/
111 # 750bffe6620e565bda731add1509507f5c40c8bb/src/PsfFlux.cc#L112
112 psfArea = np.sum(im.array)/np.sum(im.array**2.)
113 else:
114 psfSigma = np.nan
115 psfIxx = np.nan
116 psfIyy = np.nan
117 psfIxy = np.nan
118 psfArea = np.nan
120 wcs = exposure.getWcs()
121 if wcs is not None:
122 sph_pts = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners())
123 raCorners = [float(sph.getRa().asDegrees()) for sph in sph_pts]
124 decCorners = [float(sph.getDec().asDegrees()) for sph in sph_pts]
126 sph_pt = wcs.pixelToSky(bbox.getCenter())
127 ra = sph_pt.getRa().asDegrees()
128 decl = sph_pt.getDec().asDegrees()
129 else:
130 raCorners = [float(np.nan)]*4
131 decCorners = [float(np.nan)]*4
132 ra = np.nan
133 decl = np.nan
135 photoCalib = exposure.getPhotoCalib()
136 if photoCalib is not None:
137 zeroPoint = 2.5*np.log10(photoCalib.getInstFluxAtZeroMagnitude())
138 else:
139 zeroPoint = np.nan
141 visitInfo = exposure.getInfo().getVisitInfo()
142 date = visitInfo.getDate()
144 if date.isValid():
145 # We compute the zenithDistance at the center of the detector rather
146 # than use the boresight value available via the visitInfo, because
147 # the zenithDistance may vary significantly over a large field of view.
148 observatory = visitInfo.getObservatory()
149 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
150 lon=observatory.getLongitude().asDegrees()*units.deg,
151 height=observatory.getElevation()*units.m)
152 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
153 location=loc, format='mjd')
154 coord = SkyCoord(ra*units.degree, decl*units.degree, obstime=obstime, location=loc)
155 with warnings.catch_warnings():
156 warnings.simplefilter('ignore')
157 altaz = coord.transform_to(AltAz)
159 zenithDistance = 90.0 - altaz.alt.degree
160 else:
161 zenithDistance = np.nan
163 if background is not None:
164 bgStats = (bg[0].getStatsImage().getImage().array
165 for bg in background)
166 skyBg = sum(np.median(bg[np.isfinite(bg)]) for bg in bgStats)
167 else:
168 skyBg = np.nan
170 statsCtrl = afwMath.StatisticsControl()
171 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
172 statsCtrl.setNumIter(self.config.clipIter)
173 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
174 statsCtrl.setNanSafe(True)
176 statObj = afwMath.makeStatistics(exposure.getMaskedImage(), afwMath.STDEVCLIP,
177 statsCtrl)
178 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
180 statObj = afwMath.makeStatistics(exposure.getMaskedImage().getVariance(),
181 exposure.getMaskedImage().getMask(),
182 afwMath.MEANCLIP, statsCtrl)
183 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
185 md = exposure.getMetadata()
186 if 'SFM_ASTROM_OFFSET_MEAN' in md:
187 astromOffsetMean = md['SFM_ASTROM_OFFSET_MEAN']
188 astromOffsetStd = md['SFM_ASTROM_OFFSET_STD']
189 else:
190 astromOffsetMean = np.nan
191 astromOffsetStd = np.nan
193 summary = afwImage.ExposureSummaryStats(psfSigma=float(psfSigma),
194 psfArea=float(psfArea),
195 psfIxx=float(psfIxx),
196 psfIyy=float(psfIyy),
197 psfIxy=float(psfIxy),
198 ra=float(ra),
199 decl=float(decl),
200 zenithDistance=float(zenithDistance),
201 zeroPoint=float(zeroPoint),
202 skyBg=float(skyBg),
203 skyNoise=float(skyNoise),
204 meanVar=float(meanVar),
205 raCorners=raCorners,
206 decCorners=decCorners,
207 astromOffsetMean=astromOffsetMean,
208 astromOffsetStd=astromOffsetStd)
210 return summary