Coverage for tests / test_computeExposureSummaryStats.py: 16%
164 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 09:21 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 09:21 +0000
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/>.
22"""Test ComputeExposureSummaryStatsTask.
23"""
24import unittest
26import numpy as np
28import lsst.utils.tests
29from lsst.afw.detection import GaussianPsf
30import lsst.afw.image as afwImage
31import lsst.afw.math as afwMath
32from lsst.daf.base import DateTime, PropertyList
33from lsst.afw.coord import Observatory
34from lsst.afw.geom import makeCdMatrix, makeSkyWcs
35from lsst.afw.cameraGeom.testUtils import DetectorWrapper
36from lsst.pipe.tasks.computeExposureSummaryStats import ComputeExposureSummaryStatsTask
37from lsst.pipe.tasks.computeExposureSummaryStats import ComputeExposureSummaryStatsConfig
38from lsst.pipe.tasks.computeExposureSummaryStats import compute_magnitude_limit
39from lsst.pipe.tasks.computeExposureSummaryStats import compute_effective_time
41# Values from syseng_throughputs notebook assuming 30s exposure
42# consisting of 2x15s snaps each with readnoise of 9e-
43fwhm_eff_fid = {'g': 0.87, 'r': 0.83, 'i': 0.80}
44skycounts_fid = {'g': 463.634122, 'r': 988.626863, 'i': 1588.280513}
45zeropoint_fid = {'g': 28.508375, 'r': 28.360838, 'i': 28.171396}
46readnoise_fid = {'g': 12.73, 'r': 12.73, 'i': 12.73}
47# Output magnitude limit from syseng_throughputs notebook
48m5_fid = {'g': 24.90, 'r': 24.48, 'i': 24.10}
51class ComputeExposureSummaryTestCase(lsst.utils.tests.TestCase):
53 def testComputeExposureSummary(self):
54 """Make a fake exposure and background and compute summary.
55 """
56 np.random.seed(12345)
58 # Make an exposure with a noise image
59 exposure = afwImage.ExposureF(100, 100)
61 band = "i"
62 physical_filter = "test-i"
63 exposure.setFilter(afwImage.FilterLabel(band=band, physical=physical_filter))
65 readNoise = 5.0
66 gain = 1.0
67 detector = DetectorWrapper(numAmps=1).detector
68 metadata = PropertyList()
69 metadata.add("LSST ISR UNITS", "electron")
70 for amp in detector.getAmplifiers():
71 metadata.add(f"LSST ISR READNOISE {amp.getName()}", readNoise)
72 metadata.add(f"LSST ISR GAIN {amp.getName()}", gain)
73 exposure.setDetector(detector)
74 exposure.setMetadata(metadata)
76 skyMean = 100.0
77 skySigma = 10.0
78 exposure.getImage().getArray()[:, :] = np.random.normal(skyMean, skySigma, size=(100, 100))
79 exposure.getVariance().getArray()[:, :] = skySigma**2.
81 # Set the visitInfo
82 date = DateTime(date=59234.7083333334, system=DateTime.DateSystem.MJD)
83 observatory = Observatory(-70.7366*lsst.geom.degrees, -30.2407*lsst.geom.degrees,
84 2650.)
85 expTime = 10.0
86 visitInfo = afwImage.VisitInfo(exposureTime=expTime,
87 date=date,
88 observatory=observatory)
89 exposure.getInfo().setVisitInfo(visitInfo)
91 # Install a Gaussian PSF
92 psfSize = 2.0
93 psf = GaussianPsf(5, 5, psfSize)
94 exposure.setPsf(psf)
96 # Install a simple WCS
97 scale = 0.2*lsst.geom.arcseconds
98 raCenter = 300.0*lsst.geom.degrees
99 decCenter = 0.0*lsst.geom.degrees
100 cdMatrix = makeCdMatrix(scale=scale)
101 skyWcs = makeSkyWcs(crpix=exposure.getBBox().getCenter(),
102 crval=lsst.geom.SpherePoint(raCenter, decCenter),
103 cdMatrix=cdMatrix)
104 pixelScale = skyWcs.getPixelScale().asArcseconds()
105 exposure.setWcs(skyWcs)
107 # Install a simple photoCalib
108 photoCalib = afwImage.PhotoCalib(calibrationMean=0.3)
109 zp = 2.5*np.log10(photoCalib.getInstFluxAtZeroMagnitude())
110 exposure.setPhotoCalib(photoCalib)
112 # Install a simple apCorrMap
113 apCorrMap = afwImage.ApCorrMap()
114 apCorrMap.set(
115 "base_PsfFlux_instFlux", afwMath.ChebyshevBoundedField(exposure.getBBox(), np.zeros((3, 3)))
116 )
117 exposure.setApCorrMap(apCorrMap)
119 # Compute the background image
120 bgGridSize = 10
121 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.NATURAL_SPLINE)
122 bctrl.setNxSample(int(exposure.getMaskedImage().getWidth()/bgGridSize) + 1)
123 bctrl.setNySample(int(exposure.getMaskedImage().getHeight()/bgGridSize) + 1)
124 backobj = afwMath.makeBackground(exposure.getMaskedImage().getImage(), bctrl)
125 background = afwMath.BackgroundList()
126 background.append(backobj)
128 # Configure and run the task
129 expSummaryTask = ComputeExposureSummaryStatsTask()
130 config = expSummaryTask.config
132 # Configure nominal values for effective time calculation (normalized to 1s exposure)
133 config.fiducialZeroPoint = {band: float(zp - 2.5*np.log10(expTime))}
134 config.fiducialPsfSigma = {band: float(psfSize)}
135 config.fiducialSkyBackground = {band: float(skyMean/expTime)}
136 config.fiducialReadNoise = {band: float(readNoise)}
137 config.fiducialExpTime = {band: expTime}
138 config.fiducialMagLim = {band: 26.584}
140 # Run the task
141 summary = expSummaryTask.run(exposure, None, background)
143 # Test the outputs
144 self.assertFloatsAlmostEqual(summary.psfSigma, psfSize)
145 self.assertFloatsAlmostEqual(summary.psfIxx, psfSize**2.)
146 self.assertFloatsAlmostEqual(summary.psfIyy, psfSize**2.)
147 self.assertFloatsAlmostEqual(summary.psfIxy, 0.0)
148 self.assertFloatsAlmostEqual(summary.psfArea, 23.088975164455444)
150 self.assertFloatsAlmostEqual(summary.psfTraceRadiusDelta, 0.0)
151 self.assertFloatsAlmostEqual(summary.psfApFluxDelta, 0.0)
152 self.assertFloatsAlmostEqual(summary.psfApCorrSigmaScaledDelta, 0.0)
153 nan = float("nan")
154 self.assertFloatsAlmostEqual(summary.psfStarDeltaE1Median, nan, ignoreNaNs=True)
155 self.assertFloatsAlmostEqual(summary.psfStarDeltaE2Median, nan, ignoreNaNs=True)
156 self.assertFloatsAlmostEqual(summary.psfStarDeltaE1Scatter, nan, ignoreNaNs=True)
157 self.assertFloatsAlmostEqual(summary.psfStarDeltaE2Scatter, nan, ignoreNaNs=True)
158 self.assertFloatsAlmostEqual(summary.psfStarDeltaSizeMedian, nan, ignoreNaNs=True)
159 self.assertFloatsAlmostEqual(summary.psfStarDeltaSizeScatter, nan, ignoreNaNs=True)
160 self.assertFloatsAlmostEqual(summary.psfStarScaledDeltaSizeScatter, nan, ignoreNaNs=True)
161 self.assertFloatsAlmostEqual(summary.maxDistToNearestPsf, nan, ignoreNaNs=True)
162 self.assertFloatsAlmostEqual(summary.starEMedian, nan, ignoreNaNs=True)
163 self.assertFloatsAlmostEqual(summary.starUnNormalizedEMedian, nan, ignoreNaNs=True)
165 delta = (scale*50).asDegrees()
166 for a, b in zip(summary.raCorners,
167 [raCenter.asDegrees() + delta, raCenter.asDegrees() - delta,
168 raCenter.asDegrees() - delta, raCenter.asDegrees() + delta]):
169 self.assertFloatsAlmostEqual(a, b, atol=1e-10)
170 for a, b in zip(summary.decCorners,
171 [decCenter.asDegrees() - delta, decCenter.asDegrees() - delta,
172 decCenter.asDegrees() + delta, decCenter.asDegrees() + delta]):
173 self.assertFloatsAlmostEqual(a, b, atol=1e-10)
175 self.assertFloatsAlmostEqual(summary.ra, raCenter.asDegrees(), atol=1e-10)
176 self.assertFloatsAlmostEqual(summary.dec, decCenter.asDegrees(), atol=1e-10)
178 self.assertFloatsAlmostEqual(summary.pixelScale, pixelScale)
180 self.assertFloatsAlmostEqual(summary.zeroPoint, zp)
181 self.assertFloatsAlmostEqual(summary.expTime, expTime)
183 # Need to compare background level and noise
184 # These are only approximately 0+/-10 because of the small image
185 self.assertFloatsAlmostEqual(summary.skyBg, skyMean, rtol=1e-3)
186 self.assertFloatsAlmostEqual(summary.meanVar, skySigma**2.)
188 self.assertFloatsAlmostEqual(summary.zenithDistance, 30.57112, atol=1e-5)
190 # Effective exposure time and depth
191 self.assertFloatsAlmostEqual(summary.magLim, config.fiducialMagLim[band], rtol=1e-3)
192 self.assertFloatsAlmostEqual(summary.effTime, expTime, rtol=1e-3)
194 def testComputeMagnitudeLimit(self):
195 """Test the magnitude limit calculation using fiducials from SMTN-002
196 and syseng_throughputs."""
198 # Assumed values from SMTN-002
199 snr = 5
200 gain = 1.0
201 # For testing non-unity gain
202 gain_test = 2.0
203 exposure_time = 30
204 pixel_scale = 0.2
206 for band in ['g', 'r', 'i']:
207 # Translate into DM quantities
208 psfArea = 2.266 * (fwhm_eff_fid[band] / pixel_scale)**2
209 skyBg = skycounts_fid[band]
210 zeroPoint = zeropoint_fid[band] + 2.5*np.log10(exposure_time)
211 readNoise = readnoise_fid[band]
213 # Calculate the M5 values
214 m5 = compute_magnitude_limit(psfArea, skyBg, zeroPoint, readNoise, gain, snr)
215 self.assertFloatsAlmostEqual(m5, m5_fid[band], atol=1e-2)
217 # Changing the gain consistently should not change M5
218 m5_gain = compute_magnitude_limit(psfArea, skyBg/gain_test,
219 zeroPoint - 2.5*np.log10(gain_test),
220 readNoise/gain_test, gain_test, snr)
221 self.assertFloatsAlmostEqual(m5_gain, m5, atol=1e-6)
223 # Calculate effective time values
224 eff_time = compute_effective_time(m5, m5_fid[band], exposure_time)
225 self.assertFloatsAlmostEqual(eff_time, exposure_time, rtol=1e-2)
226 eff_time_half = compute_effective_time(m5 - 0.375, m5_fid[band], exposure_time)
227 self.assertFloatsAlmostEqual(eff_time_half, exposure_time/2.0, rtol=1e-2)
229 # Check that input NaN lead to output NaN
230 nan = float('nan')
231 m5 = compute_magnitude_limit(nan, skyBg, zeroPoint, readNoise, gain, snr)
232 self.assertFloatsAlmostEqual(m5, nan, ignoreNaNs=True)
233 m5 = compute_magnitude_limit(psfArea, nan, zeroPoint, readNoise, gain, snr)
234 self.assertFloatsAlmostEqual(m5, nan, ignoreNaNs=True)
235 m5 = compute_magnitude_limit(psfArea, skyBg, nan, readNoise, gain, snr)
236 self.assertFloatsAlmostEqual(m5, nan, ignoreNaNs=True)
237 m5 = compute_magnitude_limit(psfArea, skyBg, zeroPoint, nan, gain, snr)
238 self.assertFloatsAlmostEqual(m5, nan, ignoreNaNs=True)
240 def testComputeFiducialMagnitudeLimit(self):
241 """Test the magnitude limit calculation using fiducials from SMTN-002
242 and syseng_throughputs."""
244 # Setup the config
245 config = ComputeExposureSummaryStatsConfig()
246 # Configure fiducial values.
247 # These come from obs_lsst/config/lsstCam
248 config.fiducialZeroPoint = {
249 "u": 26.52, "g": 28.51, "r": 28.36, "i": 28.17, "z": 27.78, "y": 26.82,
250 }
251 config.fiducialPsfSigma = {
252 "u": 1.72, "g": 1.63, "r": 1.56, "i": 1.51, "z": 1.47, "y": 1.44,
253 }
254 config.fiducialSkyBackground = {
255 "u": 1.51, "g": 15.45, "r": 32.95, "i": 52.94, "z": 79.40, "y": 90.57,
256 }
257 config.fiducialReadNoise = {
258 "u": 9.0, "g": 9.0, "r": 9.0, "i": 9.0, "z": 9.0, "y": 9.0,
259 }
260 config.fiducialExpTime = {
261 "u": 30.0, "g": 30.0, "r": 30.0, "i": 30.0, "z": 30.0, "y": 30.0,
262 }
264 # Other properties
265 pixelScale = 0.2 # arcsec/pix
266 gain = 1.0 # [e-/ADU]
268 # Fiducial m5 from SMTN-002
269 fiducialMagLim = {
270 "u": 23.70, "g": 24.97, "r": 24.52, "i": 24.13, "z": 23.56, "y": 22.55
271 }
273 # Compare fiducial m5 calculated from config to SMTN-002 values
274 for band in fiducialMagLim.keys():
275 print(f"\n{band}")
276 m5fid = config.fiducialMagnitudeLimit(band, pixelScale, gain)
277 print(f"{fiducialMagLim[band]:.2f} {m5fid:.3f}")
278 self.assertFloatsAlmostEqual(m5fid, fiducialMagLim[band], atol=1e-2)
280 # Check that passing an unknown band outputs NaN
281 nan = float('nan')
282 m5fid = config.fiducialMagnitudeLimit("block", pixelScale, gain)
283 self.assertFloatsAlmostEqual(m5fid, nan, ignoreNaNs=True)
286class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
287 pass
290def setup_module(module):
291 lsst.utils.tests.init()
294if __name__ == "__main__": 294 ↛ 295line 294 didn't jump to line 295 because the condition on line 294 was never true
295 lsst.utils.tests.init()
296 unittest.main()