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

Shortcuts 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

86 statements  

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 

27 

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 

33 

34 

35__all__ = ("ComputeExposureSummaryStatsTask", "ComputeExposureSummaryStatsConfig") 

36 

37 

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 ) 

55 

56 

57class ComputeExposureSummaryStatsTask(pipeBase.Task): 

58 """Task to compute exposure summary statistics. 

59 

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" 

81 

82 @pipeBase.timeMethod 

83 def run(self, exposure, sources, background): 

84 """Measure exposure statistics from the exposure, sources, and background. 

85 

86 Parameters 

87 ---------- 

88 exposure : `lsst.afw.image.ExposureF` 

89 sources : `lsst.afw.table.SourceCatalog` 

90 background : `lsst.afw.math.BackgroundList` 

91 

92 Returns 

93 ------- 

94 summary : `lsst.afw.image.ExposureSummary` 

95 """ 

96 self.log.info("Measuring exposure statistics") 

97 

98 bbox = exposure.getBBox() 

99 

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 

119 

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] 

125 

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 

134 

135 photoCalib = exposure.getPhotoCalib() 

136 if photoCalib is not None: 

137 zeroPoint = 2.5*np.log10(photoCalib.getInstFluxAtZeroMagnitude()) 

138 else: 

139 zeroPoint = np.nan 

140 

141 visitInfo = exposure.getInfo().getVisitInfo() 

142 date = visitInfo.getDate() 

143 

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) 

158 

159 zenithDistance = 90.0 - altaz.alt.degree 

160 else: 

161 zenithDistance = np.nan 

162 

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 

169 

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) 

175 

176 statObj = afwMath.makeStatistics(exposure.getMaskedImage(), afwMath.STDEVCLIP, 

177 statsCtrl) 

178 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP) 

179 

180 statObj = afwMath.makeStatistics(exposure.getMaskedImage().getVariance(), 

181 exposure.getMaskedImage().getMask(), 

182 afwMath.MEANCLIP, statsCtrl) 

183 meanVar, _ = statObj.getResult(afwMath.MEANCLIP) 

184 

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 

192 

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) 

209 

210 return summary