Coverage for tests/test_detectAndMeasure.py: 6%
405 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-06 03:35 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-06 03:35 -0700
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, subtractImages
27from lsst.ip.diffim.utils import makeTestImage
28import lsst.utils.tests
31class DetectAndMeasureTestBase(lsst.utils.tests.TestCase):
33 def _check_diaSource(self, refSources, diaSource, refIds=None,
34 matchDistance=1., scale=1., usePsfFlux=True,
35 rtol=0.02, atol=None):
36 """Match a diaSource with a source in a reference catalog
37 and compare properties.
39 Parameters
40 ----------
41 refSources : `lsst.afw.table.SourceCatalog`
42 The reference catalog.
43 diaSource : `lsst.afw.table.SourceRecord`
44 The new diaSource to match to the reference catalog.
45 refIds : `list` of `int`, optional
46 Source IDs of previously associated diaSources.
47 matchDistance : `float`, optional
48 Maximum distance allowed between the detected and reference source
49 locations, in pixels.
50 scale : `float`, optional
51 Optional factor to scale the flux by before performing the test.
52 usePsfFlux : `bool`, optional
53 If set, test the PsfInstFlux field, otherwise use ApInstFlux.
54 rtol : `float`, optional
55 Relative tolerance of the flux value test.
56 atol : `float`, optional
57 Absolute tolerance of the flux value test.
58 """
59 distance = np.sqrt((diaSource.getX() - refSources.getX())**2
60 + (diaSource.getY() - refSources.getY())**2)
61 self.assertLess(min(distance), matchDistance)
62 src = refSources[np.argmin(distance)]
63 if refIds is not None:
64 # Check that the same source was not previously associated
65 self.assertNotIn(src.getId(), refIds)
66 refIds.append(src.getId())
67 if atol is None:
68 atol = rtol*src.getPsfInstFlux() if usePsfFlux else rtol*src.getApInstFlux()
69 if usePsfFlux:
70 self.assertFloatsAlmostEqual(src.getPsfInstFlux()*scale, diaSource.getPsfInstFlux(),
71 rtol=rtol, atol=atol)
72 else:
73 self.assertFloatsAlmostEqual(src.getApInstFlux()*scale, diaSource.getApInstFlux(),
74 rtol=rtol, atol=atol)
76 def _check_values(self, values, minValue=None, maxValue=None):
77 """Verify that an array has finite values, and optionally that they are
78 within specified minimum and maximum bounds.
80 Parameters
81 ----------
82 values : `numpy.ndarray`
83 Array of values to check.
84 minValue : `float`, optional
85 Minimum allowable value.
86 maxValue : `float`, optional
87 Maximum allowable value.
88 """
89 self.assertTrue(np.all(np.isfinite(values)))
90 if minValue is not None:
91 self.assertTrue(np.all(values >= minValue))
92 if maxValue is not None:
93 self.assertTrue(np.all(values <= maxValue))
95 def _setup_detection(self, doApCorr=False, doMerge=False,
96 doSkySources=False, doForcedMeasurement=False):
97 """Setup and configure the detection and measurement PipelineTask.
99 Parameters
100 ----------
101 doApCorr : `bool`, optional
102 Run subtask to apply aperture corrections.
103 doMerge : `bool`, optional
104 Merge positive and negative diaSources.
105 doSkySources : `bool`, optional
106 Generate sky sources.
107 doForcedMeasurement : `bool`, optional
108 Force photometer diaSource locations on PVI.
110 Returns
111 -------
112 `lsst.pipe.base.PipelineTask`
113 The configured Task to use for detection and measurement.
114 """
115 config = self.detectionTask.ConfigClass()
116 config.doApCorr = doApCorr
117 config.doMerge = doMerge
118 config.doSkySources = doSkySources
119 config.doForcedMeasurement = doForcedMeasurement
120 if doSkySources:
121 config.skySources.nSources = 5
122 return self.detectionTask(config=config)
125class DetectAndMeasureTest(DetectAndMeasureTestBase):
126 detectionTask = detectAndMeasure.DetectAndMeasureTask
128 def test_detection_xy0(self):
129 """Basic functionality test with non-zero x0 and y0.
130 """
131 # Set up the simulated images
132 noiseLevel = 1.
133 staticSeed = 1
134 fluxLevel = 500
135 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 12345, "y0": 67890}
136 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
137 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
138 difference = science.clone()
140 # Configure the detection Task
141 detectionTask = self._setup_detection()
143 # Run detection and check the results
144 output = detectionTask.run(science, matchedTemplate, difference)
145 subtractedMeasuredExposure = output.subtractedMeasuredExposure
147 self.assertImagesEqual(subtractedMeasuredExposure.image, difference.image)
149 def test_measurements_finite(self):
150 """Measured fluxes and centroids should always be finite.
151 """
152 columnNames = ["coord_ra", "coord_dec", "ip_diffim_forced_PsfFlux_instFlux"]
154 # Set up the simulated images
155 noiseLevel = 1.
156 staticSeed = 1
157 transientSeed = 6
158 xSize = 256
159 ySize = 256
160 kwargs = {"psfSize": 2.4, "x0": 0, "y0": 0,
161 "xSize": xSize, "ySize": ySize}
162 science, sources = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel, noiseSeed=6,
163 nSrc=1, **kwargs)
164 matchedTemplate, _ = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel/4, noiseSeed=7,
165 nSrc=1, **kwargs)
166 rng = np.random.RandomState(3)
167 xLoc = np.arange(-5, xSize+5, 10)
168 rng.shuffle(xLoc)
169 yLoc = np.arange(-5, ySize+5, 10)
170 rng.shuffle(yLoc)
171 transients, transientSources = makeTestImage(seed=transientSeed,
172 nSrc=len(xLoc), fluxLevel=1000.,
173 noiseLevel=noiseLevel, noiseSeed=8,
174 xLoc=xLoc, yLoc=yLoc,
175 **kwargs)
176 difference = science.clone()
177 difference.maskedImage -= matchedTemplate.maskedImage
178 difference.maskedImage += transients.maskedImage
180 # Configure the detection Task
181 detectionTask = self._setup_detection(doForcedMeasurement=True)
183 # Run detection and check the results
184 output = detectionTask.run(science, matchedTemplate, difference)
186 for column in columnNames:
187 self._check_values(output.diaSources[column])
188 self._check_values(output.diaSources.getX(), minValue=0, maxValue=xSize)
189 self._check_values(output.diaSources.getY(), minValue=0, maxValue=ySize)
190 self._check_values(output.diaSources.getPsfInstFlux())
192 def test_detect_transients(self):
193 """Run detection on a difference image containing transients.
194 """
195 # Set up the simulated images
196 noiseLevel = 1.
197 staticSeed = 1
198 transientSeed = 6
199 fluxLevel = 500
200 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
201 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
202 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
204 # Configure the detection Task
205 detectionTask = self._setup_detection()
206 kwargs["seed"] = transientSeed
207 kwargs["nSrc"] = 10
208 kwargs["fluxLevel"] = 1000
210 # Run detection and check the results
211 def _detection_wrapper(positive=True):
212 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs)
213 difference = science.clone()
214 difference.maskedImage -= matchedTemplate.maskedImage
215 if positive:
216 difference.maskedImage += transients.maskedImage
217 else:
218 difference.maskedImage -= transients.maskedImage
219 output = detectionTask.run(science, matchedTemplate, difference)
220 refIds = []
221 scale = 1. if positive else -1.
222 for diaSource in output.diaSources:
223 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=scale)
224 _detection_wrapper(positive=True)
225 _detection_wrapper(positive=False)
227 def test_detect_dipoles(self):
228 """Run detection on a difference image containing dipoles.
229 """
230 # Set up the simulated images
231 noiseLevel = 1.
232 staticSeed = 1
233 fluxLevel = 1000
234 fluxRange = 1.5
235 nSources = 10
236 offset = 1
237 xSize = 300
238 ySize = 300
239 kernelSize = 32
240 # Avoid placing sources near the edge for this test, so that we can
241 # easily check that the correct number of sources are detected.
242 templateBorderSize = kernelSize//2
243 dipoleFlag = "ip_diffim_DipoleFit_flag_classification"
244 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "fluxRange": fluxRange,
245 "nSrc": nSources, "templateBorderSize": templateBorderSize, "kernelSize": kernelSize,
246 "xSize": xSize, "ySize": ySize}
247 dipoleFlag = "ip_diffim_DipoleFit_flag_classification"
248 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
249 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
250 difference = science.clone()
251 matchedTemplate.image.array[...] = np.roll(matchedTemplate.image.array[...], offset, axis=0)
252 matchedTemplate.variance.array[...] = np.roll(matchedTemplate.variance.array[...], offset, axis=0)
253 matchedTemplate.mask.array[...] = np.roll(matchedTemplate.mask.array[...], offset, axis=0)
254 difference.maskedImage -= matchedTemplate.maskedImage[science.getBBox()]
256 # Configure the detection Task
257 detectionTask = self._setup_detection()
259 # Run detection and check the results
260 output = detectionTask.run(science, matchedTemplate, difference)
261 self.assertIn(dipoleFlag, output.diaSources.schema.getNames())
262 nSourcesDet = len(sources)
263 self.assertEqual(len(output.diaSources), 2*nSourcesDet)
264 refIds = []
265 # The diaSource check should fail if we don't merge positive and negative footprints
266 for diaSource in output.diaSources:
267 with self.assertRaises(AssertionError):
268 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0,
269 atol=np.sqrt(fluxRange*fluxLevel))
271 detectionTask2 = self._setup_detection(doMerge=True)
272 output2 = detectionTask2.run(science, matchedTemplate, difference)
273 self.assertEqual(len(output2.diaSources), nSourcesDet)
274 refIds = []
275 for diaSource in output2.diaSources:
276 if diaSource[dipoleFlag]:
277 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0,
278 rtol=0.05, atol=None, usePsfFlux=False)
279 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_orientation"], -90., atol=2.)
280 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_separation"], offset, rtol=0.1)
281 else:
282 raise ValueError("DiaSource with ID %s is not a dipole!", diaSource.getId())
284 def test_sky_sources(self):
285 """Add sky sources and check that they are sufficiently far from other
286 sources and have negligible flux.
287 """
288 # Set up the simulated images
289 noiseLevel = 1.
290 staticSeed = 1
291 transientSeed = 6
292 transientFluxLevel = 1000.
293 transientFluxRange = 1.5
294 fluxLevel = 500
295 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
296 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
297 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
298 transients, transientSources = makeTestImage(seed=transientSeed, psfSize=2.4,
299 nSrc=10, fluxLevel=transientFluxLevel,
300 fluxRange=transientFluxRange,
301 noiseLevel=noiseLevel, noiseSeed=8)
302 difference = science.clone()
303 difference.maskedImage -= matchedTemplate.maskedImage
304 difference.maskedImage += transients.maskedImage
305 kernelWidth = np.max(science.psf.getKernel().getDimensions())//2
307 # Configure the detection Task
308 detectionTask = self._setup_detection(doSkySources=True)
310 # Run detection and check the results
311 output = detectionTask.run(science, matchedTemplate, difference)
312 skySources = output.diaSources[output.diaSources["sky_source"]]
313 self.assertEqual(len(skySources), detectionTask.config.skySources.nSources)
314 for skySource in skySources:
315 # The sky sources should not be close to any other source
316 with self.assertRaises(AssertionError):
317 self._check_diaSource(transientSources, skySource, matchDistance=kernelWidth)
318 with self.assertRaises(AssertionError):
319 self._check_diaSource(sources, skySource, matchDistance=kernelWidth)
320 # The sky sources should have low flux levels.
321 self._check_diaSource(transientSources, skySource, matchDistance=1000, scale=0.,
322 atol=np.sqrt(transientFluxRange*transientFluxLevel))
324 def test_edge_detections(self):
325 """Sources with certain bad mask planes set should not be detected.
326 """
327 # Set up the simulated images
328 noiseLevel = 1.
329 staticSeed = 1
330 transientSeed = 6
331 fluxLevel = 500
332 radius = 2
333 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
334 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
335 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
337 _checkMask = subtractImages.AlardLuptonSubtractTask._checkMask
338 # Configure the detection Task
339 detectionTask = self._setup_detection()
340 excludeMaskPlanes = detectionTask.config.detection.excludeMaskPlanes
341 nBad = len(excludeMaskPlanes)
342 self.assertGreater(nBad, 0)
343 kwargs["seed"] = transientSeed
344 kwargs["nSrc"] = nBad
345 kwargs["fluxLevel"] = 1000
347 # Run detection and check the results
348 def _detection_wrapper(setFlags=True):
349 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs)
350 difference = science.clone()
351 difference.maskedImage -= matchedTemplate.maskedImage
352 difference.maskedImage += transients.maskedImage
353 if setFlags:
354 for src, badMask in zip(transientSources, excludeMaskPlanes):
355 srcX = int(src.getX())
356 srcY = int(src.getY())
357 srcBbox = lsst.geom.Box2I(lsst.geom.Point2I(srcX - radius, srcY - radius),
358 lsst.geom.Extent2I(2*radius + 1, 2*radius + 1))
359 difference[srcBbox].mask.array |= lsst.afw.image.Mask.getPlaneBitMask(badMask)
360 output = detectionTask.run(science, matchedTemplate, difference)
361 refIds = []
362 goodSrcFlags = _checkMask(difference.mask, transientSources, excludeMaskPlanes)
363 if setFlags:
364 self.assertEqual(np.sum(~goodSrcFlags), nBad)
365 else:
366 self.assertEqual(np.sum(~goodSrcFlags), 0)
367 for diaSource, goodSrcFlag in zip(output.diaSources, goodSrcFlags):
368 if ~goodSrcFlag:
369 with self.assertRaises(AssertionError):
370 self._check_diaSource(transientSources, diaSource, refIds=refIds)
371 else:
372 self._check_diaSource(transientSources, diaSource, refIds=refIds)
373 _detection_wrapper(setFlags=False)
374 _detection_wrapper(setFlags=True)
377class DetectAndMeasureScoreTest(DetectAndMeasureTestBase):
378 detectionTask = detectAndMeasure.DetectAndMeasureScoreTask
380 def test_detection_xy0(self):
381 """Basic functionality test with non-zero x0 and y0.
382 """
383 # Set up the simulated images
384 noiseLevel = 1.
385 staticSeed = 1
386 fluxLevel = 500
387 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 12345, "y0": 67890}
388 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
389 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
390 difference = science.clone()
391 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask()
392 scienceKernel = science.psf.getKernel()
393 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl)
395 # Configure the detection Task
396 detectionTask = self._setup_detection()
398 # Run detection and check the results
399 output = detectionTask.run(science, matchedTemplate, difference, score)
400 subtractedMeasuredExposure = output.subtractedMeasuredExposure
402 self.assertImagesEqual(subtractedMeasuredExposure.image, difference.image)
404 def test_measurements_finite(self):
405 """Measured fluxes and centroids should always be finite.
406 """
407 columnNames = ["coord_ra", "coord_dec", "ip_diffim_forced_PsfFlux_instFlux"]
409 # Set up the simulated images
410 noiseLevel = 1.
411 staticSeed = 1
412 transientSeed = 6
413 xSize = 256
414 ySize = 256
415 kwargs = {"psfSize": 2.4, "x0": 0, "y0": 0,
416 "xSize": xSize, "ySize": ySize}
417 science, sources = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel, noiseSeed=6,
418 nSrc=1, **kwargs)
419 matchedTemplate, _ = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel/4, noiseSeed=7,
420 nSrc=1, **kwargs)
421 rng = np.random.RandomState(3)
422 xLoc = np.arange(-5, xSize+5, 10)
423 rng.shuffle(xLoc)
424 yLoc = np.arange(-5, ySize+5, 10)
425 rng.shuffle(yLoc)
426 transients, transientSources = makeTestImage(seed=transientSeed,
427 nSrc=len(xLoc), fluxLevel=1000.,
428 noiseLevel=noiseLevel, noiseSeed=8,
429 xLoc=xLoc, yLoc=yLoc,
430 **kwargs)
431 difference = science.clone()
432 difference.maskedImage -= matchedTemplate.maskedImage
433 difference.maskedImage += transients.maskedImage
434 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask()
435 scienceKernel = science.psf.getKernel()
436 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl)
438 # Configure the detection Task
439 detectionTask = self._setup_detection(doForcedMeasurement=True)
441 # Run detection and check the results
442 output = detectionTask.run(science, matchedTemplate, difference, score)
444 for column in columnNames:
445 self._check_values(output.diaSources[column])
446 self._check_values(output.diaSources.getX(), minValue=0, maxValue=xSize)
447 self._check_values(output.diaSources.getY(), minValue=0, maxValue=ySize)
448 self._check_values(output.diaSources.getPsfInstFlux())
450 def test_detect_transients(self):
451 """Run detection on a difference image containing transients.
452 """
453 # Set up the simulated images
454 noiseLevel = 1.
455 staticSeed = 1
456 transientSeed = 6
457 fluxLevel = 500
458 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
459 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
460 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
461 scienceKernel = science.psf.getKernel()
462 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask()
464 # Configure the detection Task
465 detectionTask = self._setup_detection()
466 kwargs["seed"] = transientSeed
467 kwargs["nSrc"] = 10
468 kwargs["fluxLevel"] = 1000
470 # Run detection and check the results
471 def _detection_wrapper(positive=True):
472 """Simulate positive or negative transients and run detection.
474 Parameters
475 ----------
476 positive : `bool`, optional
477 If set, use positive transient sources.
478 """
480 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs)
481 difference = science.clone()
482 difference.maskedImage -= matchedTemplate.maskedImage
483 if positive:
484 difference.maskedImage += transients.maskedImage
485 else:
486 difference.maskedImage -= transients.maskedImage
487 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl)
488 output = detectionTask.run(science, matchedTemplate, difference, score)
489 refIds = []
490 scale = 1. if positive else -1.
491 goodSrcFlags = subtractTask._checkMask(score.mask, transientSources,
492 subtractTask.config.badMaskPlanes)
493 for diaSource, goodSrcFlag in zip(output.diaSources, goodSrcFlags):
494 if ~goodSrcFlag:
495 with self.assertRaises(AssertionError):
496 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=scale)
497 else:
498 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=scale)
499 _detection_wrapper(positive=True)
500 _detection_wrapper(positive=False)
502 def test_detect_dipoles(self):
503 """Run detection on a difference image containing dipoles.
504 """
505 # Set up the simulated images
506 noiseLevel = 1.
507 staticSeed = 1
508 fluxLevel = 1000
509 fluxRange = 1.5
510 nSources = 10
511 offset = 1
512 xSize = 300
513 ySize = 300
514 kernelSize = 32
515 # Avoid placing sources near the edge for this test, so that we can
516 # easily check that the correct number of sources are detected.
517 templateBorderSize = kernelSize//2
518 dipoleFlag = "ip_diffim_DipoleFit_flag_classification"
519 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "fluxRange": fluxRange,
520 "nSrc": nSources, "templateBorderSize": templateBorderSize, "kernelSize": kernelSize,
521 "xSize": xSize, "ySize": ySize}
522 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
523 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
524 difference = science.clone()
525 # Shift the template by a pixel in order to make dipoles in the difference image.
526 matchedTemplate.image.array[...] = np.roll(matchedTemplate.image.array[...], offset, axis=0)
527 matchedTemplate.variance.array[...] = np.roll(matchedTemplate.variance.array[...], offset, axis=0)
528 matchedTemplate.mask.array[...] = np.roll(matchedTemplate.mask.array[...], offset, axis=0)
529 difference.maskedImage -= matchedTemplate.maskedImage[science.getBBox()]
530 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask()
531 scienceKernel = science.psf.getKernel()
532 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl)
534 # Configure the detection Task
535 detectionTask = self._setup_detection()
537 # Run detection and check the results
538 output = detectionTask.run(science, matchedTemplate, difference, score)
539 self.assertIn(dipoleFlag, output.diaSources.schema.getNames())
540 nSourcesDet = len(sources)
541 # Since we did not merge the dipoles, each source should result in
542 # both a positive and a negative diaSource
543 self.assertEqual(len(output.diaSources), 2*nSourcesDet)
544 refIds = []
545 # The diaSource check should fail if we don't merge positive and negative footprints
546 for diaSource in output.diaSources:
547 with self.assertRaises(AssertionError):
548 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0,
549 atol=np.sqrt(fluxRange*fluxLevel))
551 detectionTask2 = self._setup_detection(doMerge=True)
552 output2 = detectionTask2.run(science, matchedTemplate, difference, score)
553 self.assertEqual(len(output2.diaSources), nSourcesDet)
554 refIds = []
555 for diaSource in output2.diaSources:
556 if diaSource[dipoleFlag]:
557 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0,
558 rtol=0.05, atol=None, usePsfFlux=False)
559 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_orientation"], -90., atol=2.)
560 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_separation"], offset, rtol=0.1)
561 else:
562 raise ValueError("DiaSource with ID %s is not a dipole!", diaSource.getId())
564 def test_sky_sources(self):
565 """Add sky sources and check that they are sufficiently far from other
566 sources and have negligible flux.
567 """
568 # Set up the simulated images
569 noiseLevel = 1.
570 staticSeed = 1
571 transientSeed = 6
572 transientFluxLevel = 1000.
573 transientFluxRange = 1.5
574 fluxLevel = 500
575 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
576 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
577 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
578 transients, transientSources = makeTestImage(seed=transientSeed, psfSize=2.4,
579 nSrc=10, fluxLevel=transientFluxLevel,
580 fluxRange=transientFluxRange,
581 noiseLevel=noiseLevel, noiseSeed=8)
582 difference = science.clone()
583 difference.maskedImage -= matchedTemplate.maskedImage
584 difference.maskedImage += transients.maskedImage
585 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask()
586 scienceKernel = science.psf.getKernel()
587 kernelWidth = np.max(scienceKernel.getDimensions())//2
588 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl)
590 # Configure the detection Task
591 detectionTask = self._setup_detection(doSkySources=True)
593 # Run detection and check the results
594 output = detectionTask.run(science, matchedTemplate, difference, score)
595 skySources = output.diaSources[output.diaSources["sky_source"]]
596 self.assertEqual(len(skySources), detectionTask.config.skySources.nSources)
597 for skySource in skySources:
598 # The sky sources should not be close to any other source
599 with self.assertRaises(AssertionError):
600 self._check_diaSource(transientSources, skySource, matchDistance=kernelWidth)
601 with self.assertRaises(AssertionError):
602 self._check_diaSource(sources, skySource, matchDistance=kernelWidth)
603 # The sky sources should have low flux levels.
604 self._check_diaSource(transientSources, skySource, matchDistance=1000, scale=0.,
605 atol=np.sqrt(transientFluxRange*transientFluxLevel))
607 def test_edge_detections(self):
608 """Sources with certain bad mask planes set should not be detected.
609 """
610 # Set up the simulated images
611 noiseLevel = 1.
612 staticSeed = 1
613 transientSeed = 6
614 fluxLevel = 500
615 radius = 2
616 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel}
617 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs)
618 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs)
620 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask()
621 scienceKernel = science.psf.getKernel()
622 # Configure the detection Task
623 detectionTask = self._setup_detection()
624 excludeMaskPlanes = detectionTask.config.detection.excludeMaskPlanes
625 nBad = len(excludeMaskPlanes)
626 self.assertGreater(nBad, 0)
627 kwargs["seed"] = transientSeed
628 kwargs["nSrc"] = nBad
629 kwargs["fluxLevel"] = 1000
631 # Run detection and check the results
632 def _detection_wrapper(setFlags=True):
633 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs)
634 difference = science.clone()
635 difference.maskedImage -= matchedTemplate.maskedImage
636 difference.maskedImage += transients.maskedImage
637 if setFlags:
638 for src, badMask in zip(transientSources, excludeMaskPlanes):
639 srcX = int(src.getX())
640 srcY = int(src.getY())
641 srcBbox = lsst.geom.Box2I(lsst.geom.Point2I(srcX - radius, srcY - radius),
642 lsst.geom.Extent2I(2*radius + 1, 2*radius + 1))
643 difference[srcBbox].mask.array |= lsst.afw.image.Mask.getPlaneBitMask(badMask)
644 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl)
645 output = detectionTask.run(science, matchedTemplate, difference, score)
646 refIds = []
647 goodSrcFlags = subtractTask._checkMask(difference.mask, transientSources, excludeMaskPlanes)
648 if setFlags:
649 self.assertEqual(np.sum(~goodSrcFlags), nBad)
650 else:
651 self.assertEqual(np.sum(~goodSrcFlags), 0)
652 for diaSource, goodSrcFlag in zip(output.diaSources, goodSrcFlags):
653 if ~goodSrcFlag:
654 with self.assertRaises(AssertionError):
655 self._check_diaSource(transientSources, diaSource, refIds=refIds)
656 else:
657 self._check_diaSource(transientSources, diaSource, refIds=refIds)
658 _detection_wrapper(setFlags=False)
659 _detection_wrapper(setFlags=True)
662def setup_module(module):
663 lsst.utils.tests.init()
666class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
667 pass
670if __name__ == "__main__": 670 ↛ 671line 670 didn't jump to line 671, because the condition on line 670 was never true
671 lsst.utils.tests.init()
672 unittest.main()