Coverage for tests/test_CompensatedGaussianFlux.py: 11%
155 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 02:21 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 02:21 -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
23# import math
25import numpy as np
27import lsst.geom
28import lsst.afw.geom
29import lsst.meas.base
30import lsst.utils.tests
31from lsst.meas.base.tests import AlgorithmTestCase
32from lsst.meas.base._measBaseLib import _compensatedGaussianFiltInnerProduct
35class CompensatedGaussianFluxTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
36 def setUp(self):
37 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-20, -30),
38 lsst.geom.Extent2I(1000, 1000))
40 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox)
41 self.psf_size = 2.0
42 self.dataset.psfShape = lsst.afw.geom.Quadrupole(self.psf_size**2., self.psf_size**2., 0.0)
44 # We want a set of point sources at various flux levels.
45 self.dataset.addSource(10000.0, lsst.geom.Point2D(50.1, 49.8))
46 self.dataset.addSource(20000.0, lsst.geom.Point2D(100.5, 100.4))
47 self.dataset.addSource(30000.0, lsst.geom.Point2D(150.4, 149.6))
48 self.dataset.addSource(40000.0, lsst.geom.Point2D(200.2, 200.3))
49 self.dataset.addSource(50000.0, lsst.geom.Point2D(250.3, 250.1))
50 self.dataset.addSource(60000.0, lsst.geom.Point2D(300.4, 300.2))
51 self.dataset.addSource(70000.0, lsst.geom.Point2D(350.5, 350.6))
52 self.dataset.addSource(80000.0, lsst.geom.Point2D(400.6, 400.0))
53 self.dataset.addSource(90000.0, lsst.geom.Point2D(450.0, 450.0))
54 self.dataset.addSource(100000.0, lsst.geom.Point2D(500.7, 500.8))
56 # Small test for Monte Carlo
57 self.bbox_single = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
58 lsst.geom.Extent2I(101, 101))
59 self.dataset_single = lsst.meas.base.tests.TestDataset(self.bbox_single)
60 self.dataset_single.psfShape = self.dataset.psfShape
62 self.dataset_single.addSource(100000.0, lsst.geom.Point2D(50.0, 50.0))
64 self.all_widths = (2, 3, 5)
65 self.larger_widths = (3, 5)
66 self.all_ts = (1.5, 2.0)
68 def tearDown(self):
69 del self.bbox
70 del self.dataset
71 del self.bbox_single
72 del self.dataset_single
74 def makeAlgorithm(self, config=None):
75 """Construct an algorithm and return both it and its schema.
76 """
77 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema()
78 if config is None:
79 config = lsst.meas.base.SingleFrameCompensatedGaussianFluxConfig()
80 algorithm = lsst.meas.base.SingleFrameCompensatedGaussianFluxPlugin(
81 config,
82 "base_CompensatedGaussianFlux",
83 schema,
84 None
85 )
86 return algorithm, schema
88 def _calcNumpyCompensatedGaussian(self, arr, var_arr, x_cent, y_cent, width, t):
89 """Calculate the compensated Gaussian using numpy (for testing).
91 Parameters
92 ----------
93 arr : `np.ndarray`
94 Array of pixel values.
95 var_arr : `np.ndarray`
96 Array of variance values.
97 x_cent : `float`
98 x value of centroid.
99 y_cent : `float`
100 y value of centroid.
101 width : `float`
102 Width of inner kernel.
103 t : `float`
104 Scaling factor for outer kernel (outer_width = width*t).
106 Returns
107 -------
108 flux : `float`
109 variance : `float`
110 """
111 xx, yy = np.meshgrid(np.arange(arr.shape[0]), np.arange(arr.shape[1]))
112 # Compute the inner and outer normalized Gaussian weights.
113 inner = (1./(2.*np.pi*width**2.))*np.exp(-0.5*(((xx - x_cent)/width)**2. + ((yy - y_cent)/width)**2.))
114 outer = (1./(2.*np.pi*(t*width)**2.))*np.exp(-0.5*(((xx - x_cent)/(t*width))**2.
115 + ((yy - y_cent)/(t*width))**2.))
116 weight = inner - outer
118 # Compute the weighted sum of the pixels.
119 flux = np.sum(weight*arr)
120 # And the normalization term, derived in Lupton et al. (in prep).
121 flux *= 4.*np.pi*(width**2.)*(t**2. + 1)/(t**2. - 1)
123 # And compute the variance
124 variance = np.sum(weight*weight*var_arr)
125 variance /= np.sum(weight*weight)
127 # The variance normalization term, derived in Lupton et al. (in prep).
128 variance *= 4.*np.pi*(width**2.)*(t**2 + 1)/t**2.
130 return flux, variance
132 def testCompensatedGaussianInnerProduct(self):
133 """Test using the inner product routine directly."""
135 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
136 lsst.geom.Extent2I(101, 101))
137 dataset = lsst.meas.base.tests.TestDataset(bbox)
138 psf_size = 2.0
139 dataset.psfShape = lsst.afw.geom.Quadrupole(psf_size**2., psf_size**2., 0.0)
140 centroid = lsst.geom.Point2D(50.0, 50.0)
141 true_flux = 100000.0
142 dataset.addSource(true_flux, centroid)
144 # We need to set up the task in order to create the test dataset.
145 task = self.makeSingleFrameMeasurementTask("base_CompensatedGaussianFlux")
147 for width in self.all_widths:
148 for t in self.all_ts:
149 flux_0 = None
150 var_0 = None
151 for dc_offset in (0.0, -1.0, 1.0):
152 exposure, catalog = dataset.realize(10.0, task.schema, randomSeed=1000)
154 exposure.image.array += dc_offset
156 flux, var = _compensatedGaussianFiltInnerProduct(
157 exposure.image.array,
158 exposure.variance.array,
159 centroid.getX(),
160 centroid.getY(),
161 width,
162 t,
163 )
165 flux_numpy, var_numpy = self._calcNumpyCompensatedGaussian(
166 exposure.image.array,
167 exposure.variance.array,
168 centroid.getX(),
169 centroid.getY(),
170 width,
171 t,
172 )
174 # Compare values from c++ code to numpy code.
175 self.assertFloatsAlmostEqual(flux, flux_numpy, rtol=1e-6)
176 self.assertFloatsAlmostEqual(var, var_numpy, rtol=1e-6)
178 # If the kernel width is equal to the simulated PSF then it
179 # should be nearly unbiased.
180 if width == psf_size:
181 self.assertFloatsAlmostEqual(flux, true_flux, rtol=1e-3)
183 # And check biases with non-zero DC offset; these should be
184 # equal with some floating point tolerance.
185 if dc_offset == 0.0:
186 flux_0 = flux
187 var_0 = var
188 else:
189 self.assertFloatsAlmostEqual(flux, flux_0, rtol=1e-7)
190 self.assertFloatsAlmostEqual(var, var_0, rtol=1e-7)
192 def testCompensatedGaussianSubPixels(self):
193 """Test for correct instFlux as a function of sub-pixel position."""
194 np.random.seed(12345)
196 n_points = 100
198 x_sub = np.random.uniform(low=0.0, high=1.0, size=n_points)
199 y_sub = np.random.uniform(low=0.0, high=1.0, size=n_points)
201 # We need to set up the task in order to create the test dataset.
202 task = self.makeSingleFrameMeasurementTask("base_CompensatedGaussianFlux")
204 for width in self.all_widths:
205 for t in self.all_ts:
206 fluxes = np.zeros(n_points)
208 for i in range(n_points):
209 dataset = lsst.meas.base.tests.TestDataset(self.bbox_single)
210 centroid = lsst.geom.Point2D(50.0 + x_sub[i], 50.0 + y_sub[i])
211 dataset.addSource(50000.0, centroid)
213 exposure, catalog = dataset.realize(10.0, task.schema, randomSeed=i)
214 flux, var = _compensatedGaussianFiltInnerProduct(
215 exposure.image.array,
216 exposure.variance.array,
217 centroid.getX(),
218 centroid.getY(),
219 width,
220 t,
221 )
223 fluxes[i] = flux
225 # Check for no correlation with x_sub and y_sub.
226 fit_x = np.polyfit(x_sub, fluxes/50000.0, 1)
227 self.assertLess(np.abs(fit_x[0]), 1e-3)
228 fit_y = np.polyfit(y_sub, fluxes/50000.0, 1)
229 self.assertLess(np.abs(fit_y[0]), 1e-3)
231 def testCompensatedGaussianPlugin(self):
232 """Test for correct instFlux given known position and shape.
233 """
234 # In the z-band, HSC images have a noise of about 40.0 ADU, and a background
235 # offset of ~ -0.6 ADU/pixel. This determines our test levels.
236 for width in self.all_widths:
237 for t in self.all_ts:
238 for dc_offset in (0.0, -1.0, 1.0):
239 config = self.makeSingleFrameMeasurementConfig("base_CompensatedGaussianFlux")
240 config.algorithms["base_CompensatedGaussianFlux"].kernel_widths = [width]
241 config.algorithms["base_CompensatedGaussianFlux"].t = t
243 task = self.makeSingleFrameMeasurementTask(config=config)
244 exposure, catalog = self.dataset.realize(40.0, task.schema, randomSeed=0)
245 exposure.image.array += dc_offset
246 task.run(catalog, exposure)
248 filter_flux = catalog[f"base_CompensatedGaussianFlux_{width}_instFlux"]
249 filter_err = catalog[f"base_CompensatedGaussianFlux_{width}_instFluxErr"]
250 truth_flux = catalog["truth_instFlux"]
252 if width == self.psf_size:
253 # When the filter matches the PSF, we should get close to the true flux.
254 tol = np.sqrt((filter_err/filter_flux)**2. + 0.02**2.)
255 self.assertFloatsAlmostEqual(filter_flux, truth_flux, rtol=tol)
256 elif width > self.psf_size:
257 # When the filter is larger than the PSF, the filter flux will be
258 # greater than the truth flux.
259 np.testing.assert_array_less(truth_flux, filter_flux)
261 if dc_offset == 0.0:
262 # Use the no-offset run as a comparison for offset runs.
263 flux_0 = filter_flux
264 else:
265 # Note: this tolerance is determined empirically, but this is
266 # larger than preferable.
267 self.assertFloatsAlmostEqual(filter_flux, flux_0, rtol=5e-3)
269 # The ratio of the filter flux to the truth flux should be consistent.
270 # I'm not sure how to scale this with the error, so this is a loose
271 # tolerance now.
272 ratio = filter_flux / truth_flux
273 self.assertLess(np.std(ratio), 0.04)
275 def testMonteCarlo(self):
276 """Test an ideal simulation, with no noise.
278 Demonstrate that:
280 - We get exactly the right answer, and
281 - The reported uncertainty agrees with a Monte Carlo test of the noise.
282 """
283 nSamples = 500
285 for width in self.larger_widths:
286 for t in self.all_ts:
287 config = lsst.meas.base.SingleFrameCompensatedGaussianFluxConfig()
288 config.kernel_widths = [width]
289 config.t = t
291 algorithm, schema = self.makeAlgorithm(config=config)
293 # Make a noiseless catalog.
294 exposure, catalog = self.dataset_single.realize(1E-8, schema, randomSeed=1)
296 # Only use the high-flux source for the error tests.
297 record = catalog[0]
298 algorithm.measure(record, exposure)
299 inst_flux = record[f"base_CompensatedGaussianFlux_{width}_instFlux"]
301 for noise in (0.001, 0.01, 0.1):
302 fluxes = np.zeros(nSamples)
303 errs = np.zeros_like(fluxes)
305 for repeat in range(nSamples):
306 # By using ``repeat`` to seed the RNG, we get results which
307 # fall within the tolerances defined below. If we allow this
308 # test to be truly random, passing becomes RNG-dependent.
309 exposure_samp, catalog_samp = self.dataset_single.realize(
310 noise*inst_flux,
311 schema,
312 randomSeed=repeat,
313 )
314 record_samp = catalog_samp[0]
315 algorithm.measure(record_samp, exposure_samp)
316 fluxes[repeat] = record_samp[f"base_CompensatedGaussianFlux_{width}_instFlux"]
317 errs[repeat] = record_samp[f"base_CompensatedGaussianFlux_{width}_instFluxErr"]
319 err_mean = np.mean(errs)
320 flux_std = np.std(fluxes)
321 self.assertFloatsAlmostEqual(err_mean, flux_std, rtol=0.10)
324class TestMemory(lsst.utils.tests.MemoryTestCase):
325 pass
328def setup_module(module):
329 lsst.utils.tests.init()
332if __name__ == "__main__": 332 ↛ 333line 332 didn't jump to line 333, because the condition on line 332 was never true
333 lsst.utils.tests.init()
334 unittest.main()