Coverage for tests/test_PsfFlux.py: 20%
157 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-17 09:57 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-17 09:57 +0000
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 MeasurementError
120 maskArray[:, :] |= badMask
121 with self.assertRaises(lsst.meas.base.MeasurementError) as context:
122 algorithm.measure(record, exposure)
123 self.assertEqual(context.exception.getFlagBit(),
124 lsst.meas.base.PsfFluxAlgorithm.NO_GOOD_PIXELS.number)
125 self.assertEqual(record.get("base_PsfFlux_npixels"), nPixels)
127 def testSubImage(self):
128 """Test measurement on sub-images.
130 Specifically, checks that we don't get confused by images with nonzero
131 ``xy0``, and that the ``EDGE`` flag is set when it should be.
132 """
134 algorithm, schema = self.makeAlgorithm()
135 # Results are RNG dependent; we choose a seed that is known to pass.
136 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=1)
137 record = catalog[0]
138 psfImage = exposure.getPsf().computeImage(record.getCentroid())
139 bbox = psfImage.getBBox()
140 bbox.grow(-1)
141 subExposure = exposure.Factory(exposure, bbox, lsst.afw.image.LOCAL)
143 algorithm.measure(record, subExposure)
144 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), record.get("truth_instFlux"),
145 atol=3*record.get("base_PsfFlux_instFluxErr"))
146 self.assertTrue(record.get("base_PsfFlux_flag_edge"))
148 # Calculating chi2 requires trimming the PSF image by one pixel per side
149 # to match the subExposure created above, so we can't use compute_chi2()
150 # directly here.
151 scaledPsf = psfImage.array[1:-1, 1:-1]*record.get("base_PsfFlux_instFlux")
152 chi2 = np.sum((subExposure.image.array - scaledPsf)**2 / subExposure.variance.array)
153 nPixels = np.prod(subExposure.image.array.shape)
154 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_chi2"), chi2, rtol=1e-7)
155 self.assertEqual(record.get("base_PsfFlux_npixels"), nPixels)
157 def testNoPsf(self):
158 """Test that we raise `FatalAlgorithmError` when there's no PSF.
159 """
160 algorithm, schema = self.makeAlgorithm()
161 # Results are RNG dependent; we choose a seed that is known to pass.
162 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=2)
163 exposure.setPsf(None)
164 with self.assertRaises(lsst.meas.base.FatalAlgorithmError):
165 algorithm.measure(catalog[0], exposure)
167 def testMonteCarlo(self):
168 """Test an ideal simulation, with no noise.
170 Demonstrate that:
172 - We get exactly the right answer, and
173 - The reported uncertainty agrees with a Monte Carlo test of the noise.
174 """
175 algorithm, schema = self.makeAlgorithm()
176 # Results are RNG dependent; we choose a seed that is known to pass.
177 exposure, catalog = self.dataset.realize(0.0, schema, randomSeed=3)
178 record = catalog[0]
179 instFlux = record.get("truth_instFlux")
180 algorithm.measure(record, exposure)
181 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), instFlux, rtol=1E-3)
182 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFluxErr"), 0.0, rtol=1E-3)
183 # no noise, so infinite chi2 on this one
184 self.assertEqual(record.get("base_PsfFlux_chi2"), np.inf)
185 for noise in (0.001, 0.01, 0.1):
186 nSamples = 1000
187 instFluxes = np.zeros(nSamples, dtype=np.float64)
188 instFluxErrs = np.zeros(nSamples, dtype=np.float64)
189 expectedChi2s = np.zeros(nSamples, dtype=np.float64)
190 expectedNPixels = -100
191 measuredChi2s = np.zeros(nSamples, dtype=np.float64)
192 measuredNPixels = np.zeros(nSamples, dtype=np.float64)
193 for i in range(nSamples):
194 # By using ``i`` to seed the RNG, we get results which
195 # fall within the tolerances defined below. If we allow this
196 # test to be truly random, passing becomes RNG-dependent.
197 exposure, catalog = self.dataset.realize(noise*instFlux, schema, randomSeed=i)
198 record = catalog[0]
199 algorithm.measure(record, exposure)
200 instFluxes[i] = record.get("base_PsfFlux_instFlux")
201 instFluxErrs[i] = record.get("base_PsfFlux_instFluxErr")
202 measuredChi2s[i] = record.get("base_PsfFlux_chi2")
203 measuredNPixels[i] = record.get("base_PsfFlux_npixels")
204 chi2, nPixels = compute_chi2(exposure, record.getCentroid(), instFluxes[i])
205 expectedChi2s[i] = chi2
206 expectedNPixels = nPixels # should be the same each time
207 instFluxMean = np.mean(instFluxes)
208 instFluxErrMean = np.mean(instFluxErrs)
209 instFluxStandardDeviation = np.std(instFluxes)
210 self.assertFloatsAlmostEqual(instFluxErrMean, instFluxStandardDeviation, rtol=0.10)
211 self.assertLess(abs(instFluxMean - instFlux), 2.0*instFluxErrMean / nSamples**0.5)
212 self.assertFloatsAlmostEqual(measuredChi2s, expectedChi2s, rtol=1e-7)
213 # Should have the exact same number of pixels used every time.
214 self.assertTrue(np.all(measuredNPixels == expectedNPixels))
216 def testSingleFramePlugin(self):
217 task = self.makeSingleFrameMeasurementTask("base_PsfFlux")
218 # Results are RNG dependent; we choose a seed that is known to pass.
219 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=4)
220 task.run(catalog, exposure)
221 record = catalog[0]
222 self.assertFalse(record.get("base_PsfFlux_flag"))
223 self.assertFalse(record.get("base_PsfFlux_flag_noGoodPixels"))
224 self.assertFalse(record.get("base_PsfFlux_flag_edge"))
225 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), record.get("truth_instFlux"),
226 atol=3*record.get("base_PsfFlux_instFluxErr"))
228 def testForcedPlugin(self):
229 task = self.makeForcedMeasurementTask("base_PsfFlux")
230 # Results of this test are RNG dependent: we choose seeds that are
231 # known to pass.
232 measWcs = self.dataset.makePerturbedWcs(self.dataset.exposure.getWcs(), randomSeed=5)
233 measDataset = self.dataset.transform(measWcs)
234 exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=5)
235 refCat = self.dataset.catalog
236 refWcs = self.dataset.exposure.getWcs()
237 measCat = task.generateMeasCat(exposure, refCat, refWcs)
238 task.attachTransformedFootprints(measCat, refCat, exposure, refWcs)
239 task.run(measCat, exposure, refCat, refWcs)
240 measRecord = measCat[0]
241 truthRecord = truthCatalog[0]
242 # Centroid tolerances set to ~ single precision epsilon
243 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_x"),
244 truthRecord.get("truth_x"), rtol=1E-7)
245 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_y"),
246 truthRecord.get("truth_y"), rtol=1E-7)
247 self.assertFalse(measRecord.get("base_PsfFlux_flag"))
248 self.assertFalse(measRecord.get("base_PsfFlux_flag_noGoodPixels"))
249 self.assertFalse(measRecord.get("base_PsfFlux_flag_edge"))
250 self.assertFloatsAlmostEqual(measRecord.get("base_PsfFlux_instFlux"),
251 truthCatalog.get("truth_instFlux"), rtol=1E-3)
252 self.assertLess(measRecord.get("base_PsfFlux_instFluxErr"), 500.0)
255class PsfFluxTransformTestCase(FluxTransformTestCase, SingleFramePluginTransformSetupHelper,
256 lsst.utils.tests.TestCase):
257 controlClass = lsst.meas.base.PsfFluxControl
258 algorithmClass = lsst.meas.base.PsfFluxAlgorithm
259 transformClass = lsst.meas.base.PsfFluxTransform
260 flagNames = ('flag', 'flag_noGoodPixels', 'flag_edge')
261 singleFramePlugins = ('base_PsfFlux',)
262 forcedPlugins = ('base_PsfFlux',)
265class TestMemory(lsst.utils.tests.MemoryTestCase):
266 pass
269def setup_module(module):
270 lsst.utils.tests.init()
273if __name__ == "__main__": 273 ↛ 274line 273 didn't jump to line 274, because the condition on line 273 was never true
274 lsst.utils.tests.init()
275 unittest.main()