Coverage for tests/test_subtractTask.py: 9%
420 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-09 12:05 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-09 12:05 +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
23import numpy as np
25import lsst.afw.geom as afwGeom
26from lsst.afw.image import PhotoCalib
27import lsst.afw.image as afwImage
28import lsst.afw.math as afwMath
29import lsst.geom
30from lsst.meas.algorithms.testUtils import plantSources
31import lsst.ip.diffim.imagePsfMatch
32from lsst.ip.diffim import subtractImages
33from lsst.ip.diffim.utils import getPsfFwhm
34from lsst.pex.config import FieldValidationError
35import lsst.utils.tests
38def makeFakeWcs():
39 """Make a fake, affine Wcs.
40 """
41 crpix = lsst.geom.Point2D(123.45, 678.9)
42 crval = lsst.geom.SpherePoint(0.1, 0.1, lsst.geom.degrees)
43 cdMatrix = np.array([[5.19513851e-05, -2.81124812e-07],
44 [-3.25186974e-07, -5.19112119e-05]])
45 return afwGeom.makeSkyWcs(crpix, crval, cdMatrix)
48def makeTestImage(seed=5, nSrc=20, psfSize=2., noiseLevel=5.,
49 noiseSeed=6, fluxLevel=500., fluxRange=2.,
50 kernelSize=32, templateBorderSize=0,
51 background=None,
52 xSize=256,
53 ySize=256,
54 x0=12345,
55 y0=67890,
56 calibration=1.,
57 doApplyCalibration=False,
58 ):
59 """Make a reproduceable PSF-convolved exposure for testing.
61 Parameters
62 ----------
63 seed : `int`, optional
64 Seed value to initialize the random number generator for sources.
65 nSrc : `int`, optional
66 Number of sources to simulate.
67 psfSize : `float`, optional
68 Width of the PSF of the simulated sources, in pixels.
69 noiseLevel : `float`, optional
70 Standard deviation of the noise to add to each pixel.
71 noiseSeed : `int`, optional
72 Seed value to initialize the random number generator for noise.
73 fluxLevel : `float`, optional
74 Reference flux of the simulated sources.
75 fluxRange : `float`, optional
76 Range in flux amplitude of the simulated sources.
77 kernelSize : `int`, optional
78 Size in pixels of the kernel for simulating sources.
79 templateBorderSize : `int`, optional
80 Size in pixels of the image border used to pad the image.
81 background : `lsst.afw.math.Chebyshev1Function2D`, optional
82 Optional background to add to the output image.
83 xSize, ySize : `int`, optional
84 Size in pixels of the simulated image.
85 x0, y0 : `int`, optional
86 Origin of the image.
87 calibration : `float`, optional
88 Conversion factor between instFlux and nJy.
89 doApplyCalibration : `bool`, optional
90 Apply the photometric calibration and return the image in nJy?
92 Returns
93 -------
94 modelExposure : `lsst.afw.image.Exposure`
95 The model image, with the mask and variance planes.
96 sourceCat : `lsst.afw.table.SourceCatalog`
97 Catalog of sources detected on the model image.
98 """
99 # Distance from the inner edge of the bounding box to avoid placing test
100 # sources in the model images.
101 bufferSize = kernelSize/2 + templateBorderSize + 1
103 bbox = lsst.geom.Box2I(lsst.geom.Point2I(x0, y0), lsst.geom.Extent2I(xSize, ySize))
104 if templateBorderSize > 0:
105 bbox.grow(templateBorderSize)
107 rng = np.random.RandomState(seed)
108 rngNoise = np.random.RandomState(noiseSeed)
109 x0, y0 = bbox.getBegin()
110 xSize, ySize = bbox.getDimensions()
111 xLoc = rng.rand(nSrc)*(xSize - 2*bufferSize) + bufferSize + x0
112 yLoc = rng.rand(nSrc)*(ySize - 2*bufferSize) + bufferSize + y0
114 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel
115 sigmas = [psfSize for src in range(nSrc)]
116 coordList = list(zip(xLoc, yLoc, flux, sigmas))
117 skyLevel = 0
118 # Don't use the built in poisson noise: it modifies the global state of numpy random
119 modelExposure = plantSources(bbox, kernelSize, skyLevel, coordList, addPoissonNoise=False)
120 modelExposure.setWcs(makeFakeWcs())
121 noise = rngNoise.randn(ySize, xSize)*noiseLevel
122 noise -= np.mean(noise)
123 modelExposure.variance.array = np.sqrt(np.abs(modelExposure.image.array)) + noiseLevel**2
124 modelExposure.image.array += noise
126 # Run source detection to set up the mask plane
127 psfMatchTask = lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask()
128 sourceCat = psfMatchTask.getSelectSources(modelExposure)
129 modelExposure.setPhotoCalib(PhotoCalib(calibration, 0., bbox))
130 if background is not None:
131 modelExposure.image += background
132 modelExposure.maskedImage /= calibration
133 if doApplyCalibration:
134 modelExposure.maskedImage = modelExposure.photoCalib.calibrateImage(modelExposure.maskedImage)
136 return modelExposure, sourceCat
139class AlardLuptonSubtractTest(lsst.utils.tests.TestCase):
141 def test_allowed_config_modes(self):
142 """Verify the allowable modes for convolution.
143 """
144 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
145 config.mode = 'auto'
146 config.mode = 'convolveScience'
147 config.mode = 'convolveTemplate'
149 with self.assertRaises(FieldValidationError):
150 config.mode = 'aotu'
152 def test_config_validate_forceCompatibility(self):
153 """Check that forceCompatibility sets `mode=convolveTemplate`.
154 """
155 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
156 config.mode = "auto"
157 config.forceCompatibility = True
158 # Ensure we're not trying to change config values inside validate().
159 config.freeze()
160 with self.assertRaises(FieldValidationError):
161 config.validate()
163 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
164 config.mode = "convolveTemplate"
165 config.forceCompatibility = True
166 # Ensure we're not trying to change config values inside validate().
167 config.freeze()
168 # Should not raise:
169 config.validate()
171 def test_mismatched_template(self):
172 """Test that an error is raised if the template
173 does not fully contain the science image.
174 """
175 xSize = 200
176 ySize = 200
177 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
178 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
179 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
180 task = subtractImages.AlardLuptonSubtractTask(config=config)
181 with self.assertRaises(AssertionError):
182 task.run(template, science, sources)
184 def test_equal_images(self):
185 """Test that running with enough sources produces reasonable output,
186 with the same size psf in the template and science.
187 """
188 noiseLevel = 1.
189 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
190 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
191 templateBorderSize=20, doApplyCalibration=True)
192 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
193 config.doSubtractBackground = False
194 task = subtractImages.AlardLuptonSubtractTask(config=config)
195 output = task.run(template, science, sources)
196 # There shoud be no NaN values in the difference image
197 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
198 # Mean of difference image should be close to zero.
199 meanError = noiseLevel/np.sqrt(output.difference.image.array.size)
200 # Make sure to include pixels with the DETECTED mask bit set.
201 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
202 differenceMean = _computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl)
203 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError)
204 # stddev of difference image should be close to expected value.
205 differenceStd = _computeRobustStatistics(output.difference.image, output.difference.mask,
206 _makeStats(), statistic=afwMath.STDEV)
207 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1)
209 def test_auto_convolveTemplate(self):
210 """Test that auto mode gives the same result as convolveTemplate when
211 the template psf is the smaller.
212 """
213 noiseLevel = 1.
214 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
215 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
216 templateBorderSize=20, doApplyCalibration=True)
217 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
218 config.doSubtractBackground = False
219 config.mode = "convolveTemplate"
221 task = subtractImages.AlardLuptonSubtractTask(config=config)
222 output = task.run(template.clone(), science.clone(), sources)
224 config.mode = "auto"
225 task = subtractImages.AlardLuptonSubtractTask(config=config)
226 outputAuto = task.run(template, science, sources)
227 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
229 def test_auto_convolveScience(self):
230 """Test that auto mode gives the same result as convolveScience when
231 the science psf is the smaller.
232 """
233 noiseLevel = 1.
234 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
235 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
236 templateBorderSize=20, doApplyCalibration=True)
237 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
238 config.doSubtractBackground = False
239 config.mode = "convolveScience"
241 task = subtractImages.AlardLuptonSubtractTask(config=config)
242 output = task.run(template.clone(), science.clone(), sources)
244 config.mode = "auto"
245 task = subtractImages.AlardLuptonSubtractTask(config=config)
246 outputAuto = task.run(template, science, sources)
247 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
249 def test_science_better(self):
250 """Test that running with enough sources produces reasonable output,
251 with the science psf being smaller than the template.
252 """
253 statsCtrl = _makeStats()
254 statsCtrlDetect = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
256 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
257 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
258 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
259 templateBorderSize=20, doApplyCalibration=True)
260 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
261 config.doSubtractBackground = False
262 config.forceCompatibility = False
263 config.mode = "convolveScience"
264 task = subtractImages.AlardLuptonSubtractTask(config=config)
265 output = task.run(template, science, sources)
266 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
267 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
268 # Mean of difference image should be close to zero.
269 nGoodPix = np.sum(np.isfinite(output.difference.image.array))
270 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix)
271 diffimMean = _computeRobustStatistics(output.difference.image, output.difference.mask,
272 statsCtrlDetect)
274 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
275 # stddev of difference image should be close to expected value.
276 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
277 varianceMean = _computeRobustStatistics(output.difference.variance, output.difference.mask,
278 statsCtrl)
279 diffimStd = _computeRobustStatistics(output.difference.image, output.difference.mask,
280 statsCtrl, statistic=afwMath.STDEV)
281 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
282 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
284 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
285 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
286 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
288 def test_template_better(self):
289 """Test that running with enough sources produces reasonable output,
290 with the template psf being smaller than the science.
291 """
292 statsCtrl = _makeStats()
293 statsCtrlDetect = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
295 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
296 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
297 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
298 templateBorderSize=20, doApplyCalibration=True)
299 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
300 config.doSubtractBackground = False
301 config.forceCompatibility = False
302 task = subtractImages.AlardLuptonSubtractTask(config=config)
303 output = task.run(template, science, sources)
304 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
305 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
306 # There should be no NaNs in the image if we convolve the template with a buffer
307 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
308 # Mean of difference image should be close to zero.
309 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size)
311 diffimMean = _computeRobustStatistics(output.difference.image, output.difference.mask,
312 statsCtrlDetect)
313 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
314 # stddev of difference image should be close to expected value.
315 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
316 varianceMean = _computeRobustStatistics(output.difference.variance, output.difference.mask,
317 statsCtrl)
318 diffimStd = _computeRobustStatistics(output.difference.image, output.difference.mask,
319 statsCtrl, statistic=afwMath.STDEV)
320 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
321 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
323 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
324 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
325 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
327 def test_symmetry(self):
328 """Test that convolving the science and convolving the template are
329 symmetric: if the psfs are switched between them, the difference image
330 should be nearly the same.
331 """
332 noiseLevel = 1.
333 # Don't include a border for the template, in order to make the results
334 # comparable when we swap which image is treated as the "science" image.
335 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
336 noiseSeed=6, templateBorderSize=0)
337 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
338 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True)
339 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
340 config.mode = 'auto'
341 config.doSubtractBackground = False
342 task = subtractImages.AlardLuptonSubtractTask(config=config)
344 # The science image will be modified in place, so use a copy for the second run.
345 science_better = task.run(template.clone(), science.clone(), sources)
346 template_better = task.run(science, template, sources)
348 delta = template_better.difference.clone()
349 delta.image -= science_better.difference.image
350 delta.variance -= science_better.difference.variance
351 delta.mask.array -= science_better.difference.mask.array
353 statsCtrl = _makeStats()
354 # Mean of delta should be very close to zero.
355 nGoodPix = np.sum(np.isfinite(delta.image.array))
356 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
357 deltaMean = _computeRobustStatistics(delta.image, delta.mask, statsCtrl)
358 deltaStd = _computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV)
359 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
360 # stddev of difference image should be close to expected value
361 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1)
363 def test_few_sources(self):
364 """Test with only 1 source, to check that we get a useful error.
365 """
366 xSize = 256
367 ySize = 256
368 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
369 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
370 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
371 task = subtractImages.AlardLuptonSubtractTask(config=config)
372 sources = sources[0:1]
373 with self.assertRaisesRegex(RuntimeError,
374 "Cannot compute PSF matching kernel: too few sources selected."):
375 task.run(template, science, sources)
377 def test_order_equal_images(self):
378 """Verify that the result is the same regardless of convolution mode
379 if the images are equivalent.
380 """
381 noiseLevel = .1
382 seed1 = 6
383 seed2 = 7
384 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
385 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
386 templateBorderSize=0, doApplyCalibration=True)
387 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
388 config1.mode = "convolveTemplate"
389 config1.doSubtractBackground = False
390 task1 = subtractImages.AlardLuptonSubtractTask(config=config1)
391 results_convolveTemplate = task1.run(template1, science1, sources1)
393 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
394 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
395 templateBorderSize=0, doApplyCalibration=True)
396 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
397 config2.mode = "convolveScience"
398 config2.doSubtractBackground = False
399 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
400 results_convolveScience = task2.run(template2, science2, sources2)
401 diff1 = science1.maskedImage.clone()
402 diff1 -= template1.maskedImage
403 diff2 = science2.maskedImage.clone()
404 diff2 -= template2.maskedImage
405 self.assertFloatsAlmostEqual(results_convolveTemplate.difference.image.array,
406 diff1.image.array,
407 atol=noiseLevel*5.)
408 self.assertFloatsAlmostEqual(results_convolveScience.difference.image.array,
409 diff2.image.array,
410 atol=noiseLevel*5.)
411 diffErr = noiseLevel*2
412 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage,
413 results_convolveScience.difference.maskedImage,
414 atol=diffErr*5.)
416 def test_background_subtraction(self):
417 """Check that we can recover the background,
418 and that it is subtracted correctly in the difference image.
419 """
420 noiseLevel = 1.
421 xSize = 512
422 ySize = 512
423 x0 = 123
424 y0 = 456
425 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
426 templateBorderSize=20,
427 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
428 doApplyCalibration=True)
429 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
431 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
432 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
433 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
434 background=background_model,
435 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
436 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
437 config.doSubtractBackground = True
439 config.makeKernel.kernel.name = "AL"
440 config.makeKernel.kernel.active.fitForBackground = True
441 config.makeKernel.kernel.active.spatialKernelOrder = 1
442 config.makeKernel.kernel.active.spatialBgOrder = 2
443 statsCtrl = _makeStats()
445 def _run_and_check_images(config, statsCtrl, mode):
446 """Check that the fit background matches the input model.
447 """
448 config.mode = mode
449 task = subtractImages.AlardLuptonSubtractTask(config=config)
450 output = task.run(template.clone(), science.clone(), sources)
452 # We should be fitting the same number of parameters as were in the input model
453 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
455 # The parameters of the background fit should be close to the input model
456 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
457 np.array(params), rtol=0.3)
459 # stddev of difference image should be close to expected value.
460 # This will fail if we have mis-subtracted the background.
461 stdVal = _computeRobustStatistics(output.difference.image, output.difference.mask,
462 statsCtrl, statistic=afwMath.STDEV)
463 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
465 _run_and_check_images(config, statsCtrl, "convolveTemplate")
466 _run_and_check_images(config, statsCtrl, "convolveScience")
468 def test_scale_variance_convolve_template(self):
469 """Check variance scaling of the image difference.
470 """
471 scienceNoiseLevel = 4.
472 templateNoiseLevel = 2.
473 scaleFactor = 1.345
474 # Make sure to include pixels with the DETECTED mask bit set.
475 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
477 def _run_and_check_images(science, template, sources, statsCtrl,
478 doDecorrelation, doScaleVariance, scaleFactor=1.):
479 """Check that the variance plane matches the expected value for
480 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
481 """
483 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
484 config.doSubtractBackground = False
485 config.forceCompatibility = False
486 config.doDecorrelation = doDecorrelation
487 config.doScaleVariance = doScaleVariance
488 task = subtractImages.AlardLuptonSubtractTask(config=config)
489 output = task.run(template.clone(), science.clone(), sources)
490 if doScaleVariance:
491 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
492 scaleFactor, atol=0.05)
493 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
494 scaleFactor, atol=0.05)
496 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl)
497 if doDecorrelation:
498 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl)
499 else:
500 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance,
501 output.matchedTemplate.mask,
502 statsCtrl)
504 if doScaleVariance:
505 templateNoise *= scaleFactor
506 scienceNoise *= scaleFactor
507 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
508 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
510 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
511 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
512 templateBorderSize=20, doApplyCalibration=True)
513 # Verify that the variance plane of the difference image is correct
514 # when the template and science variance planes are correct
515 _run_and_check_images(science, template, sources, statsCtrl,
516 doDecorrelation=True, doScaleVariance=True)
517 _run_and_check_images(science, template, sources, statsCtrl,
518 doDecorrelation=True, doScaleVariance=False)
519 _run_and_check_images(science, template, sources, statsCtrl,
520 doDecorrelation=False, doScaleVariance=True)
521 _run_and_check_images(science, template, sources, statsCtrl,
522 doDecorrelation=False, doScaleVariance=False)
524 # Verify that the variance plane of the difference image is correct
525 # when the template variance plane is incorrect
526 template.variance.array /= scaleFactor
527 science.variance.array /= scaleFactor
528 _run_and_check_images(science, template, sources, statsCtrl,
529 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
530 _run_and_check_images(science, template, sources, statsCtrl,
531 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
532 _run_and_check_images(science, template, sources, statsCtrl,
533 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
534 _run_and_check_images(science, template, sources, statsCtrl,
535 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
537 def test_scale_variance_convolve_science(self):
538 """Check variance scaling of the image difference.
539 """
540 scienceNoiseLevel = 4.
541 templateNoiseLevel = 2.
542 scaleFactor = 1.345
543 # Make sure to include pixels with the DETECTED mask bit set.
544 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
546 def _run_and_check_images(science, template, sources, statsCtrl,
547 doDecorrelation, doScaleVariance, scaleFactor=1.):
548 """Check that the variance plane matches the expected value for
549 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
550 """
552 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
553 config.mode = "convolveScience"
554 config.doSubtractBackground = False
555 config.forceCompatibility = False
556 config.doDecorrelation = doDecorrelation
557 config.doScaleVariance = doScaleVariance
558 task = subtractImages.AlardLuptonSubtractTask(config=config)
559 output = task.run(template.clone(), science.clone(), sources)
560 if doScaleVariance:
561 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
562 scaleFactor, atol=0.05)
563 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
564 scaleFactor, atol=0.05)
566 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl)
567 if doDecorrelation:
568 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl)
569 else:
570 scienceNoise = _computeRobustStatistics(output.matchedScience.variance,
571 output.matchedScience.mask,
572 statsCtrl)
574 if doScaleVariance:
575 templateNoise *= scaleFactor
576 scienceNoise *= scaleFactor
578 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
579 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
581 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
582 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
583 templateBorderSize=20, doApplyCalibration=True)
584 # Verify that the variance plane of the difference image is correct
585 # when the template and science variance planes are correct
586 _run_and_check_images(science, template, sources, statsCtrl,
587 doDecorrelation=True, doScaleVariance=True)
588 _run_and_check_images(science, template, sources, statsCtrl,
589 doDecorrelation=True, doScaleVariance=False)
590 _run_and_check_images(science, template, sources, statsCtrl,
591 doDecorrelation=False, doScaleVariance=True)
592 _run_and_check_images(science, template, sources, statsCtrl,
593 doDecorrelation=False, doScaleVariance=False)
595 # Verify that the variance plane of the difference image is correct
596 # when the template and science variance planes are incorrect
597 science.variance.array /= scaleFactor
598 template.variance.array /= scaleFactor
599 _run_and_check_images(science, template, sources, statsCtrl,
600 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
601 _run_and_check_images(science, template, sources, statsCtrl,
602 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
603 _run_and_check_images(science, template, sources, statsCtrl,
604 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
605 _run_and_check_images(science, template, sources, statsCtrl,
606 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
608 def test_exposure_properties_convolve_template(self):
609 """Check that all necessary exposure metadata is included
610 when the template is convolved.
611 """
612 noiseLevel = 1.
613 seed = 37
614 rng = np.random.RandomState(seed)
615 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
616 psf = science.psf
617 psfAvgPos = psf.getAveragePosition()
618 psfSize = getPsfFwhm(science.psf)
619 psfImg = psf.computeKernelImage(psfAvgPos)
620 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
621 templateBorderSize=20, doApplyCalibration=True)
623 # Generate a random aperture correction map
624 apCorrMap = lsst.afw.image.ApCorrMap()
625 for name in ("a", "b", "c"):
626 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
627 science.info.setApCorrMap(apCorrMap)
629 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
630 config.mode = "convolveTemplate"
632 def _run_and_check_images(doDecorrelation):
633 """Check that the metadata is correct with or without decorrelation.
634 """
635 config.doDecorrelation = doDecorrelation
636 task = subtractImages.AlardLuptonSubtractTask(config=config)
637 output = task.run(template.clone(), science.clone(), sources)
638 psfOut = output.difference.psf
639 psfAvgPos = psfOut.getAveragePosition()
640 if doDecorrelation:
641 # Decorrelation requires recalculating the PSF,
642 # so it will not be the same as the input
643 psfOutSize = getPsfFwhm(science.psf)
644 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
645 else:
646 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
647 self.assertImagesAlmostEqual(psfImg, psfOutImg)
649 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
650 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
651 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
652 self.assertEqual(science.filter, output.difference.filter)
653 self.assertEqual(science.photoCalib, output.difference.photoCalib)
654 _run_and_check_images(doDecorrelation=True)
655 _run_and_check_images(doDecorrelation=False)
657 def test_exposure_properties_convolve_science(self):
658 """Check that all necessary exposure metadata is included
659 when the science image is convolved.
660 """
661 noiseLevel = 1.
662 seed = 37
663 rng = np.random.RandomState(seed)
664 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
665 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
666 templateBorderSize=20, doApplyCalibration=True)
667 psf = template.psf
668 psfAvgPos = psf.getAveragePosition()
669 psfSize = getPsfFwhm(template.psf)
670 psfImg = psf.computeKernelImage(psfAvgPos)
672 # Generate a random aperture correction map
673 apCorrMap = lsst.afw.image.ApCorrMap()
674 for name in ("a", "b", "c"):
675 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
676 science.info.setApCorrMap(apCorrMap)
678 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
679 config.mode = "convolveScience"
681 def _run_and_check_images(doDecorrelation):
682 """Check that the metadata is correct with or without decorrelation.
683 """
684 config.doDecorrelation = doDecorrelation
685 task = subtractImages.AlardLuptonSubtractTask(config=config)
686 output = task.run(template.clone(), science.clone(), sources)
687 if doDecorrelation:
688 # Decorrelation requires recalculating the PSF,
689 # so it will not be the same as the input
690 psfOutSize = getPsfFwhm(template.psf)
691 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
692 else:
693 psfOut = output.difference.psf
694 psfAvgPos = psfOut.getAveragePosition()
695 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
696 self.assertImagesAlmostEqual(psfImg, psfOutImg)
698 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
699 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
700 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
701 self.assertEqual(science.filter, output.difference.filter)
702 self.assertEqual(science.photoCalib, output.difference.photoCalib)
704 _run_and_check_images(doDecorrelation=True)
705 _run_and_check_images(doDecorrelation=False)
707 def _compare_apCorrMaps(self, a, b):
708 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
709 same addresses (i.e. so we can compare after serialization).
711 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
713 Parameters
714 ----------
715 a, b : `lsst.afw.image.ApCorrMap`
716 The two aperture correction maps to compare.
717 """
718 self.assertEqual(len(a), len(b))
719 for name, value in list(a.items()):
720 value2 = b.get(name)
721 self.assertIsNotNone(value2)
722 self.assertEqual(value.getBBox(), value2.getBBox())
723 self.assertFloatsAlmostEqual(
724 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
727def _makeStats(badMaskPlanes=None):
728 """Create a statistics control for configuring calculations on images.
730 Parameters
731 ----------
732 badMaskPlanes : `list` of `str`, optional
733 List of mask planes to exclude from calculations.
735 Returns
736 -------
737 statsControl : ` lsst.afw.math.StatisticsControl`
738 Statistics control object for configuring calculations on images.
739 """
740 if badMaskPlanes is None:
741 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR",
742 "BAD", "NO_DATA", "DETECTED_NEGATIVE")
743 statsControl = afwMath.StatisticsControl()
744 statsControl.setNumSigmaClip(3.)
745 statsControl.setNumIter(3)
746 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes))
747 return statsControl
750def _computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP):
751 """Calculate a robust mean of the variance plane of an exposure.
753 Parameters
754 ----------
755 image : `lsst.afw.image.Image`
756 Image or variance plane of an exposure to evaluate.
757 mask : `lsst.afw.image.Mask`
758 Mask plane to use for excluding pixels.
759 statsCtrl : `lsst.afw.math.StatisticsControl`
760 Statistics control object for configuring the calculation.
761 statistic : `lsst.afw.math.Property`, optional
762 The type of statistic to compute. Typical values are
763 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``.
765 Returns
766 -------
767 value : `float`
768 The result of the statistic calculated from the unflagged pixels.
769 """
770 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl)
771 return statObj.getValue(statistic)
774def setup_module(module):
775 lsst.utils.tests.init()
778class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
779 pass
782if __name__ == "__main__": 782 ↛ 783line 782 didn't jump to line 783, because the condition on line 782 was never true
783 lsst.utils.tests.init()
784 unittest.main()