Coverage for tests/test_dipoleFitter.py: 20%
111 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-10 11:28 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-10 11:28 +0000
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):
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 table = afwTable.SourceTable.make(schema)
154 detectResult = detectTask.run(table, testImage.diffim)
155 # catalog = detectResult.sources
156 # deblendTask.run(self.dipole, catalog, psf=self.dipole.getPsf())
158 fpSet = detectResult.positive
159 fpSet.merge(detectResult.negative, 2, 2, False)
160 sources = afwTable.SourceCatalog(table)
161 fpSet.makeSources(sources)
163 measureTask.run(sources, testImage.diffim, testImage.posImage, testImage.negImage)
164 return sources
166 def _checkTaskOutput(self, dipoleTestImage, sources, rtol=None):
167 """Compare the fluxes/centroids in `sources` are entered
168 into the correct slots of the catalog, and have values that
169 are very close to the input values for both dipoles in the
170 image.
172 Also test that the resulting fluxes are close to those
173 generated by the existing ip_diffim_DipoleMeasurement task
174 (PsfDipoleFit).
175 """
177 if rtol is None:
178 rtol = dipoleTestImage.rtol
179 offsets = dipoleTestImage.offsets
180 for i, r1 in enumerate(sources):
181 result = r1.extract("ip_diffim_DipoleFit*")
182 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
183 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
184 dipoleTestImage.flux[i], rtol=rtol)
185 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'],
186 dipoleTestImage.xc[i] + offsets[i], rtol=rtol)
187 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'],
188 dipoleTestImage.yc[i] + offsets[i], rtol=rtol)
189 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'],
190 dipoleTestImage.xc[i] - offsets[i], rtol=rtol)
191 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'],
192 dipoleTestImage.yc[i] - offsets[i], rtol=rtol)
193 # Note this is dependent on the noise (variance) being realistic in the image.
194 # otherwise it throws off the chi2 estimate, which is used for classification:
195 self.assertTrue(result['ip_diffim_DipoleFit_flag_classification'])
197 # compare to the original ip_diffim_PsfDipoleFlux measurements
198 result2 = r1.extract("ip_diffim_PsfDipoleFlux*")
199 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
200 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
201 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux']
202 + abs(result2['ip_diffim_PsfDipoleFlux_neg_instFlux']))/2.,
203 rtol=rtol)
204 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'],
205 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'],
206 rtol=rtol)
207 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'],
208 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'],
209 rtol=rtol)
210 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'],
211 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'],
212 rtol=rtol)
213 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'],
214 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'],
215 rtol=rtol)
217 return result
219 def testDipoleTask(self):
220 """Test the dipole fitting singleFramePlugin.
222 Test that the resulting fluxes/centroids are entered into the
223 correct slots of the catalog, and have values that are very
224 close to the input values for both dipoles in the image.
226 Also test that the resulting fluxes are close to those
227 generated by the existing ip_diffim_DipoleMeasurement task
228 (PsfDipoleFit).
229 """
230 dipoleTestImage = DipoleTestImage()
231 sources = self._runDetection(dipoleTestImage)
232 self._checkTaskOutput(dipoleTestImage, sources)
234 def testDipoleTaskNoPosImage(self):
235 """Test the dipole fitting singleFramePlugin in the case where no
236 `posImage` is provided. It should be the same as above because
237 `posImage` can be constructed from `diffim+negImage`.
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.posImage = None
249 sources = self._runDetection(dipoleTestImage)
250 self._checkTaskOutput(dipoleTestImage, sources)
252 def testDipoleTaskNoNegImage(self):
253 """Test the dipole fitting singleFramePlugin in the case where no
254 `negImage` is provided. It should be the same as above because
255 `negImage` can be constructed from `posImage-diffim`.
257 Test that the resulting fluxes/centroids are entered into the
258 correct slots of the catalog, and have values that are very
259 close to the input values for both dipoles in the image.
261 Also test that the resulting fluxes are close to those
262 generated by the existing ip_diffim_DipoleMeasurement task
263 (PsfDipoleFit).
264 """
265 dipoleTestImage = DipoleTestImage()
266 dipoleTestImage.testImage.negImage = None
267 sources = self._runDetection(dipoleTestImage)
268 self._checkTaskOutput(dipoleTestImage, sources)
270 def testDipoleTaskNoPreSubImages(self):
271 """Test the dipole fitting singleFramePlugin in the case where no
272 pre-subtraction data (`posImage` or `negImage`) are provided.
273 In this case it just fits a dipole model to the diffim
274 (dipole) image alone. Note that this test will only pass for
275 widely-separated dipoles.
277 Test that the resulting fluxes/centroids are entered into the
278 correct slots of the catalog, and have values that are very
279 close to the input values for both dipoles in the image.
281 Also test that the resulting fluxes are close to those
282 generated by the existing ip_diffim_DipoleMeasurement task
283 (PsfDipoleFit).
284 """
285 dipoleTestImage = DipoleTestImage()
286 dipoleTestImage.testImage.posImage = dipoleTestImage.testImage.negImage = None
287 sources = self._runDetection(dipoleTestImage)
288 self._checkTaskOutput(dipoleTestImage, sources)
290 def testDipoleEdge(self):
291 """Test the too-close-to-image-edge scenario for dipole fitting
292 singleFramePlugin.
294 Test that the dipoles which are too close to the edge are
295 flagged as such in the catalog and do not raise an error that is
296 not caught. Make sure both diaSources are actually detected,
297 if not measured.
298 """
300 dipoleTestImage = DipoleTestImage(xc=[5.3, 4.8], yc=[4.6, 96.5])
301 sources = self._runDetection(dipoleTestImage)
303 self.assertTrue(len(sources) == 2)
305 for i, s in enumerate(sources):
306 result = s.extract("ip_diffim_DipoleFit*")
307 self.assertTrue(result.get("ip_diffim_DipoleFit_flag"))
310class TestMemory(lsst.utils.tests.MemoryTestCase):
311 pass
314def setup_module(module):
315 lsst.utils.tests.init()
318if __name__ == "__main__": 318 ↛ 319line 318 didn't jump to line 319, because the condition on line 318 was never true
319 lsst.utils.tests.init()
320 unittest.main()