Coverage for tests/test_dipoleFitter.py: 20%
117 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-28 04:49 -0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-28 04:49 -0700
1#
2# LSST Data Management System
3# Copyright 2008-2017 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
22"""Tests of the DipoleFitAlgorithm and its related tasks and plugins.
24Each test generates a fake image with two synthetic dipoles as input data.
25"""
26import unittest
28import numpy as np
30import lsst.utils.tests
31import lsst.afw.table as afwTable
32import lsst.meas.base as measBase
33from lsst.ip.diffim.dipoleFitTask import (DipoleFitAlgorithm, DipoleFitTask)
34import lsst.ip.diffim.utils as ipUtils
37class DipoleTestImage:
38 """Create a test dipole image and store the parameters used to make it,
39 for comparison with the fitted results.
41 Parameters
42 ----------
43 xc, yc : `list` [`float`]
44 x, y coordinate (pixels) of center(s) of input dipole(s).
45 flux: `list` [`float`]
46 Flux(es) of input dipole(s).
47 gradientParams : `tuple`
48 Tuple with three parameters for linear background gradient.
49 offsets : `list` [`float`]
50 Pixel coordinates between lobes of dipoles.
51 """
53 def __init__(self, xc=None, yc=None, flux=None, offsets=None, gradientParams=None):
54 self.xc = xc if xc is not None else [65.3, 24.2]
55 self.yc = yc if yc is not None else [38.6, 78.5]
56 self.offsets = offsets if offsets is not None else np.array([-2., 2.])
57 self.flux = flux if flux is not None else [2500., 2345.]
58 self.gradientParams = gradientParams if gradientParams is not None else [10., 3., 5.]
60 # The default tolerance for comparisons of fitted parameters with input values.
61 # Given the noise in the input images (default noise value of 2.), this is a
62 # useful test of algorithm robustness, and will guard against future regressions.
63 self.rtol = 0.01
65 self.generateTestImage()
67 def generateTestImage(self):
68 self.testImage = ipUtils.DipoleTestImage(
69 w=100, h=100,
70 xcenPos=self.xc + self.offsets,
71 ycenPos=self.yc + self.offsets,
72 xcenNeg=self.xc - self.offsets,
73 ycenNeg=self.yc - self.offsets,
74 flux=self.flux, fluxNeg=self.flux,
75 noise=2., # Note the input noise - this affects the relative tolerances used.
76 gradientParams=self.gradientParams)
79class DipoleFitTest(lsst.utils.tests.TestCase):
80 """A test case for separately testing the dipole fit algorithm
81 directly, and the single frame measurement.
83 In each test, create a simulated diffim with two dipoles, noise,
84 and a linear background gradient in the pre-sub images then
85 compare the input fluxes/centroids with the fitted results.
86 """
88 def testDipoleAlgorithm(self):
89 """Test the dipole fitting algorithm directly (fitDipole()).
91 Test that the resulting fluxes/centroids are very close to the
92 input values for both dipoles in the image.
93 """
94 # Display (plot) the output dipole thumbnails with matplotlib.
95 display = False
96 # Be verbose during fitting, including the lmfit internal details.
97 verbose = False
99 dipoleTestImage = DipoleTestImage()
100 catalog = dipoleTestImage.testImage.detectDipoleSources(minBinSize=32)
102 for s in catalog:
103 fp = s.getFootprint()
104 self.assertTrue(len(fp.getPeaks()) == 2)
106 rtol = dipoleTestImage.rtol
107 offsets = dipoleTestImage.offsets
108 testImage = dipoleTestImage.testImage
109 for i, s in enumerate(catalog):
110 alg = DipoleFitAlgorithm(testImage.diffim, testImage.posImage, testImage.negImage)
111 result, _ = alg.fitDipole(
112 s, rel_weight=0.5, separateNegParams=False,
113 verbose=verbose, display=display)
115 self.assertFloatsAlmostEqual((result.posFlux + abs(result.negFlux))/2.,
116 dipoleTestImage.flux[i], rtol=rtol)
117 self.assertFloatsAlmostEqual(result.posCentroidX, dipoleTestImage.xc[i] + offsets[i], rtol=rtol)
118 self.assertFloatsAlmostEqual(result.posCentroidY, dipoleTestImage.yc[i] + offsets[i], rtol=rtol)
119 self.assertFloatsAlmostEqual(result.negCentroidX, dipoleTestImage.xc[i] - offsets[i], rtol=rtol)
120 self.assertFloatsAlmostEqual(result.negCentroidY, dipoleTestImage.yc[i] - offsets[i], rtol=rtol)
122 def _runDetection(self, dipoleTestImage, maxFootprintArea=None):
123 """Run 'diaSource' detection on the diffim, including merging of
124 positive and negative sources.
126 Then run DipoleFitTask on the image and return the resulting catalog.
127 """
129 # Create the various tasks and schema -- avoid code reuse.
130 testImage = dipoleTestImage.testImage
131 detectTask, schema = testImage.detectDipoleSources(doMerge=False, minBinSize=32)
133 measureConfig = measBase.SingleFrameMeasurementConfig()
135 measureConfig.slots.calibFlux = None
136 measureConfig.slots.modelFlux = None
137 measureConfig.slots.gaussianFlux = None
138 measureConfig.slots.shape = None
139 measureConfig.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
140 measureConfig.doReplaceWithNoise = False
142 measureConfig.plugins.names = ["base_CircularApertureFlux",
143 "base_PixelFlags",
144 "base_SkyCoord",
145 "base_PsfFlux",
146 "ip_diffim_NaiveDipoleCentroid",
147 "ip_diffim_NaiveDipoleFlux",
148 "ip_diffim_PsfDipoleFlux"]
150 # Here is where we make the dipole fitting task. It can run the other measurements as well.
151 measureTask = DipoleFitTask(config=measureConfig, schema=schema)
153 if maxFootprintArea:
154 measureTask.config.plugins["ip_diffim_DipoleFit"].maxFootprintArea = maxFootprintArea
156 table = afwTable.SourceTable.make(schema)
157 detectResult = detectTask.run(table, testImage.diffim)
158 # catalog = detectResult.sources
159 # deblendTask.run(self.dipole, catalog, psf=self.dipole.getPsf())
161 fpSet = detectResult.positive
162 fpSet.merge(detectResult.negative, 2, 2, False)
163 sources = afwTable.SourceCatalog(table)
164 fpSet.makeSources(sources)
166 measureTask.run(sources, testImage.diffim, testImage.posImage, testImage.negImage)
167 return sources
169 def _checkTaskOutput(self, dipoleTestImage, sources, rtol=None):
170 """Compare the fluxes/centroids in `sources` are entered
171 into the correct slots of the catalog, and have values that
172 are very close to the input values for both dipoles in the
173 image.
175 Also test that the resulting fluxes are close to those
176 generated by the existing ip_diffim_DipoleMeasurement task
177 (PsfDipoleFit).
178 """
180 if rtol is None:
181 rtol = dipoleTestImage.rtol
182 offsets = dipoleTestImage.offsets
183 for i, r1 in enumerate(sources):
184 result = r1.extract("ip_diffim_DipoleFit*")
185 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
186 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
187 dipoleTestImage.flux[i], rtol=rtol)
188 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'],
189 dipoleTestImage.xc[i] + offsets[i], rtol=rtol)
190 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'],
191 dipoleTestImage.yc[i] + offsets[i], rtol=rtol)
192 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'],
193 dipoleTestImage.xc[i] - offsets[i], rtol=rtol)
194 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'],
195 dipoleTestImage.yc[i] - offsets[i], rtol=rtol)
196 # Note this is dependent on the noise (variance) being realistic in the image.
197 # otherwise it throws off the chi2 estimate, which is used for classification:
198 self.assertTrue(result['ip_diffim_DipoleFit_flag_classification'])
200 # compare to the original ip_diffim_PsfDipoleFlux measurements
201 result2 = r1.extract("ip_diffim_PsfDipoleFlux*")
202 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
203 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
204 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux']
205 + abs(result2['ip_diffim_PsfDipoleFlux_neg_instFlux']))/2.,
206 rtol=rtol)
207 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'],
208 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'],
209 rtol=rtol)
210 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'],
211 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'],
212 rtol=rtol)
213 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'],
214 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'],
215 rtol=rtol)
216 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'],
217 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'],
218 rtol=rtol)
220 return result
222 def testDipoleTask(self):
223 """Test the dipole fitting singleFramePlugin.
225 Test that the resulting fluxes/centroids are entered into the
226 correct slots of the catalog, and have values that are very
227 close to the input values for both dipoles in the image.
229 Also test that the resulting fluxes are close to those
230 generated by the existing ip_diffim_DipoleMeasurement task
231 (PsfDipoleFit).
232 """
233 dipoleTestImage = DipoleTestImage()
234 sources = self._runDetection(dipoleTestImage)
235 self._checkTaskOutput(dipoleTestImage, sources)
237 def testDipoleTaskNoPosImage(self):
238 """Test the dipole fitting singleFramePlugin in the case where no
239 `posImage` is provided. It should be the same as above because
240 `posImage` can be constructed from `diffim+negImage`.
242 Test that the resulting fluxes/centroids are entered into the
243 correct slots of the catalog, and have values that are very
244 close to the input values for both dipoles in the image.
246 Also test that the resulting fluxes are close to those
247 generated by the existing ip_diffim_DipoleMeasurement task
248 (PsfDipoleFit).
249 """
250 dipoleTestImage = DipoleTestImage()
251 dipoleTestImage.testImage.posImage = None
252 sources = self._runDetection(dipoleTestImage)
253 self._checkTaskOutput(dipoleTestImage, sources)
255 def testDipoleTaskNoNegImage(self):
256 """Test the dipole fitting singleFramePlugin in the case where no
257 `negImage` is provided. It should be the same as above because
258 `negImage` can be constructed from `posImage-diffim`.
260 Test that the resulting fluxes/centroids are entered into the
261 correct slots of the catalog, and have values that are very
262 close to the input values for both dipoles in the image.
264 Also test that the resulting fluxes are close to those
265 generated by the existing ip_diffim_DipoleMeasurement task
266 (PsfDipoleFit).
267 """
268 dipoleTestImage = DipoleTestImage()
269 dipoleTestImage.testImage.negImage = None
270 sources = self._runDetection(dipoleTestImage)
271 self._checkTaskOutput(dipoleTestImage, sources)
273 def testDipoleTaskNoPreSubImages(self):
274 """Test the dipole fitting singleFramePlugin in the case where no
275 pre-subtraction data (`posImage` or `negImage`) are provided.
276 In this case it just fits a dipole model to the diffim
277 (dipole) image alone. Note that this test will only pass for
278 widely-separated dipoles.
280 Test that the resulting fluxes/centroids are entered into the
281 correct slots of the catalog, and have values that are very
282 close to the input values for both dipoles in the image.
284 Also test that the resulting fluxes are close to those
285 generated by the existing ip_diffim_DipoleMeasurement task
286 (PsfDipoleFit).
287 """
288 dipoleTestImage = DipoleTestImage()
289 dipoleTestImage.testImage.posImage = dipoleTestImage.testImage.negImage = None
290 sources = self._runDetection(dipoleTestImage)
291 self._checkTaskOutput(dipoleTestImage, sources)
293 def testDipoleEdge(self):
294 """Test the too-close-to-image-edge scenario for dipole fitting
295 singleFramePlugin.
297 Test that the dipoles which are too close to the edge are
298 flagged as such in the catalog and do not raise an error that is
299 not caught. Make sure both diaSources are actually detected,
300 if not measured.
301 """
303 dipoleTestImage = DipoleTestImage(xc=[5.3, 4.8], yc=[4.6, 96.5])
304 sources = self._runDetection(dipoleTestImage)
306 self.assertTrue(len(sources) == 2)
308 for i, s in enumerate(sources):
309 result = s.extract("ip_diffim_DipoleFit*")
310 self.assertTrue(result.get("ip_diffim_DipoleFit_flag"))
312 def testDipoleFootprintTooLarge(self):
313 """Test that the footprint area cut flags sources."""
315 dipoleTestImage = DipoleTestImage()
316 # This area is smaller than the area of the test sources (~750).
317 sources = self._runDetection(dipoleTestImage, maxFootprintArea=500)
319 self.assertTrue(np.all(sources["ip_diffim_DipoleFit_flag"]))
322class TestMemory(lsst.utils.tests.MemoryTestCase):
323 pass
326def setup_module(module):
327 lsst.utils.tests.init()
330if __name__ == "__main__": 330 ↛ 331line 330 didn't jump to line 331, because the condition on line 330 was never true
331 lsst.utils.tests.init()
332 unittest.main()