Coverage for tests/test_dipoleFitter.py: 21%
107 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 02:08 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 02:08 -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
32from lsst.ip.diffim.dipoleFitTask import (DipoleFitAlgorithm, DipoleFitTask)
33import lsst.ip.diffim.utils as ipUtils
36class DipoleTestImage:
37 """Create a test dipole image and store the parameters used to make it,
38 for comparison with the fitted results.
40 Parameters
41 ----------
42 xc, yc : `list` [`float`]
43 x, y coordinate (pixels) of center(s) of input dipole(s).
44 flux: `list` [`float`]
45 Flux(es) of input dipole(s).
46 gradientParams : `tuple`
47 Tuple with three parameters for linear background gradient.
48 offsets : `list` [`float`]
49 Pixel coordinates between lobes of dipoles.
50 """
52 def __init__(self, xc=None, yc=None, flux=None, offsets=None, gradientParams=None):
53 self.xc = xc if xc is not None else [65.3, 24.2]
54 self.yc = yc if yc is not None else [38.6, 78.5]
55 self.offsets = offsets if offsets is not None else np.array([-2., 2.])
56 self.flux = flux if flux is not None else [2500., 2345.]
57 self.gradientParams = gradientParams if gradientParams is not None else [10., 3., 5.]
59 # The default tolerance for comparisons of fitted parameters with input values.
60 # Given the noise in the input images (default noise value of 2.), this is a
61 # useful test of algorithm robustness, and will guard against future regressions.
62 self.rtol = 0.01
64 self.generateTestImage()
66 def generateTestImage(self):
67 self.testImage = ipUtils.DipoleTestImage(
68 w=100, h=100,
69 xcenPos=self.xc + self.offsets,
70 ycenPos=self.yc + self.offsets,
71 xcenNeg=self.xc - self.offsets,
72 ycenNeg=self.yc - self.offsets,
73 flux=self.flux, fluxNeg=self.flux,
74 noise=2., # Note the input noise - this affects the relative tolerances used.
75 gradientParams=self.gradientParams)
78class DipoleFitTest(lsst.utils.tests.TestCase):
79 """A test case for separately testing the dipole fit algorithm
80 directly, and the single frame measurement.
82 In each test, create a simulated diffim with two dipoles, noise,
83 and a linear background gradient in the pre-sub images then
84 compare the input fluxes/centroids with the fitted results.
85 """
87 def testDipoleAlgorithm(self):
88 """Test the dipole fitting algorithm directly (fitDipole()).
90 Test that the resulting fluxes/centroids are very close to the
91 input values for both dipoles in the image.
92 """
93 # Display (plot) the output dipole thumbnails with matplotlib.
94 display = False
95 # Be verbose during fitting, including the lmfit internal details.
96 verbose = False
98 dipoleTestImage = DipoleTestImage()
99 catalog = dipoleTestImage.testImage.detectDipoleSources(minBinSize=32)
101 for s in catalog:
102 fp = s.getFootprint()
103 self.assertTrue(len(fp.getPeaks()) == 2)
105 rtol = dipoleTestImage.rtol
106 offsets = dipoleTestImage.offsets
107 testImage = dipoleTestImage.testImage
108 for i, s in enumerate(catalog):
109 alg = DipoleFitAlgorithm(testImage.diffim, testImage.posImage, testImage.negImage)
110 result, _ = alg.fitDipole(
111 s, rel_weight=0.5, separateNegParams=False,
112 verbose=verbose, display=display)
114 self.assertFloatsAlmostEqual((result.posFlux + abs(result.negFlux))/2.,
115 dipoleTestImage.flux[i], rtol=rtol)
116 self.assertFloatsAlmostEqual(result.posCentroidX, dipoleTestImage.xc[i] + offsets[i], rtol=rtol)
117 self.assertFloatsAlmostEqual(result.posCentroidY, dipoleTestImage.yc[i] + offsets[i], rtol=rtol)
118 self.assertFloatsAlmostEqual(result.negCentroidX, dipoleTestImage.xc[i] - offsets[i], rtol=rtol)
119 self.assertFloatsAlmostEqual(result.negCentroidY, dipoleTestImage.yc[i] - offsets[i], rtol=rtol)
121 def _runDetection(self, dipoleTestImage, maxFootprintArea=None):
122 """Run 'diaSource' detection on the diffim, including merging of
123 positive and negative sources.
125 Then run DipoleFitTask on the image and return the resulting catalog.
126 """
127 # Create the various tasks and schema -- avoid code reuse.
128 testImage = dipoleTestImage.testImage
129 detectTask, schema = testImage.detectDipoleSources(doMerge=False, minBinSize=32)
131 config = DipoleFitTask.ConfigClass()
132 # Also run the older C++ DipoleFlux algorithm for comparison purposes.
133 config.plugins.names |= ["ip_diffim_PsfDipoleFlux"]
134 if maxFootprintArea:
135 config.plugins["ip_diffim_DipoleFit"].maxFootprintArea = maxFootprintArea
136 measureTask = DipoleFitTask(schema=schema, config=config)
138 table = afwTable.SourceTable.make(schema)
139 detectResult = detectTask.run(table, testImage.diffim)
140 fpSet = detectResult.positive
141 fpSet.merge(detectResult.negative, 2, 2, False)
142 sources = afwTable.SourceCatalog(table)
143 fpSet.makeSources(sources)
145 measureTask.run(sources, testImage.diffim, testImage.posImage, testImage.negImage)
146 return sources
148 def _checkTaskOutput(self, dipoleTestImage, sources, rtol=None):
149 """Compare the fluxes/centroids in `sources` are entered
150 into the correct slots of the catalog, and have values that
151 are very close to the input values for both dipoles in the
152 image.
154 Also test that the resulting fluxes are close to those
155 generated by the existing ip_diffim_DipoleMeasurement task
156 (PsfDipoleFit).
157 """
159 if rtol is None:
160 rtol = dipoleTestImage.rtol
161 offsets = dipoleTestImage.offsets
162 for i, r1 in enumerate(sources):
163 result = r1.extract("ip_diffim_DipoleFit*")
164 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
165 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
166 dipoleTestImage.flux[i], rtol=rtol)
167 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_x'],
168 dipoleTestImage.xc[i] + offsets[i], rtol=rtol)
169 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_y'],
170 dipoleTestImage.yc[i] + offsets[i], rtol=rtol)
171 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_x'],
172 dipoleTestImage.xc[i] - offsets[i], rtol=rtol)
173 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_y'],
174 dipoleTestImage.yc[i] - offsets[i], rtol=rtol)
175 # Note this is dependent on the noise (variance) being realistic in the image.
176 # otherwise it throws off the chi2 estimate, which is used for classification:
177 self.assertTrue(result['ip_diffim_DipoleFit_flag_classification'])
179 # compare to the original ip_diffim_PsfDipoleFlux measurements
180 result2 = r1.extract("ip_diffim_PsfDipoleFlux*")
181 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
182 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
183 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux']
184 + abs(result2['ip_diffim_PsfDipoleFlux_neg_instFlux']))/2.,
185 rtol=rtol)
186 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_x'],
187 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'],
188 rtol=rtol)
189 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_y'],
190 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'],
191 rtol=rtol)
192 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_x'],
193 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'],
194 rtol=rtol)
195 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_y'],
196 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'],
197 rtol=rtol)
199 return result
201 def testDipoleTask(self):
202 """Test the dipole fitting singleFramePlugin.
204 Test that the resulting fluxes/centroids are entered into the
205 correct slots of the catalog, and have values that are very
206 close to the input values for both dipoles in the image.
208 Also test that the resulting fluxes are close to those
209 generated by the existing ip_diffim_DipoleMeasurement task
210 (PsfDipoleFit).
211 """
212 dipoleTestImage = DipoleTestImage()
213 sources = self._runDetection(dipoleTestImage)
214 self._checkTaskOutput(dipoleTestImage, sources)
216 def testDipoleTaskNoPosImage(self):
217 """Test the dipole fitting singleFramePlugin in the case where no
218 `posImage` is provided. It should be the same as above because
219 `posImage` can be constructed from `diffim+negImage`.
221 Test that the resulting fluxes/centroids are entered into the
222 correct slots of the catalog, and have values that are very
223 close to the input values for both dipoles in the image.
225 Also test that the resulting fluxes are close to those
226 generated by the existing ip_diffim_DipoleMeasurement task
227 (PsfDipoleFit).
228 """
229 dipoleTestImage = DipoleTestImage()
230 dipoleTestImage.testImage.posImage = None
231 sources = self._runDetection(dipoleTestImage)
232 self._checkTaskOutput(dipoleTestImage, sources)
234 def testDipoleTaskNoNegImage(self):
235 """Test the dipole fitting singleFramePlugin in the case where no
236 `negImage` is provided. It should be the same as above because
237 `negImage` can be constructed from `posImage-diffim`.
239 Test that the resulting fluxes/centroids are entered into the
240 correct slots of the catalog, and have values that are very
241 close to the input values for both dipoles in the image.
243 Also test that the resulting fluxes are close to those
244 generated by the existing ip_diffim_DipoleMeasurement task
245 (PsfDipoleFit).
246 """
247 dipoleTestImage = DipoleTestImage()
248 dipoleTestImage.testImage.negImage = None
249 sources = self._runDetection(dipoleTestImage)
250 self._checkTaskOutput(dipoleTestImage, sources)
252 def testDipoleTaskNoPreSubImages(self):
253 """Test the dipole fitting singleFramePlugin in the case where no
254 pre-subtraction data (`posImage` or `negImage`) are provided.
255 In this case it just fits a dipole model to the diffim
256 (dipole) image alone. Note that this test will only pass for
257 widely-separated dipoles.
259 Test that the resulting fluxes/centroids are entered into the
260 correct slots of the catalog, and have values that are very
261 close to the input values for both dipoles in the image.
263 Also test that the resulting fluxes are close to those
264 generated by the existing ip_diffim_DipoleMeasurement task
265 (PsfDipoleFit).
266 """
267 dipoleTestImage = DipoleTestImage()
268 dipoleTestImage.testImage.posImage = dipoleTestImage.testImage.negImage = None
269 sources = self._runDetection(dipoleTestImage)
270 self._checkTaskOutput(dipoleTestImage, sources)
272 def testDipoleEdge(self):
273 """Test the too-close-to-image-edge scenario for dipole fitting
274 singleFramePlugin.
276 Test that the dipoles which are too close to the edge are
277 not detected.
278 """
280 dipoleTestImage = DipoleTestImage(xc=[5.3, 4.8], yc=[4.6, 96.5])
281 sources = self._runDetection(dipoleTestImage)
283 self.assertTrue(len(sources) == 0)
285 def testDipoleFootprintTooLarge(self):
286 """Test that the footprint area cut flags sources."""
288 dipoleTestImage = DipoleTestImage()
289 # This area is smaller than the area of the test sources (~750).
290 sources = self._runDetection(dipoleTestImage, maxFootprintArea=500)
292 self.assertTrue(np.all(sources["ip_diffim_DipoleFit_flag"]))
295class TestMemory(lsst.utils.tests.MemoryTestCase):
296 pass
299def setup_module(module):
300 lsst.utils.tests.init()
303if __name__ == "__main__": 303 ↛ 304line 303 didn't jump to line 304, because the condition on line 303 was never true
304 lsst.utils.tests.init()
305 unittest.main()