Coverage for tests/test_detectAndMeasure.py: 10%
156 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-28 02:46 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-28 02:46 -0800
1# This file is part of ip_diffim.
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 numpy as np
23import unittest
25import lsst.geom
26from lsst.ip.diffim import detectAndMeasure
27from lsst.ip.diffim.utils import makeTestImage
28import lsst.utils.tests
31class DetectAndMeasureTest(lsst.utils.tests.TestCase):
33 def test_detection_runs(self):
34 """Basic smoke test.
35 """
36 noiseLevel = 1.
37 staticSeed = 1
38 fluxLevel = 500
39 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 0, "y0": 0}
40 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
41 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
42 difference = science.clone()
43 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass()
44 config.doApCorr = False
45 task = detectAndMeasure.DetectAndMeasureTask(config=config)
46 output = task.run(science, matchedTemplate, difference)
47 subtractedMeasuredExposure = output.subtractedMeasuredExposure
48 self.assertImagesEqual(subtractedMeasuredExposure.image, difference.image)
50 def test_detection_xy0(self):
51 """Basic smoke test with non-zero x0 and y0.
52 """
53 noiseLevel = 1.
54 staticSeed = 1
55 fluxLevel = 500
56 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 12345, "y0": 67890}
57 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
58 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
59 difference = science.clone()
60 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass()
61 config.doApCorr = False
62 config.doMerge = False
63 config.doSkySources = False
64 config.doForcedMeasurement = False
65 task = detectAndMeasure.DetectAndMeasureTask(config=config)
66 output = task.run(science, matchedTemplate, difference)
67 subtractedMeasuredExposure = output.subtractedMeasuredExposure
69 self.assertImagesEqual(subtractedMeasuredExposure.image, difference.image)
71 def test_detect_transients(self):
72 """Run detection on a difference image containing transients.
73 """
74 noiseLevel = 1.
75 staticSeed = 1
76 transientSeed = 6
77 fluxLevel = 500
78 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
79 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
80 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
81 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass()
82 config.doApCorr = False
83 config.doMerge = False
84 config.doSkySources = False
85 config.doForcedMeasurement = False
86 detectionTask = detectAndMeasure.DetectAndMeasureTask(config=config)
88 def _detection_wrapper(polarity=1):
89 transients, transientSources = makeTestImage(seed=transientSeed, psfSize=2.4,
90 nSrc=10, fluxLevel=1000.,
91 noiseLevel=noiseLevel, noiseSeed=8)
92 difference = science.clone()
93 difference.maskedImage -= matchedTemplate.maskedImage
94 if polarity > 0:
95 difference.maskedImage += transients.maskedImage
96 else:
97 difference.maskedImage -= transients.maskedImage
98 output = detectionTask.run(science, matchedTemplate, difference)
99 refIds = []
100 for diaSource in output.diaSources:
101 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=polarity)
102 _detection_wrapper(polarity=1)
103 _detection_wrapper(polarity=-1)
105 def test_detect_dipoles(self):
106 """Run detection on a difference image containing dipoles.
107 """
108 noiseLevel = 1.
109 staticSeed = 1
110 fluxLevel = 1000
111 fluxRange = 1.5
112 nSources = 10
113 offset = 1
114 dipoleFlag = "ip_diffim_DipoleFit_flag_classification"
115 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "fluxRange": fluxRange,
116 "nSrc": nSources}
117 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
118 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
119 difference = science.clone()
120 matchedTemplate.image.array[...] = np.roll(matchedTemplate.image.array[...], offset, axis=0)
121 matchedTemplate.variance.array[...] = np.roll(matchedTemplate.variance.array[...], offset, axis=0)
122 matchedTemplate.mask.array[...] = np.roll(matchedTemplate.mask.array[...], offset, axis=0)
123 difference.maskedImage -= matchedTemplate.maskedImage[science.getBBox()]
124 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass()
125 config.doApCorr = False
126 config.doMerge = False
127 config.doSkySources = False
128 config.doForcedMeasurement = False
129 detectionTask = detectAndMeasure.DetectAndMeasureTask(config=config)
130 output = detectionTask.run(science, matchedTemplate, difference)
131 self.assertIn(dipoleFlag, output.diaSources.schema.getNames())
132 nSourcesDet = len(sources)
133 self.assertEqual(len(output.diaSources), 2*nSourcesDet)
134 refIds = []
135 # The diaSource check should fail if we don't merge positive and negative footprints
136 for diaSource in output.diaSources:
137 with self.assertRaises(AssertionError):
138 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0,
139 atol=np.sqrt(fluxRange*fluxLevel))
141 config.doMerge = True
142 detectionTask2 = detectAndMeasure.DetectAndMeasureTask(config=config)
143 output2 = detectionTask2.run(science, matchedTemplate, difference)
144 self.assertEqual(len(output2.diaSources), nSourcesDet)
145 refIds = []
146 for diaSource in output2.diaSources:
147 if diaSource[dipoleFlag]:
148 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0,
149 rtol=0.05, atol=None, usePsfFlux=False)
150 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_orientation"], -90., atol=2.)
151 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_separation"], offset, rtol=0.1)
152 else:
153 raise ValueError("DiaSource with ID %s is not a dipole!", diaSource.getId())
155 def test_sky_sources(self):
156 """Add sky sources and check that they are sufficiently far from other
157 sources and have negligible flux.
158 """
159 noiseLevel = 1.
160 staticSeed = 1
161 transientSeed = 6
162 transientFluxLevel = 1000.
163 transientFluxRange = 1.5
164 fluxLevel = 500
165 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
166 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
167 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
168 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass()
169 config.doApCorr = False
170 config.doMerge = False
171 config.doSkySources = True
172 config.doForcedMeasurement = False
173 config.skySources.nSources = 5
174 kernelWidth = np.max(science.psf.getKernel().getDimensions())//2
175 detectionTask = detectAndMeasure.DetectAndMeasureTask(config=config)
176 transients, transientSources = makeTestImage(seed=transientSeed, psfSize=2.4,
177 nSrc=10, fluxLevel=transientFluxLevel,
178 fluxRange=transientFluxRange,
179 noiseLevel=noiseLevel, noiseSeed=8)
180 difference = science.clone()
181 difference.maskedImage -= matchedTemplate.maskedImage
182 difference.maskedImage += transients.maskedImage
183 output = detectionTask.run(science, matchedTemplate, difference)
184 skySources = output.diaSources[output.diaSources["sky_source"]]
185 self.assertEqual(len(skySources), config.skySources.nSources)
186 for skySource in skySources:
187 # The sky sources should not be close to any other source
188 with self.assertRaises(AssertionError):
189 self._check_diaSource(transientSources, skySource, matchDistance=kernelWidth)
190 with self.assertRaises(AssertionError):
191 self._check_diaSource(sources, skySource, matchDistance=kernelWidth)
192 # The sky sources should have low flux levels.
193 self._check_diaSource(transientSources, skySource, matchDistance=1000, scale=0.,
194 atol=np.sqrt(transientFluxRange*transientFluxLevel))
196 def _check_diaSource(self, refSources, diaSource, refIds=None,
197 matchDistance=1., scale=1., usePsfFlux=True,
198 rtol=0.02, atol=None):
199 """Match a diaSource with a source in a reference catalog
200 and compare properties.
201 """
202 distance = np.sqrt((diaSource.getX() - refSources.getX())**2
203 + (diaSource.getY() - refSources.getY())**2)
204 self.assertLess(min(distance), matchDistance)
205 src = refSources[np.argmin(distance)]
206 if refIds is not None:
207 # Check that the same source was not previously associated
208 self.assertNotIn(src.getId(), refIds)
209 refIds.append(src.getId())
210 if atol is None:
211 atol = rtol*src.getPsfInstFlux() if usePsfFlux else rtol*src.getApInstFlux()
212 if usePsfFlux:
213 self.assertFloatsAlmostEqual(src.getPsfInstFlux()*scale, diaSource.getPsfInstFlux(),
214 rtol=rtol, atol=atol)
215 else:
216 self.assertFloatsAlmostEqual(src.getApInstFlux()*scale, diaSource.getApInstFlux(),
217 rtol=rtol, atol=atol)
220def setup_module(module):
221 lsst.utils.tests.init()
224class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
225 pass
228if __name__ == "__main__": 228 ↛ 229line 228 didn't jump to line 229, because the condition on line 228 was never true
229 lsst.utils.tests.init()
230 unittest.main()