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