Coverage for tests/test_subtractTask.py: 6%
732 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 07:50 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 07:50 +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 unittest
24import lsst.afw.math as afwMath
25import lsst.afw.table as afwTable
26import lsst.geom
27import lsst.meas.algorithms as measAlg
28from lsst.ip.diffim import subtractImages
29from lsst.pex.config import FieldValidationError
30from lsst.pipe.base import NoWorkFound
31import lsst.utils.tests
32import numpy as np
33from lsst.ip.diffim.utils import (computeRobustStatistics, computePSFNoiseEquivalentArea,
34 evaluateMeanPsfFwhm, getPsfFwhm, makeStats, makeTestImage)
35from lsst.pex.exceptions import InvalidParameterError
38class CustomCoaddPsf(measAlg.CoaddPsf):
39 """A custom CoaddPSF that overrides the getAveragePosition method.
40 """
41 def getAveragePosition(self):
42 return lsst.geom.Point2D(-10000, -10000)
45class AlardLuptonSubtractTest(lsst.utils.tests.TestCase):
47 def test_allowed_config_modes(self):
48 """Verify the allowable modes for convolution.
49 """
50 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
51 config.mode = 'auto'
52 config.mode = 'convolveScience'
53 config.mode = 'convolveTemplate'
55 with self.assertRaises(FieldValidationError):
56 config.mode = 'aotu'
58 def test_mismatched_template(self):
59 """Test that an error is raised if the template
60 does not fully contain the science image.
61 """
62 xSize = 200
63 ySize = 200
64 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
65 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
66 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
67 task = subtractImages.AlardLuptonSubtractTask(config=config)
68 with self.assertRaises(AssertionError):
69 task.run(template, science, sources)
71 def test_incomplete_template_coverage(self):
72 noiseLevel = 1.
73 border = 20
74 xSize = 400
75 ySize = 400
76 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=50,
77 xSize=xSize, ySize=ySize)
78 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=50,
79 templateBorderSize=border, doApplyCalibration=True,
80 xSize=xSize, ySize=ySize)
82 science_height = science.getBBox().getDimensions().getY()
84 def _run_and_check_coverage(template_coverage,
85 requiredTemplateFraction=0.1,
86 minTemplateFractionForExpectedSuccess=0.2):
87 template_cut = template.clone()
88 template_height = int(science_height*template_coverage + border)
89 template_cut.image.array[:, template_height:] = 0
90 template_cut.mask.array[:, template_height:] = template_cut.mask.getPlaneBitMask('NO_DATA')
91 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
92 config.requiredTemplateFraction = requiredTemplateFraction
93 config.minTemplateFractionForExpectedSuccess = minTemplateFractionForExpectedSuccess
94 if template_coverage < requiredTemplateFraction:
95 doRaise = True
96 elif template_coverage < minTemplateFractionForExpectedSuccess:
97 doRaise = True
98 else:
99 doRaise = False
100 task = subtractImages.AlardLuptonSubtractTask(config=config)
101 if doRaise:
102 with self.assertRaises(NoWorkFound):
103 task.run(template_cut, science.clone(), sources.copy(deep=True))
104 else:
105 task.run(template_cut, science.clone(), sources.copy(deep=True))
106 _run_and_check_coverage(template_coverage=0.09)
107 _run_and_check_coverage(template_coverage=0.19)
108 _run_and_check_coverage(template_coverage=0.7)
110 def test_clear_template_mask(self):
111 noiseLevel = 1.
112 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
113 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
114 templateBorderSize=20, doApplyCalibration=True)
115 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"]
116 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
117 config.doSubtractBackground = False
118 config.mode = "convolveTemplate"
119 # Ensure that each each mask plane is set for some pixels
120 mask = template.mask
121 x0 = 50
122 x1 = 75
123 y0 = 150
124 y1 = 175
125 scienceMaskCheck = {}
126 for maskPlane in mask.getMaskPlaneDict().keys():
127 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0)
128 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane)
129 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
131 task = subtractImages.AlardLuptonSubtractTask(config=config)
132 output = task.run(template, science, sources)
133 # Verify that the template mask has been modified in place
134 for maskPlane in mask.getMaskPlaneDict().keys():
135 if maskPlane in diffimEmptyMaskPlanes:
136 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
137 elif maskPlane in config.preserveTemplateMask:
138 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
139 else:
140 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
141 # Mask planes set in the science image should also be set in the difference
142 # Except the "DETECTED" planes should have been cleared
143 diffimMask = output.difference.mask
144 for maskPlane, scienceSum in scienceMaskCheck.items():
145 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0)
146 if maskPlane in diffimEmptyMaskPlanes:
147 self.assertEqual(diffimSum, 0)
148 else:
149 self.assertTrue(diffimSum >= scienceSum)
151 def test_equal_images(self):
152 """Test that running with enough sources produces reasonable output,
153 with the same size psf in the template and science.
154 """
155 noiseLevel = 1.
156 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
157 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
158 templateBorderSize=20, doApplyCalibration=True)
159 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
160 config.doSubtractBackground = False
161 task = subtractImages.AlardLuptonSubtractTask(config=config)
162 output = task.run(template, science, sources)
163 # There shoud be no NaN values in the difference image
164 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
165 # Mean of difference image should be close to zero.
166 meanError = noiseLevel/np.sqrt(output.difference.image.array.size)
167 # Make sure to include pixels with the DETECTED mask bit set.
168 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA", "DETECTED", "DETECTED_NEGATIVE"))
169 differenceMean = computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl)
170 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError)
171 # stddev of difference image should be close to expected value.
172 differenceStd = computeRobustStatistics(output.difference.image, output.difference.mask,
173 makeStats(), statistic=afwMath.STDEV)
174 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1)
176 def test_psf_size(self):
177 """Test that the image subtract task runs without failing, if
178 fwhmExposureBuffer and fwhmExposureGrid parameters are set.
179 """
180 noiseLevel = 1.
181 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
182 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
183 templateBorderSize=20, doApplyCalibration=True)
185 schema = afwTable.ExposureTable.makeMinimalSchema()
186 weightKey = schema.addField("weight", type="D", doc="Coadd weight")
187 exposureCatalog = afwTable.ExposureCatalog(schema)
188 kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
189 psf = measAlg.KernelPsf(kernel, template.getBBox().getCenter())
191 record = exposureCatalog.addNew()
192 record.setPsf(psf)
193 record.setWcs(template.wcs)
194 record.setD(weightKey, 1.0)
195 record.setBBox(template.getBBox())
197 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs)
198 template.setPsf(customPsf)
200 # Test that we get an exception if we simply get the FWHM at center.
201 with self.assertRaises(InvalidParameterError):
202 getPsfFwhm(template.psf, True)
204 with self.assertRaises(InvalidParameterError):
205 getPsfFwhm(template.psf, False)
207 # Test that evaluateMeanPsfFwhm runs successfully on the template.
208 evaluateMeanPsfFwhm(template, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
210 # Since the PSF is spatially invariant, the FWHM should be the same at
211 # all points in the science image.
212 fwhm1 = getPsfFwhm(science.psf, False)
213 fwhm2 = evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
214 self.assertAlmostEqual(fwhm1[0], fwhm2, places=13)
215 self.assertAlmostEqual(fwhm1[1], fwhm2, places=13)
217 self.assertAlmostEqual(evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05,
218 fwhmExposureGrid=10),
219 getPsfFwhm(science.psf, True), places=7
220 )
222 # Test that the image subtraction task runs successfully.
223 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
224 config.doSubtractBackground = False
225 task = subtractImages.AlardLuptonSubtractTask(config=config)
227 # Test that the task runs if we take the mean FWHM on a grid.
228 with self.assertLogs(level="INFO") as cm:
229 task.run(template, science, sources)
231 # Check that evaluateMeanPsfFwhm was called.
232 # This tests that getPsfFwhm failed raising InvalidParameterError,
233 # that is caught and handled appropriately.
234 logMessage = ("INFO:lsst.alardLuptonSubtract:Unable to evaluate PSF at the average position. "
235 "Evaluting PSF on a grid of points."
236 )
237 self.assertIn(logMessage, cm.output)
239 def test_auto_convolveTemplate(self):
240 """Test that auto mode gives the same result as convolveTemplate when
241 the template psf is the smaller.
242 """
243 noiseLevel = 1.
244 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
245 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
246 templateBorderSize=20, doApplyCalibration=True)
247 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
248 config.doSubtractBackground = False
249 config.mode = "convolveTemplate"
251 task = subtractImages.AlardLuptonSubtractTask(config=config)
252 output = task.run(template.clone(), science.clone(), sources)
254 config.mode = "auto"
255 task = subtractImages.AlardLuptonSubtractTask(config=config)
256 outputAuto = task.run(template, science, sources)
257 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
259 def test_auto_convolveScience(self):
260 """Test that auto mode gives the same result as convolveScience when
261 the science psf is the smaller.
262 """
263 noiseLevel = 1.
264 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
265 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
266 templateBorderSize=20, doApplyCalibration=True)
267 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
268 config.doSubtractBackground = False
269 config.mode = "convolveScience"
271 task = subtractImages.AlardLuptonSubtractTask(config=config)
272 output = task.run(template.clone(), science.clone(), sources)
274 config.mode = "auto"
275 task = subtractImages.AlardLuptonSubtractTask(config=config)
276 outputAuto = task.run(template, science, sources)
277 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
279 def test_science_better(self):
280 """Test that running with enough sources produces reasonable output,
281 with the science psf being smaller than the template.
282 """
283 statsCtrl = makeStats()
284 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
286 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
287 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
288 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
289 templateBorderSize=20, doApplyCalibration=True)
290 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
291 config.doSubtractBackground = False
292 config.mode = "convolveScience"
293 task = subtractImages.AlardLuptonSubtractTask(config=config)
294 output = task.run(template, science, sources)
295 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
296 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
297 # Mean of difference image should be close to zero.
298 nGoodPix = np.sum(np.isfinite(output.difference.image.array))
299 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix)
300 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
301 statsCtrlDetect)
303 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
304 # stddev of difference image should be close to expected value.
305 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
306 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
307 statsCtrl)
308 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
309 statsCtrl, statistic=afwMath.STDEV)
310 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
311 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
313 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
314 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
315 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
317 def test_template_better(self):
318 """Test that running with enough sources produces reasonable output,
319 with the template psf being smaller than the science.
320 """
321 statsCtrl = makeStats()
322 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
324 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
325 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
326 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
327 templateBorderSize=20, doApplyCalibration=True)
328 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
329 config.doSubtractBackground = False
330 task = subtractImages.AlardLuptonSubtractTask(config=config)
331 output = task.run(template, science, sources)
332 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
333 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
334 # There should be no NaNs in the image if we convolve the template with a buffer
335 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
336 # Mean of difference image should be close to zero.
337 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size)
339 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
340 statsCtrlDetect)
341 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
342 # stddev of difference image should be close to expected value.
343 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
344 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
345 statsCtrl)
346 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
347 statsCtrl, statistic=afwMath.STDEV)
348 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
349 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
351 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
352 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
353 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
355 def test_symmetry(self):
356 """Test that convolving the science and convolving the template are
357 symmetric: if the psfs are switched between them, the difference image
358 should be nearly the same.
359 """
360 noiseLevel = 1.
361 # Don't include a border for the template, in order to make the results
362 # comparable when we swap which image is treated as the "science" image.
363 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
364 noiseSeed=6, templateBorderSize=0, doApplyCalibration=True)
365 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
366 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True)
367 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
368 config.mode = 'auto'
369 config.doSubtractBackground = False
370 task = subtractImages.AlardLuptonSubtractTask(config=config)
372 # The science image will be modified in place, so use a copy for the second run.
373 science_better = task.run(template.clone(), science.clone(), sources)
374 template_better = task.run(science, template, sources)
376 delta = template_better.difference.clone()
377 delta.image -= science_better.difference.image
378 delta.variance -= science_better.difference.variance
379 delta.mask.array -= science_better.difference.mask.array
381 statsCtrl = makeStats()
382 # Mean of delta should be very close to zero.
383 nGoodPix = np.sum(np.isfinite(delta.image.array))
384 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
385 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
386 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV)
387 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
388 # stddev of difference image should be close to expected value
389 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1)
391 def test_few_sources(self):
392 """Test with only 1 source, to check that we get a useful error.
393 """
394 xSize = 256
395 ySize = 256
396 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
397 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
398 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
399 task = subtractImages.AlardLuptonSubtractTask(config=config)
400 sources = sources[0:1]
401 with self.assertRaisesRegex(RuntimeError,
402 "Cannot compute PSF matching kernel: too few sources selected."):
403 task.run(template, science, sources)
405 def test_kernel_source_selector(self):
406 """Check that kernel source selection behaves as expected.
407 """
408 xSize = 256
409 ySize = 256
410 nSourcesSimulated = 20
411 science, sources = makeTestImage(psfSize=2.4, nSrc=nSourcesSimulated,
412 xSize=xSize, ySize=ySize)
413 template, _ = makeTestImage(psfSize=2.0, nSrc=nSourcesSimulated,
414 xSize=xSize, ySize=ySize, doApplyCalibration=True)
415 badSourceFlag = "slot_Centroid_flag"
417 def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3):
418 sources = sourcesIn.copy(deep=True)
419 # Verify that source flags are not set in the input catalog
420 self.assertEqual(np.sum(sources[badSourceFlag]), 0)
421 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
422 config.badSourceFlags = [badSourceFlag, ]
423 config.maxKernelSources = maxKernelSources
424 config.minKernelSources = minKernelSources
426 task = subtractImages.AlardLuptonSubtractTask(config=config)
427 nSources = len(sources)
428 # Flag a third of the sources
429 sources[0:: 3][badSourceFlag] = True
430 nBadSources = np.sum(sources[badSourceFlag])
431 if maxKernelSources > 0:
432 nGoodSources = np.minimum(nSources - nBadSources, maxKernelSources)
433 else:
434 nGoodSources = nSources - nBadSources
436 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
437 signalToNoise = signalToNoise[~sources[badSourceFlag]]
438 signalToNoise.sort()
439 selectSources = task._sourceSelector(sources, science.mask)
440 self.assertEqual(nGoodSources, len(selectSources))
441 signalToNoiseOut = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
442 signalToNoiseOut.sort()
443 self.assertFloatsAlmostEqual(signalToNoise[-nGoodSources:], signalToNoiseOut)
445 _run_and_check_sources(sources)
446 _run_and_check_sources(sources, maxKernelSources=len(sources)//3)
447 _run_and_check_sources(sources, maxKernelSources=-1)
448 with self.assertRaises(RuntimeError):
449 _run_and_check_sources(sources, minKernelSources=1000)
451 def test_order_equal_images(self):
452 """Verify that the result is the same regardless of convolution mode
453 if the images are equivalent.
454 """
455 noiseLevel = .1
456 seed1 = 6
457 seed2 = 7
458 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1,
459 clearEdgeMask=True)
460 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
461 templateBorderSize=0, doApplyCalibration=True,
462 clearEdgeMask=True)
463 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
464 config1.mode = "convolveTemplate"
465 config1.doSubtractBackground = False
466 task1 = subtractImages.AlardLuptonSubtractTask(config=config1)
467 results_convolveTemplate = task1.run(template1, science1, sources1)
469 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1,
470 clearEdgeMask=True)
471 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
472 templateBorderSize=0, doApplyCalibration=True,
473 clearEdgeMask=True)
474 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
475 config2.mode = "convolveScience"
476 config2.doSubtractBackground = False
477 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
478 results_convolveScience = task2.run(template2, science2, sources2)
479 bbox = results_convolveTemplate.difference.getBBox().clippedTo(
480 results_convolveScience.difference.getBBox())
481 diff1 = science1.maskedImage.clone()[bbox]
482 diff1 -= template1.maskedImage[bbox]
483 diff2 = science2.maskedImage.clone()[bbox]
484 diff2 -= template2.maskedImage[bbox]
485 self.assertFloatsAlmostEqual(results_convolveTemplate.difference[bbox].image.array,
486 diff1.image.array,
487 atol=noiseLevel*5.)
488 self.assertFloatsAlmostEqual(results_convolveScience.difference[bbox].image.array,
489 diff2.image.array,
490 atol=noiseLevel*5.)
491 diffErr = noiseLevel*2
492 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference[bbox].maskedImage,
493 results_convolveScience.difference[bbox].maskedImage,
494 atol=diffErr*5.)
496 def test_background_subtraction(self):
497 """Check that we can recover the background,
498 and that it is subtracted correctly in the difference image.
499 """
500 noiseLevel = 1.
501 xSize = 512
502 ySize = 512
503 x0 = 123
504 y0 = 456
505 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
506 templateBorderSize=20,
507 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
508 doApplyCalibration=True)
509 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
511 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
512 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
513 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
514 background=background_model,
515 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
516 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
517 config.doSubtractBackground = True
519 config.makeKernel.kernel.name = "AL"
520 config.makeKernel.kernel.active.fitForBackground = True
521 config.makeKernel.kernel.active.spatialKernelOrder = 1
522 config.makeKernel.kernel.active.spatialBgOrder = 2
523 statsCtrl = makeStats()
525 def _run_and_check_images(config, statsCtrl, mode):
526 """Check that the fit background matches the input model.
527 """
528 config.mode = mode
529 task = subtractImages.AlardLuptonSubtractTask(config=config)
530 output = task.run(template.clone(), science.clone(), sources)
532 # We should be fitting the same number of parameters as were in the input model
533 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
535 # The parameters of the background fit should be close to the input model
536 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
537 np.array(params), rtol=0.3)
539 # stddev of difference image should be close to expected value.
540 # This will fail if we have mis-subtracted the background.
541 stdVal = computeRobustStatistics(output.difference.image, output.difference.mask,
542 statsCtrl, statistic=afwMath.STDEV)
543 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
545 _run_and_check_images(config, statsCtrl, "convolveTemplate")
546 _run_and_check_images(config, statsCtrl, "convolveScience")
548 def test_scale_variance_convolve_template(self):
549 """Check variance scaling of the image difference.
550 """
551 scienceNoiseLevel = 4.
552 templateNoiseLevel = 2.
553 scaleFactor = 1.345
554 # Make sure to include pixels with the DETECTED mask bit set.
555 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
557 def _run_and_check_images(science, template, sources, statsCtrl,
558 doDecorrelation, doScaleVariance, scaleFactor=1.):
559 """Check that the variance plane matches the expected value for
560 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
561 """
563 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
564 config.doSubtractBackground = False
565 config.doDecorrelation = doDecorrelation
566 config.doScaleVariance = doScaleVariance
567 task = subtractImages.AlardLuptonSubtractTask(config=config)
568 output = task.run(template.clone(), science.clone(), sources)
569 if doScaleVariance:
570 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
571 scaleFactor, atol=0.05)
572 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
573 scaleFactor, atol=0.05)
575 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
576 if doDecorrelation:
577 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
578 else:
579 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
580 output.matchedTemplate.mask,
581 statsCtrl)
583 if doScaleVariance:
584 templateNoise *= scaleFactor
585 scienceNoise *= scaleFactor
586 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
587 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
589 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
590 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
591 templateBorderSize=20, doApplyCalibration=True)
592 # Verify that the variance plane of the difference image is correct
593 # when the template and science variance planes are correct
594 _run_and_check_images(science, template, sources, statsCtrl,
595 doDecorrelation=True, doScaleVariance=True)
596 _run_and_check_images(science, template, sources, statsCtrl,
597 doDecorrelation=True, doScaleVariance=False)
598 _run_and_check_images(science, template, sources, statsCtrl,
599 doDecorrelation=False, doScaleVariance=True)
600 _run_and_check_images(science, template, sources, statsCtrl,
601 doDecorrelation=False, doScaleVariance=False)
603 # Verify that the variance plane of the difference image is correct
604 # when the template variance plane is incorrect
605 template.variance.array /= scaleFactor
606 science.variance.array /= scaleFactor
607 _run_and_check_images(science, template, sources, statsCtrl,
608 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
609 _run_and_check_images(science, template, sources, statsCtrl,
610 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
611 _run_and_check_images(science, template, sources, statsCtrl,
612 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
613 _run_and_check_images(science, template, sources, statsCtrl,
614 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
616 def test_scale_variance_convolve_science(self):
617 """Check variance scaling of the image difference.
618 """
619 scienceNoiseLevel = 4.
620 templateNoiseLevel = 2.
621 scaleFactor = 1.345
622 # Make sure to include pixels with the DETECTED mask bit set.
623 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
625 def _run_and_check_images(science, template, sources, statsCtrl,
626 doDecorrelation, doScaleVariance, scaleFactor=1.):
627 """Check that the variance plane matches the expected value for
628 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
629 """
631 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
632 config.mode = "convolveScience"
633 config.doSubtractBackground = False
634 config.doDecorrelation = doDecorrelation
635 config.doScaleVariance = doScaleVariance
636 task = subtractImages.AlardLuptonSubtractTask(config=config)
637 output = task.run(template.clone(), science.clone(), sources)
638 if doScaleVariance:
639 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
640 scaleFactor, atol=0.05)
641 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
642 scaleFactor, atol=0.05)
644 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
645 if doDecorrelation:
646 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
647 else:
648 scienceNoise = computeRobustStatistics(output.matchedScience.variance,
649 output.matchedScience.mask,
650 statsCtrl)
652 if doScaleVariance:
653 templateNoise *= scaleFactor
654 scienceNoise *= scaleFactor
656 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
657 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
659 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
660 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
661 templateBorderSize=20, doApplyCalibration=True)
662 # Verify that the variance plane of the difference image is correct
663 # when the template and science variance planes are correct
664 _run_and_check_images(science, template, sources, statsCtrl,
665 doDecorrelation=True, doScaleVariance=True)
666 _run_and_check_images(science, template, sources, statsCtrl,
667 doDecorrelation=True, doScaleVariance=False)
668 _run_and_check_images(science, template, sources, statsCtrl,
669 doDecorrelation=False, doScaleVariance=True)
670 _run_and_check_images(science, template, sources, statsCtrl,
671 doDecorrelation=False, doScaleVariance=False)
673 # Verify that the variance plane of the difference image is correct
674 # when the template and science variance planes are incorrect
675 science.variance.array /= scaleFactor
676 template.variance.array /= scaleFactor
677 _run_and_check_images(science, template, sources, statsCtrl,
678 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
679 _run_and_check_images(science, template, sources, statsCtrl,
680 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
681 _run_and_check_images(science, template, sources, statsCtrl,
682 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
683 _run_and_check_images(science, template, sources, statsCtrl,
684 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
686 def test_exposure_properties_convolve_template(self):
687 """Check that all necessary exposure metadata is included
688 when the template is convolved.
689 """
690 noiseLevel = 1.
691 seed = 37
692 rng = np.random.RandomState(seed)
693 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
694 psf = science.psf
695 psfAvgPos = psf.getAveragePosition()
696 psfSize = getPsfFwhm(science.psf)
697 psfImg = psf.computeKernelImage(psfAvgPos)
698 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
699 templateBorderSize=20, doApplyCalibration=True)
701 # Generate a random aperture correction map
702 apCorrMap = lsst.afw.image.ApCorrMap()
703 for name in ("a", "b", "c"):
704 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
705 science.info.setApCorrMap(apCorrMap)
707 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
708 config.mode = "convolveTemplate"
710 def _run_and_check_images(doDecorrelation):
711 """Check that the metadata is correct with or without decorrelation.
712 """
713 config.doDecorrelation = doDecorrelation
714 task = subtractImages.AlardLuptonSubtractTask(config=config)
715 output = task.run(template.clone(), science.clone(), sources)
716 psfOut = output.difference.psf
717 psfAvgPos = psfOut.getAveragePosition()
718 if doDecorrelation:
719 # Decorrelation requires recalculating the PSF,
720 # so it will not be the same as the input
721 psfOutSize = getPsfFwhm(science.psf)
722 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
723 else:
724 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
725 self.assertImagesAlmostEqual(psfImg, psfOutImg)
727 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
728 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
729 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
730 self.assertEqual(science.filter, output.difference.filter)
731 self.assertEqual(science.photoCalib, output.difference.photoCalib)
732 _run_and_check_images(doDecorrelation=True)
733 _run_and_check_images(doDecorrelation=False)
735 def test_exposure_properties_convolve_science(self):
736 """Check that all necessary exposure metadata is included
737 when the science image is convolved.
738 """
739 noiseLevel = 1.
740 seed = 37
741 rng = np.random.RandomState(seed)
742 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
743 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
744 templateBorderSize=20, doApplyCalibration=True)
745 psf = template.psf
746 psfAvgPos = psf.getAveragePosition()
747 psfSize = getPsfFwhm(template.psf)
748 psfImg = psf.computeKernelImage(psfAvgPos)
750 # Generate a random aperture correction map
751 apCorrMap = lsst.afw.image.ApCorrMap()
752 for name in ("a", "b", "c"):
753 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
754 science.info.setApCorrMap(apCorrMap)
756 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
757 config.mode = "convolveScience"
759 def _run_and_check_images(doDecorrelation):
760 """Check that the metadata is correct with or without decorrelation.
761 """
762 config.doDecorrelation = doDecorrelation
763 task = subtractImages.AlardLuptonSubtractTask(config=config)
764 output = task.run(template.clone(), science.clone(), sources)
765 if doDecorrelation:
766 # Decorrelation requires recalculating the PSF,
767 # so it will not be the same as the input
768 psfOutSize = getPsfFwhm(template.psf)
769 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
770 else:
771 psfOut = output.difference.psf
772 psfAvgPos = psfOut.getAveragePosition()
773 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
774 self.assertImagesAlmostEqual(psfImg, psfOutImg)
776 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
777 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
778 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
779 self.assertEqual(science.filter, output.difference.filter)
780 self.assertEqual(science.photoCalib, output.difference.photoCalib)
782 _run_and_check_images(doDecorrelation=True)
783 _run_and_check_images(doDecorrelation=False)
785 def _compare_apCorrMaps(self, a, b):
786 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
787 same addresses (i.e. so we can compare after serialization).
789 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
791 Parameters
792 ----------
793 a, b : `lsst.afw.image.ApCorrMap`
794 The two aperture correction maps to compare.
795 """
796 self.assertEqual(len(a), len(b))
797 for name, value in list(a.items()):
798 value2 = b.get(name)
799 self.assertIsNotNone(value2)
800 self.assertEqual(value.getBBox(), value2.getBBox())
801 self.assertFloatsAlmostEqual(
802 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
804 def test_fake_mask_plane_propagation(self):
805 """Test that we have the mask planes related to fakes in diffim images.
806 This is testing method called updateMasks
807 """
808 xSize = 200
809 ySize = 200
810 science, sources = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize)
811 science_fake_img, science_fake_sources = makeTestImage(
812 psfSize=2.4, xSize=xSize, ySize=ySize, seed=7, nSrc=2, noiseLevel=0.25, fluxRange=1
813 )
814 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
815 tmplt_fake_img, tmplt_fake_sources = makeTestImage(
816 psfSize=2.4, xSize=xSize, ySize=ySize, seed=9, nSrc=2, noiseLevel=0.25, fluxRange=1
817 )
818 # created fakes and added them to the images
819 science.image.array += science_fake_img.image.array
820 template.image.array += tmplt_fake_img.image.array
822 # TODO: DM-40796 update to INJECTED names when source injection gets refactored
823 # adding mask planes to both science and template images
824 science_mask_planes = science.mask.addMaskPlane("FAKE")
825 template_mask_planes = template.mask.addMaskPlane("FAKE")
827 for a_science_source in science_fake_sources:
828 # 3 x 3 masking of the source locations is fine
829 bbox = lsst.geom.Box2I(
830 lsst.geom.Point2I(a_science_source.getX(), a_science_source.getY()), lsst.geom.Extent2I(3, 3)
831 )
832 science[bbox].mask.array |= science_mask_planes
834 for a_template_source in tmplt_fake_sources:
835 # 3 x 3 masking of the source locations is fine
836 bbox = lsst.geom.Box2I(
837 lsst.geom.Point2I(a_template_source.getX(), a_template_source.getY()),
838 lsst.geom.Extent2I(3, 3)
839 )
840 template[bbox].mask.array |= template_mask_planes
842 science_fake_masked = (science.mask.array & science.mask.getPlaneBitMask("FAKE")) > 0
843 template_fake_masked = (template.mask.array & template.mask.getPlaneBitMask("FAKE")) > 0
845 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
846 task = subtractImages.AlardLuptonSubtractTask(config=config)
847 subtraction = task.run(template, science, sources)
849 # check subtraction mask plane is set where we set the previous masks
850 diff_mask = subtraction.difference.mask
852 # science mask should be now in INJECTED
853 inj_masked = (diff_mask.array & diff_mask.getPlaneBitMask("INJECTED")) > 0
855 # template mask should be now in INJECTED_TEMPLATE
856 injTmplt_masked = (diff_mask.array & diff_mask.getPlaneBitMask("INJECTED_TEMPLATE")) > 0
858 self.assertEqual(np.sum(inj_masked.astype(int)-science_fake_masked.astype(int)), 0)
859 self.assertEqual(np.sum(injTmplt_masked.astype(int)-template_fake_masked.astype(int)), 0)
862class AlardLuptonPreconvolveSubtractTest(lsst.utils.tests.TestCase):
864 def test_mismatched_template(self):
865 """Test that an error is raised if the template
866 does not fully contain the science image.
867 """
868 xSize = 200
869 ySize = 200
870 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
871 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
872 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
873 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
874 with self.assertRaises(AssertionError):
875 task.run(template, science, sources)
877 def test_equal_images(self):
878 """Test that running with enough sources produces reasonable output,
879 with the same size psf in the template and science.
880 """
881 noiseLevel = 1.
882 xSize = 400
883 ySize = 400
884 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6,
885 xSize=xSize, ySize=ySize)
886 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
887 templateBorderSize=20, doApplyCalibration=True,
888 xSize=xSize, ySize=ySize)
889 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
890 config.doSubtractBackground = False
891 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
892 output = task.run(template, science, sources)
893 # There shoud be no NaN values in the Score image
894 self.assertTrue(np.all(np.isfinite(output.scoreExposure.image.array)))
895 # Mean of Score image should be close to zero.
896 meanError = noiseLevel/np.sqrt(output.scoreExposure.image.array.size)
897 # Make sure to include pixels with the DETECTED mask bit set.
898 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
899 scoreMean = computeRobustStatistics(output.scoreExposure.image,
900 output.scoreExposure.mask,
901 statsCtrl)
902 self.assertFloatsAlmostEqual(scoreMean, 0, atol=5*meanError)
903 nea = computePSFNoiseEquivalentArea(science.psf)
904 # stddev of Score image should be close to expected value.
905 scoreStd = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
906 statsCtrl=statsCtrl, statistic=afwMath.STDEV)
907 self.assertFloatsAlmostEqual(scoreStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
909 def test_incomplete_template_coverage(self):
910 noiseLevel = 1.
911 border = 20
912 xSize = 400
913 ySize = 400
914 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6,
915 xSize=xSize, ySize=ySize)
916 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
917 templateBorderSize=border, doApplyCalibration=True,
918 xSize=xSize, ySize=ySize)
920 science_height = science.getBBox().getDimensions().getY()
922 def _run_and_check_coverage(template_coverage):
923 template_cut = template.clone()
924 template_height = int(science_height*template_coverage + border)
925 template_cut.image.array[:, template_height:] = 0
926 template_cut.mask.array[:, template_height:] = template_cut.mask.getPlaneBitMask('NO_DATA')
927 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
928 if template_coverage < config.requiredTemplateFraction:
929 doRaise = True
930 elif template_coverage < config.minTemplateFractionForExpectedSuccess:
931 doRaise = True
932 else:
933 doRaise = False
934 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
935 if doRaise:
936 with self.assertRaises(NoWorkFound):
937 task.run(template_cut, science.clone(), sources.copy(deep=True))
938 else:
939 task.run(template_cut, science.clone(), sources.copy(deep=True))
940 _run_and_check_coverage(template_coverage=0.09)
941 _run_and_check_coverage(template_coverage=0.19)
942 _run_and_check_coverage(template_coverage=.7)
944 def test_clear_template_mask(self):
945 noiseLevel = 1.
946 xSize = 400
947 ySize = 400
948 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6,
949 xSize=xSize, ySize=ySize)
950 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
951 templateBorderSize=20, doApplyCalibration=True,
952 xSize=xSize, ySize=ySize)
953 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"]
954 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
955 config.doSubtractBackground = False # Ensure that each each mask plane is set for some pixels
956 mask = template.mask
957 x0 = 50
958 x1 = 75
959 y0 = 150
960 y1 = 175
961 scienceMaskCheck = {}
962 for maskPlane in mask.getMaskPlaneDict().keys():
963 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0)
964 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane)
965 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
967 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
968 output = task.run(template, science, sources)
969 # Verify that the template mask has been modified in place
970 for maskPlane in mask.getMaskPlaneDict().keys():
971 if maskPlane in diffimEmptyMaskPlanes:
972 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
973 elif maskPlane in config.preserveTemplateMask:
974 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
975 else:
976 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
977 # Mask planes set in the science image should also be set in the difference
978 # Except the "DETECTED" planes should have been cleared
979 diffimMask = output.scoreExposure.mask
980 for maskPlane, scienceSum in scienceMaskCheck.items():
981 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0)
982 if maskPlane in diffimEmptyMaskPlanes:
983 self.assertEqual(diffimSum, 0)
984 else:
985 self.assertTrue(diffimSum >= scienceSum)
987 def test_agnostic_template_psf(self):
988 """Test that the Score image is the same whether the template PSF is
989 larger or smaller than the science image PSF.
990 """
991 noiseLevel = .3
992 xSize = 400
993 ySize = 400
994 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel,
995 noiseSeed=6, templateBorderSize=0,
996 xSize=xSize, ySize=ySize)
997 template1, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
998 noiseSeed=7, doApplyCalibration=True,
999 xSize=xSize, ySize=ySize)
1000 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
1001 noiseSeed=8, doApplyCalibration=True,
1002 xSize=xSize, ySize=ySize)
1003 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
1004 config.doSubtractBackground = False
1005 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
1007 science_better = task.run(template1, science.clone(), sources)
1008 template_better = task.run(template2, science, sources)
1009 bbox = science_better.scoreExposure.getBBox().clippedTo(template_better.scoreExposure.getBBox())
1011 delta = template_better.scoreExposure[bbox].clone()
1012 delta.image -= science_better.scoreExposure[bbox].image
1013 delta.variance -= science_better.scoreExposure[bbox].variance
1014 delta.mask.array &= science_better.scoreExposure[bbox].mask.array
1016 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
1017 # Mean of delta should be very close to zero.
1018 nGoodPix = np.sum(np.isfinite(delta.image.array))
1019 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
1020 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
1021 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl,
1022 statistic=afwMath.STDEV)
1023 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
1024 nea = computePSFNoiseEquivalentArea(science.psf)
1025 # stddev of Score image should be close to expected value
1026 self.assertFloatsAlmostEqual(deltaStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=.1)
1028 def test_few_sources(self):
1029 """Test with only 1 source, to check that we get a useful error.
1030 """
1031 xSize = 256
1032 ySize = 256
1033 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
1034 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
1035 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
1036 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
1037 sources = sources[0:1]
1038 with self.assertRaisesRegex(RuntimeError,
1039 "Cannot compute PSF matching kernel: too few sources selected."):
1040 task.run(template, science, sources)
1042 def test_background_subtraction(self):
1043 """Check that we can recover the background,
1044 and that it is subtracted correctly in the Score image.
1045 """
1046 noiseLevel = 1.
1047 xSize = 512
1048 ySize = 512
1049 x0 = 123
1050 y0 = 456
1051 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
1052 templateBorderSize=20,
1053 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
1054 doApplyCalibration=True)
1055 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
1057 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
1058 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
1059 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
1060 background=background_model,
1061 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
1062 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
1063 config.doSubtractBackground = True
1065 config.makeKernel.kernel.name = "AL"
1066 config.makeKernel.kernel.active.fitForBackground = True
1067 config.makeKernel.kernel.active.spatialKernelOrder = 1
1068 config.makeKernel.kernel.active.spatialBgOrder = 2
1069 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
1071 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
1072 output = task.run(template.clone(), science.clone(), sources)
1074 # We should be fitting the same number of parameters as were in the input model
1075 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
1077 # The parameters of the background fit should be close to the input model
1078 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
1079 np.array(params), rtol=0.2)
1081 # stddev of Score image should be close to expected value.
1082 # This will fail if we have mis-subtracted the background.
1083 stdVal = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
1084 statsCtrl, statistic=afwMath.STDEV)
1085 # get the img psf Noise Equivalent Area value
1086 nea = computePSFNoiseEquivalentArea(science.psf)
1087 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
1089 def test_scale_variance(self):
1090 """Check variance scaling of the Score image.
1091 """
1092 scienceNoiseLevel = 4.
1093 templateNoiseLevel = 2.
1094 scaleFactor = 1.345
1095 xSize = 400
1096 ySize = 400
1097 # Make sure to include pixels with the DETECTED mask bit set.
1098 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
1100 def _run_and_check_images(science, template, sources, statsCtrl,
1101 doDecorrelation, doScaleVariance, scaleFactor=1.):
1102 """Check that the variance plane matches the expected value for
1103 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
1104 """
1106 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
1107 config.doSubtractBackground = False
1108 config.doDecorrelation = doDecorrelation
1109 config.doScaleVariance = doScaleVariance
1110 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
1111 output = task.run(template.clone(), science.clone(), sources)
1112 if doScaleVariance:
1113 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
1114 scaleFactor, atol=0.05)
1115 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
1116 scaleFactor, atol=0.05)
1118 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
1119 # get the img psf Noise Equivalent Area value
1120 nea = computePSFNoiseEquivalentArea(science.psf)
1121 scienceNoise /= nea
1122 if doDecorrelation:
1123 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
1124 templateNoise /= nea
1125 else:
1126 # Don't divide by NEA in this case, since the template is convolved
1127 # and in the same units as the Score exposure.
1128 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
1129 output.matchedTemplate.mask,
1130 statsCtrl)
1131 if doScaleVariance:
1132 templateNoise *= scaleFactor
1133 scienceNoise *= scaleFactor
1134 varMean = computeRobustStatistics(output.scoreExposure.variance,
1135 output.scoreExposure.mask,
1136 statsCtrl)
1137 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
1139 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6,
1140 xSize=xSize, ySize=ySize)
1141 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
1142 templateBorderSize=20, doApplyCalibration=True,
1143 xSize=xSize, ySize=ySize)
1144 # Verify that the variance plane of the Score image is correct
1145 # when the template and science variance planes are correct
1146 _run_and_check_images(science, template, sources, statsCtrl,
1147 doDecorrelation=True, doScaleVariance=True)
1148 _run_and_check_images(science, template, sources, statsCtrl,
1149 doDecorrelation=True, doScaleVariance=False)
1150 _run_and_check_images(science, template, sources, statsCtrl,
1151 doDecorrelation=False, doScaleVariance=True)
1152 _run_and_check_images(science, template, sources, statsCtrl,
1153 doDecorrelation=False, doScaleVariance=False)
1155 # Verify that the variance plane of the Score image is correct
1156 # when the template variance plane is incorrect
1157 template.variance.array /= scaleFactor
1158 science.variance.array /= scaleFactor
1159 _run_and_check_images(science, template, sources, statsCtrl,
1160 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
1161 _run_and_check_images(science, template, sources, statsCtrl,
1162 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
1163 _run_and_check_images(science, template, sources, statsCtrl,
1164 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
1165 _run_and_check_images(science, template, sources, statsCtrl,
1166 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
1168 def test_exposure_properties(self):
1169 """Check that all necessary exposure metadata is included
1170 with the Score image.
1171 """
1172 noiseLevel = 1.
1173 xSize = 400
1174 ySize = 400
1175 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6,
1176 xSize=xSize, ySize=ySize)
1177 psf = science.psf
1178 psfAvgPos = psf.getAveragePosition()
1179 psfSize = getPsfFwhm(science.psf)
1180 psfImg = psf.computeKernelImage(psfAvgPos)
1181 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
1182 templateBorderSize=20, doApplyCalibration=True,
1183 xSize=xSize, ySize=ySize)
1185 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
1187 def _run_and_check_images(doDecorrelation):
1188 """Check that the metadata is correct with or without decorrelation.
1189 """
1190 config.doDecorrelation = doDecorrelation
1191 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
1192 output = task.run(template.clone(), science.clone(), sources)
1193 psfOut = output.scoreExposure.psf
1194 psfAvgPos = psfOut.getAveragePosition()
1195 if doDecorrelation:
1196 # Decorrelation requires recalculating the PSF,
1197 # so it will not be the same as the input
1198 psfOutSize = getPsfFwhm(science.psf)
1199 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
1200 else:
1201 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
1202 self.assertImagesAlmostEqual(psfImg, psfOutImg)
1204 # check PSF, WCS, bbox, filterLabel, photoCalib
1205 self.assertWcsAlmostEqualOverBBox(science.wcs, output.scoreExposure.wcs, science.getBBox())
1206 self.assertEqual(science.filter, output.scoreExposure.filter)
1207 self.assertEqual(science.photoCalib, output.scoreExposure.photoCalib)
1208 _run_and_check_images(doDecorrelation=True)
1209 _run_and_check_images(doDecorrelation=False)
1212def setup_module(module):
1213 lsst.utils.tests.init()
1216class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
1217 pass
1220if __name__ == "__main__": 1220 ↛ 1221line 1220 didn't jump to line 1221, because the condition on line 1220 was never true
1221 lsst.utils.tests.init()
1222 unittest.main()