Coverage for tests/test_PsfFlux.py: 18%
156 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-13 03:05 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-13 03:05 -0700
1# This file is part of meas_base.
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/>.
22import unittest
24import numpy as np
26import lsst.geom
27import lsst.afw.image
28import lsst.afw.table
29import lsst.utils.tests
31from lsst.meas.base.tests import (AlgorithmTestCase, FluxTransformTestCase,
32 SingleFramePluginTransformSetupHelper)
35def compute_chi2(exposure, centroid, instFlux, maskPlane=None):
36 """Return the chi2 for the exposure PSF at the given position.
38 Parameters
39 ----------
40 exposure : `lsst.afw.image.Exposure`
41 Exposure to calculate the PSF chi2 on.
42 centroid : `lsst::geom::Point2D`
43 Center of the source on the exposure to calculate the PSF at.
44 instFlux : `float`
45 Total flux of this source, as computed by the fitting algorithm.
46 maskPlane : `lsst.afw.image.Image`, optional
47 Pixels to mask out of the calculation.
49 Returns
50 -------
51 chi2, nPixels: `float`, `float`
52 The computed chi2 and number of pixels included in the calculation.
53 """
54 psfImage = exposure.getPsf().computeImage(centroid)
55 scaledPsf = psfImage.array*instFlux
56 # Get a sub-image the same size as the returned PSF image.
57 sub = exposure.Factory(exposure, psfImage.getBBox(), lsst.afw.image.LOCAL)
58 if maskPlane is not None:
59 unmasked = np.logical_not(sub.mask.array & sub.mask.getPlaneBitMask(maskPlane))
60 chi2 = np.sum((sub.image.array[unmasked] - scaledPsf[unmasked])**2
61 / sub.variance.array[unmasked])
62 nPixels = unmasked.sum()
63 else:
64 chi2 = np.sum((sub.image.array - scaledPsf)**2 / sub.variance.array)
65 nPixels = np.prod(sub.mask.array.shape)
66 return chi2, nPixels
69class PsfFluxTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
71 def setUp(self):
72 self.center = lsst.geom.Point2D(50.1, 49.8)
73 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
74 lsst.geom.Extent2I(100, 100))
75 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox)
76 self.dataset.addSource(100000.0, self.center)
78 def tearDown(self):
79 del self.center
80 del self.bbox
81 del self.dataset
83 def makeAlgorithm(self, ctrl=None):
84 """Construct an algorithm and return both it and its schema.
85 """
86 if ctrl is None:
87 ctrl = lsst.meas.base.PsfFluxControl()
88 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema()
89 algorithm = lsst.meas.base.PsfFluxAlgorithm(ctrl, "base_PsfFlux", schema)
90 return algorithm, schema
92 def testMasking(self):
93 algorithm, schema = self.makeAlgorithm()
94 # Results are RNG dependent; we choose a seed that is known to pass.
95 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=0)
96 record = catalog[0]
97 badPoint = lsst.geom.Point2I(self.center) + lsst.geom.Extent2I(3, 4)
98 imageArray = exposure.getMaskedImage().getImage().getArray()
99 maskArray = exposure.getMaskedImage().getMask().getArray()
100 badMask = exposure.getMaskedImage().getMask().getPlaneBitMask("BAD")
101 imageArray[badPoint.getY() - exposure.getY0(), badPoint.getX() - exposure.getX0()] = np.inf
102 maskArray[badPoint.getY() - exposure.getY0(), badPoint.getX() - exposure.getX0()] |= badMask
103 # Should get an infinite value exception, because we didn't mask that
104 # one pixel
105 with self.assertRaises(lsst.meas.base.PixelValueError):
106 algorithm.measure(record, exposure)
107 # If we do mask it, we should get a reasonable result
108 ctrl = lsst.meas.base.PsfFluxControl()
109 ctrl.badMaskPlanes = ["BAD"]
110 algorithm, schema = self.makeAlgorithm(ctrl)
111 algorithm.measure(record, exposure)
112 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"),
113 record.get("truth_instFlux"),
114 atol=3*record.get("base_PsfFlux_instFluxErr"))
115 chi2, nPixels = compute_chi2(exposure, record.getCentroid(),
116 record.get("base_PsfFlux_instFlux"),
117 "BAD")
118 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_chi2"), chi2, rtol=1e-7)
119 # If we mask the whole image, we should get a failure flag.
120 maskArray[:, :] |= badMask
121 algorithm.measure(record, exposure)
122 self.assertEqual(record.get("base_PsfFlux_flag_noGoodPixels"), 1)
123 self.assertEqual(record.get("base_PsfFlux_npixels"), nPixels)
125 def testSubImage(self):
126 """Test measurement on sub-images.
128 Specifically, checks that we don't get confused by images with nonzero
129 ``xy0``, and that the ``EDGE`` flag is set when it should be.
130 """
132 algorithm, schema = self.makeAlgorithm()
133 # Results are RNG dependent; we choose a seed that is known to pass.
134 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=1)
135 record = catalog[0]
136 psfImage = exposure.getPsf().computeImage(record.getCentroid())
137 bbox = psfImage.getBBox()
138 bbox.grow(-1)
139 subExposure = exposure.Factory(exposure, bbox, lsst.afw.image.LOCAL)
141 algorithm.measure(record, subExposure)
142 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), record.get("truth_instFlux"),
143 atol=3*record.get("base_PsfFlux_instFluxErr"))
144 self.assertTrue(record.get("base_PsfFlux_flag_edge"))
146 # Calculating chi2 requires trimming the PSF image by one pixel per side
147 # to match the subExposure created above, so we can't use compute_chi2()
148 # directly here.
149 scaledPsf = psfImage.array[1:-1, 1:-1]*record.get("base_PsfFlux_instFlux")
150 chi2 = np.sum((subExposure.image.array - scaledPsf)**2 / subExposure.variance.array)
151 nPixels = np.prod(subExposure.image.array.shape)
152 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_chi2"), chi2, rtol=1e-7)
153 self.assertEqual(record.get("base_PsfFlux_npixels"), nPixels)
155 def testNoPsf(self):
156 """Test that we raise `FatalAlgorithmError` when there's no PSF.
157 """
158 algorithm, schema = self.makeAlgorithm()
159 # Results are RNG dependent; we choose a seed that is known to pass.
160 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=2)
161 exposure.setPsf(None)
162 with self.assertRaises(lsst.meas.base.FatalAlgorithmError):
163 algorithm.measure(catalog[0], exposure)
165 def testMonteCarlo(self):
166 """Test an ideal simulation, with no noise.
168 Demonstrate that:
170 - We get exactly the right answer, and
171 - The reported uncertainty agrees with a Monte Carlo test of the noise.
172 """
173 algorithm, schema = self.makeAlgorithm()
174 # Results are RNG dependent; we choose a seed that is known to pass.
175 exposure, catalog = self.dataset.realize(0.0, schema, randomSeed=3)
176 record = catalog[0]
177 instFlux = record.get("truth_instFlux")
178 algorithm.measure(record, exposure)
179 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), instFlux, rtol=1E-3)
180 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFluxErr"), 0.0, rtol=1E-3)
181 # no noise, so infinite chi2 on this one
182 self.assertEqual(record.get("base_PsfFlux_chi2"), np.inf)
183 for noise in (0.001, 0.01, 0.1):
184 nSamples = 1000
185 instFluxes = np.zeros(nSamples, dtype=np.float64)
186 instFluxErrs = np.zeros(nSamples, dtype=np.float64)
187 expectedChi2s = np.zeros(nSamples, dtype=np.float64)
188 expectedNPixels = -100
189 measuredChi2s = np.zeros(nSamples, dtype=np.float64)
190 measuredNPixels = np.zeros(nSamples, dtype=np.float64)
191 for i in range(nSamples):
192 # By using ``i`` to seed the RNG, we get results which
193 # fall within the tolerances defined below. If we allow this
194 # test to be truly random, passing becomes RNG-dependent.
195 exposure, catalog = self.dataset.realize(noise*instFlux, schema, randomSeed=i)
196 record = catalog[0]
197 algorithm.measure(record, exposure)
198 instFluxes[i] = record.get("base_PsfFlux_instFlux")
199 instFluxErrs[i] = record.get("base_PsfFlux_instFluxErr")
200 measuredChi2s[i] = record.get("base_PsfFlux_chi2")
201 measuredNPixels[i] = record.get("base_PsfFlux_npixels")
202 chi2, nPixels = compute_chi2(exposure, record.getCentroid(), instFluxes[i])
203 expectedChi2s[i] = chi2
204 expectedNPixels = nPixels # should be the same each time
205 instFluxMean = np.mean(instFluxes)
206 instFluxErrMean = np.mean(instFluxErrs)
207 instFluxStandardDeviation = np.std(instFluxes)
208 self.assertFloatsAlmostEqual(instFluxErrMean, instFluxStandardDeviation, rtol=0.10)
209 self.assertLess(abs(instFluxMean - instFlux), 2.0*instFluxErrMean / nSamples**0.5)
210 self.assertFloatsAlmostEqual(measuredChi2s, expectedChi2s, rtol=1e-7)
211 # Should have the exact same number of pixels used every time.
212 self.assertTrue(np.all(measuredNPixels == expectedNPixels))
214 def testSingleFramePlugin(self):
215 task = self.makeSingleFrameMeasurementTask("base_PsfFlux")
216 # Results are RNG dependent; we choose a seed that is known to pass.
217 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=4)
218 task.run(catalog, exposure)
219 record = catalog[0]
220 self.assertFalse(record.get("base_PsfFlux_flag"))
221 self.assertFalse(record.get("base_PsfFlux_flag_noGoodPixels"))
222 self.assertFalse(record.get("base_PsfFlux_flag_edge"))
223 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), record.get("truth_instFlux"),
224 atol=3*record.get("base_PsfFlux_instFluxErr"))
226 def testForcedPlugin(self):
227 task = self.makeForcedMeasurementTask("base_PsfFlux")
228 # Results of this test are RNG dependent: we choose seeds that are
229 # known to pass.
230 measWcs = self.dataset.makePerturbedWcs(self.dataset.exposure.getWcs(), randomSeed=5)
231 measDataset = self.dataset.transform(measWcs)
232 exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=5)
233 refCat = self.dataset.catalog
234 refWcs = self.dataset.exposure.getWcs()
235 measCat = task.generateMeasCat(exposure, refCat, refWcs)
236 task.attachTransformedFootprints(measCat, refCat, exposure, refWcs)
237 task.run(measCat, exposure, refCat, refWcs)
238 measRecord = measCat[0]
239 truthRecord = truthCatalog[0]
240 # Centroid tolerances set to ~ single precision epsilon
241 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_x"),
242 truthRecord.get("truth_x"), rtol=1E-7)
243 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_y"),
244 truthRecord.get("truth_y"), rtol=1E-7)
245 self.assertFalse(measRecord.get("base_PsfFlux_flag"))
246 self.assertFalse(measRecord.get("base_PsfFlux_flag_noGoodPixels"))
247 self.assertFalse(measRecord.get("base_PsfFlux_flag_edge"))
248 self.assertFloatsAlmostEqual(measRecord.get("base_PsfFlux_instFlux"),
249 truthCatalog.get("truth_instFlux"), rtol=1E-3)
250 self.assertLess(measRecord.get("base_PsfFlux_instFluxErr"), 500.0)
253class PsfFluxTransformTestCase(FluxTransformTestCase, SingleFramePluginTransformSetupHelper,
254 lsst.utils.tests.TestCase):
255 controlClass = lsst.meas.base.PsfFluxControl
256 algorithmClass = lsst.meas.base.PsfFluxAlgorithm
257 transformClass = lsst.meas.base.PsfFluxTransform
258 flagNames = ('flag', 'flag_noGoodPixels', 'flag_edge')
259 singleFramePlugins = ('base_PsfFlux',)
260 forcedPlugins = ('base_PsfFlux',)
263class TestMemory(lsst.utils.tests.MemoryTestCase):
264 pass
267def setup_module(module):
268 lsst.utils.tests.init()
271if __name__ == "__main__": 271 ↛ 272line 271 didn't jump to line 272, because the condition on line 271 was never true
272 lsst.utils.tests.init()
273 unittest.main()