Coverage for tests/test_dipoleFitter.py: 20%
111 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-07 11:36 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-07 11:36 +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(object):
38 """!Class to initialize test dipole image used by all tests below.
40 @var display: Display (plot) the output dipole thumbnails (matplotlib)
41 @var verbose: be verbose during fitting
42 @var xc: x coordinate (pixels) of center(s) of input dipole(s)
43 @var yc: y coordinate (pixels) of center(s) of input dipole(s)
44 @var flux: flux(es) of input dipole(s)
45 @var gradientParams: tuple with three parameters for linear background gradient
46 @var offsets: pixel coordinates between lobes of dipoles
48 Also stores all parameters used to generate the test image (to compare to fitting results).
49 """
51 def __init__(self, xc=None, yc=None, flux=None, offsets=None, gradientParams=None):
52 """!Store the parameters, create the test image and run detection on it.
54 @param xc iterable x coordinate (pixels) of center(s) of input dipole(s)
55 @param yc iterable y coordinate (pixels) of center(s) of input dipole(s)
56 @param offsets iterable pixel coord offsets between lobes of dipole(s)
57 @param flux iterable fluxes of pos/neg lobes of dipole(s)
58 @param gradientParams iterable three parameters for linear background gradient
59 """
60 self.display = False # Display (plot) the output dipole thumbnails (matplotlib)
61 self.verbose = False # be verbose during fitting
63 self.xc = xc if xc is not None else [65.3, 24.2]
64 self.yc = yc if yc is not None else [38.6, 78.5]
65 self.offsets = offsets if offsets is not None else np.array([-2., 2.])
66 self.flux = flux if flux is not None else [2500., 2345.]
67 self.gradientParams = gradientParams if gradientParams is not None else [10., 3., 5.]
69 # The default tolerance for comparisons of fitted parameters with input values.
70 # Given the noise in the input images (default noise value of 2.), this is a
71 # useful test of algorithm robustness, and will guard against future regressions.
72 self.rtol = 0.01
74 self.generateTestImage()
76 def generateTestImage(self):
77 self.testImage = ipUtils.DipoleTestImage(
78 w=100, h=100,
79 xcenPos=self.xc + self.offsets,
80 ycenPos=self.yc + self.offsets,
81 xcenNeg=self.xc - self.offsets,
82 ycenNeg=self.yc - self.offsets,
83 flux=self.flux, fluxNeg=self.flux,
84 noise=2., # Note the input noise - this affects the relative tolerances used.
85 gradientParams=self.gradientParams)
88class DipoleFitTest(lsst.utils.tests.TestCase):
89 """!A test case for separately testing the dipole fit algorithm
90 directly, and the single frame measurement.
92 In each test, create a simulated diffim with two dipoles, noise,
93 and a linear background gradient in the pre-sub images then
94 compare the input fluxes/centroids with the fitted results.
95 """
97 def testDipoleAlgorithm(self):
98 """!Test the dipole fitting algorithm directly (fitDipole()).
100 Test that the resulting fluxes/centroids are very close to the
101 input values for both dipoles in the image.
102 """
103 params = DipoleTestImage()
104 catalog = params.testImage.detectDipoleSources(minBinSize=32)
106 for s in catalog:
107 fp = s.getFootprint()
108 self.assertTrue(len(fp.getPeaks()) == 2)
110 rtol = params.rtol
111 offsets = params.offsets
112 testImage = params.testImage
113 for i, s in enumerate(catalog):
114 alg = DipoleFitAlgorithm(testImage.diffim, testImage.posImage, testImage.negImage)
115 result, _ = alg.fitDipole(
116 s, rel_weight=0.5, separateNegParams=False,
117 verbose=params.verbose, display=params.display)
119 self.assertFloatsAlmostEqual((result.posFlux + abs(result.negFlux))/2.,
120 params.flux[i], rtol=rtol)
121 self.assertFloatsAlmostEqual(result.posCentroidX, params.xc[i] + offsets[i], rtol=rtol)
122 self.assertFloatsAlmostEqual(result.posCentroidY, params.yc[i] + offsets[i], rtol=rtol)
123 self.assertFloatsAlmostEqual(result.negCentroidX, params.xc[i] - offsets[i], rtol=rtol)
124 self.assertFloatsAlmostEqual(result.negCentroidY, params.yc[i] - offsets[i], rtol=rtol)
126 def _runDetection(self, params):
127 """!Run 'diaSource' detection on the diffim, including merging of
128 positive and negative sources.
130 Then run DipoleFitTask on the image and return the resulting catalog.
131 """
133 # Create the various tasks and schema -- avoid code reuse.
134 testImage = params.testImage
135 detectTask, schema = testImage.detectDipoleSources(doMerge=False, minBinSize=32)
137 measureConfig = measBase.SingleFrameMeasurementConfig()
139 measureConfig.slots.calibFlux = None
140 measureConfig.slots.modelFlux = None
141 measureConfig.slots.gaussianFlux = None
142 measureConfig.slots.shape = None
143 measureConfig.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
144 measureConfig.doReplaceWithNoise = False
146 measureConfig.plugins.names = ["base_CircularApertureFlux",
147 "base_PixelFlags",
148 "base_SkyCoord",
149 "base_PsfFlux",
150 "ip_diffim_NaiveDipoleCentroid",
151 "ip_diffim_NaiveDipoleFlux",
152 "ip_diffim_PsfDipoleFlux"]
154 # Here is where we make the dipole fitting task. It can run the other measurements as well.
155 # This is an example of how to pass it a custom config.
156 measureTask = DipoleFitTask(config=measureConfig, schema=schema)
158 table = afwTable.SourceTable.make(schema)
159 detectResult = detectTask.run(table, testImage.diffim)
160 # catalog = detectResult.sources
161 # deblendTask.run(self.dipole, catalog, psf=self.dipole.getPsf())
163 fpSet = detectResult.fpSets.positive
164 fpSet.merge(detectResult.fpSets.negative, 2, 2, False)
165 sources = afwTable.SourceCatalog(table)
166 fpSet.makeSources(sources)
168 measureTask.run(sources, testImage.diffim, testImage.posImage, testImage.negImage)
169 return sources
171 def _checkTaskOutput(self, params, sources, rtol=None):
172 """!Compare the fluxes/centroids in `sources` are entered
173 into the correct slots of the catalog, and have values that
174 are very close to the input values for both dipoles in the
175 image.
177 Also test that the resulting fluxes are close to those
178 generated by the existing ip_diffim_DipoleMeasurement task
179 (PsfDipoleFit).
180 """
182 if rtol is None:
183 rtol = params.rtol
184 offsets = params.offsets
185 for i, r1 in enumerate(sources):
186 result = r1.extract("ip_diffim_DipoleFit*")
187 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
188 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
189 params.flux[i], rtol=rtol)
190 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'],
191 params.xc[i] + offsets[i], rtol=rtol)
192 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'],
193 params.yc[i] + offsets[i], rtol=rtol)
194 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'],
195 params.xc[i] - offsets[i], rtol=rtol)
196 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'],
197 params.yc[i] - offsets[i], rtol=rtol)
198 # Note this is dependent on the noise (variance) being realistic in the image.
199 # otherwise it throws off the chi2 estimate, which is used for classification:
200 self.assertTrue(result['ip_diffim_DipoleFit_flag_classification'])
202 # compare to the original ip_diffim_PsfDipoleFlux measurements
203 result2 = r1.extract("ip_diffim_PsfDipoleFlux*")
204 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux']
205 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2.,
206 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux']
207 + abs(result2['ip_diffim_PsfDipoleFlux_neg_instFlux']))/2.,
208 rtol=rtol)
209 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'],
210 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'],
211 rtol=rtol)
212 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'],
213 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'],
214 rtol=rtol)
215 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'],
216 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'],
217 rtol=rtol)
218 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'],
219 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'],
220 rtol=rtol)
222 return result
224 def testDipoleTask(self):
225 """!Test the dipole fitting singleFramePlugin.
227 Test that the resulting fluxes/centroids are entered into the
228 correct slots of the catalog, and have values that are very
229 close to the input values for both dipoles in the image.
231 Also test that the resulting fluxes are close to those
232 generated by the existing ip_diffim_DipoleMeasurement task
233 (PsfDipoleFit).
234 """
235 params = DipoleTestImage()
236 sources = self._runDetection(params)
237 self._checkTaskOutput(params, sources)
239 def testDipoleTaskNoPosImage(self):
240 """!Test the dipole fitting singleFramePlugin in the case where no
241 `posImage` is provided. It should be the same as above because
242 `posImage` can be constructed from `diffim+negImage`.
244 Test that the resulting fluxes/centroids are entered into the
245 correct slots of the catalog, and have values that are very
246 close to the input values for both dipoles in the image.
248 Also test that the resulting fluxes are close to those
249 generated by the existing ip_diffim_DipoleMeasurement task
250 (PsfDipoleFit).
251 """
252 params = DipoleTestImage()
253 params.testImage.posImage = None
254 sources = self._runDetection(params)
255 self._checkTaskOutput(params, sources)
257 def testDipoleTaskNoNegImage(self):
258 """!Test the dipole fitting singleFramePlugin in the case where no
259 `negImage` is provided. It should be the same as above because
260 `negImage` can be constructed from `posImage-diffim`.
262 Test that the resulting fluxes/centroids are entered into the
263 correct slots of the catalog, and have values that are very
264 close to the input values for both dipoles in the image.
266 Also test that the resulting fluxes are close to those
267 generated by the existing ip_diffim_DipoleMeasurement task
268 (PsfDipoleFit).
269 """
270 params = DipoleTestImage()
271 params.testImage.negImage = None
272 sources = self._runDetection(params)
273 self._checkTaskOutput(params, sources)
275 def testDipoleTaskNoPreSubImages(self):
276 """!Test the dipole fitting singleFramePlugin in the case where no
277 pre-subtraction data (`posImage` or `negImage`) are provided.
278 In this case it just fits a dipole model to the diffim
279 (dipole) image alone. Note that this test will only pass for
280 widely-separated dipoles.
282 Test that the resulting fluxes/centroids are entered into the
283 correct slots of the catalog, and have values that are very
284 close to the input values for both dipoles in the image.
286 Also test that the resulting fluxes are close to those
287 generated by the existing ip_diffim_DipoleMeasurement task
288 (PsfDipoleFit).
289 """
290 params = DipoleTestImage()
291 params.testImage.posImage = params.testImage.negImage = None
292 sources = self._runDetection(params)
293 self._checkTaskOutput(params, sources)
295 def testDipoleEdge(self):
296 """!Test the too-close-to-image-edge scenario for dipole fitting
297 singleFramePlugin.
299 Test that the dipoles which are too close to the edge are
300 flagged as such in the catalog and do not raise an error that is
301 not caught. Make sure both diaSources are actually detected,
302 if not measured.
303 """
305 params = DipoleTestImage(xc=[5.3, 4.8], yc=[4.6, 96.5])
306 sources = self._runDetection(params)
308 self.assertTrue(len(sources) == 2)
310 for i, s in enumerate(sources):
311 result = s.extract("ip_diffim_DipoleFit*")
312 self.assertTrue(result.get("ip_diffim_DipoleFit_flag"))
315class TestMemory(lsst.utils.tests.MemoryTestCase):
316 pass
319def setup_module(module):
320 lsst.utils.tests.init()
323if __name__ == "__main__": 323 ↛ 324line 323 didn't jump to line 324, because the condition on line 323 was never true
324 lsst.utils.tests.init()
325 unittest.main()