Coverage for tests/test_subtractTask.py: 9%
419 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-08 09:52 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-08 09:52 +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=1, xSize=xSize, ySize=ySize)
369 template, _ = makeTestImage(psfSize=2.0, nSrc=1, xSize=xSize, ySize=ySize, doApplyCalibration=True)
370 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
371 task = subtractImages.AlardLuptonSubtractTask(config=config)
372 with self.assertRaisesRegex(lsst.pex.exceptions.Exception,
373 'Unable to determine kernel sum; 0 candidates'):
374 task.run(template, science, sources)
376 def test_order_equal_images(self):
377 """Verify that the result is the same regardless of convolution mode
378 if the images are equivalent.
379 """
380 noiseLevel = .1
381 seed1 = 6
382 seed2 = 7
383 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
384 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
385 templateBorderSize=0, doApplyCalibration=True)
386 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
387 config1.mode = "convolveTemplate"
388 config1.doSubtractBackground = False
389 task1 = subtractImages.AlardLuptonSubtractTask(config=config1)
390 results_convolveTemplate = task1.run(template1, science1, sources1)
392 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
393 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
394 templateBorderSize=0, doApplyCalibration=True)
395 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
396 config2.mode = "convolveScience"
397 config2.doSubtractBackground = False
398 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
399 results_convolveScience = task2.run(template2, science2, sources2)
400 diff1 = science1.maskedImage.clone()
401 diff1 -= template1.maskedImage
402 diff2 = science2.maskedImage.clone()
403 diff2 -= template2.maskedImage
404 self.assertFloatsAlmostEqual(results_convolveTemplate.difference.image.array,
405 diff1.image.array,
406 atol=noiseLevel*5.)
407 self.assertFloatsAlmostEqual(results_convolveScience.difference.image.array,
408 diff2.image.array,
409 atol=noiseLevel*5.)
410 diffErr = noiseLevel*2
411 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage,
412 results_convolveScience.difference.maskedImage,
413 atol=diffErr*5.)
415 def test_background_subtraction(self):
416 """Check that we can recover the background,
417 and that it is subtracted correctly in the difference image.
418 """
419 noiseLevel = 1.
420 xSize = 512
421 ySize = 512
422 x0 = 123
423 y0 = 456
424 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
425 templateBorderSize=20,
426 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
427 doApplyCalibration=True)
428 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
430 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
431 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
432 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
433 background=background_model,
434 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
435 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
436 config.doSubtractBackground = True
438 config.makeKernel.kernel.name = "AL"
439 config.makeKernel.kernel.active.fitForBackground = True
440 config.makeKernel.kernel.active.spatialKernelOrder = 1
441 config.makeKernel.kernel.active.spatialBgOrder = 2
442 statsCtrl = _makeStats()
444 def _run_and_check_images(config, statsCtrl, mode):
445 """Check that the fit background matches the input model.
446 """
447 config.mode = mode
448 task = subtractImages.AlardLuptonSubtractTask(config=config)
449 output = task.run(template.clone(), science.clone(), sources)
451 # We should be fitting the same number of parameters as were in the input model
452 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
454 # The parameters of the background fit should be close to the input model
455 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
456 np.array(params), rtol=0.3)
458 # stddev of difference image should be close to expected value.
459 # This will fail if we have mis-subtracted the background.
460 stdVal = _computeRobustStatistics(output.difference.image, output.difference.mask,
461 statsCtrl, statistic=afwMath.STDEV)
462 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
464 _run_and_check_images(config, statsCtrl, "convolveTemplate")
465 _run_and_check_images(config, statsCtrl, "convolveScience")
467 def test_scale_variance_convolve_template(self):
468 """Check variance scaling of the image difference.
469 """
470 scienceNoiseLevel = 4.
471 templateNoiseLevel = 2.
472 scaleFactor = 1.345
473 # Make sure to include pixels with the DETECTED mask bit set.
474 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
476 def _run_and_check_images(science, template, sources, statsCtrl,
477 doDecorrelation, doScaleVariance, scaleFactor=1.):
478 """Check that the variance plane matches the expected value for
479 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
480 """
482 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
483 config.doSubtractBackground = False
484 config.forceCompatibility = False
485 config.doDecorrelation = doDecorrelation
486 config.doScaleVariance = doScaleVariance
487 task = subtractImages.AlardLuptonSubtractTask(config=config)
488 output = task.run(template.clone(), science.clone(), sources)
489 if doScaleVariance:
490 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
491 scaleFactor, atol=0.05)
492 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
493 scaleFactor, atol=0.05)
495 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl)
496 if doDecorrelation:
497 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl)
498 else:
499 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance,
500 output.matchedTemplate.mask,
501 statsCtrl)
503 if doScaleVariance:
504 templateNoise *= scaleFactor
505 scienceNoise *= scaleFactor
506 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
507 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
509 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
510 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
511 templateBorderSize=20, doApplyCalibration=True)
512 # Verify that the variance plane of the difference image is correct
513 # when the template and science variance planes are correct
514 _run_and_check_images(science, template, sources, statsCtrl,
515 doDecorrelation=True, doScaleVariance=True)
516 _run_and_check_images(science, template, sources, statsCtrl,
517 doDecorrelation=True, doScaleVariance=False)
518 _run_and_check_images(science, template, sources, statsCtrl,
519 doDecorrelation=False, doScaleVariance=True)
520 _run_and_check_images(science, template, sources, statsCtrl,
521 doDecorrelation=False, doScaleVariance=False)
523 # Verify that the variance plane of the difference image is correct
524 # when the template variance plane is incorrect
525 template.variance.array /= scaleFactor
526 science.variance.array /= scaleFactor
527 _run_and_check_images(science, template, sources, statsCtrl,
528 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
529 _run_and_check_images(science, template, sources, statsCtrl,
530 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
531 _run_and_check_images(science, template, sources, statsCtrl,
532 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
533 _run_and_check_images(science, template, sources, statsCtrl,
534 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
536 def test_scale_variance_convolve_science(self):
537 """Check variance scaling of the image difference.
538 """
539 scienceNoiseLevel = 4.
540 templateNoiseLevel = 2.
541 scaleFactor = 1.345
542 # Make sure to include pixels with the DETECTED mask bit set.
543 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
545 def _run_and_check_images(science, template, sources, statsCtrl,
546 doDecorrelation, doScaleVariance, scaleFactor=1.):
547 """Check that the variance plane matches the expected value for
548 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
549 """
551 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
552 config.mode = "convolveScience"
553 config.doSubtractBackground = False
554 config.forceCompatibility = False
555 config.doDecorrelation = doDecorrelation
556 config.doScaleVariance = doScaleVariance
557 task = subtractImages.AlardLuptonSubtractTask(config=config)
558 output = task.run(template.clone(), science.clone(), sources)
559 if doScaleVariance:
560 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
561 scaleFactor, atol=0.05)
562 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
563 scaleFactor, atol=0.05)
565 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl)
566 if doDecorrelation:
567 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl)
568 else:
569 scienceNoise = _computeRobustStatistics(output.matchedScience.variance,
570 output.matchedScience.mask,
571 statsCtrl)
573 if doScaleVariance:
574 templateNoise *= scaleFactor
575 scienceNoise *= scaleFactor
577 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
578 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
580 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
581 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
582 templateBorderSize=20, doApplyCalibration=True)
583 # Verify that the variance plane of the difference image is correct
584 # when the template and science variance planes are correct
585 _run_and_check_images(science, template, sources, statsCtrl,
586 doDecorrelation=True, doScaleVariance=True)
587 _run_and_check_images(science, template, sources, statsCtrl,
588 doDecorrelation=True, doScaleVariance=False)
589 _run_and_check_images(science, template, sources, statsCtrl,
590 doDecorrelation=False, doScaleVariance=True)
591 _run_and_check_images(science, template, sources, statsCtrl,
592 doDecorrelation=False, doScaleVariance=False)
594 # Verify that the variance plane of the difference image is correct
595 # when the template and science variance planes are incorrect
596 science.variance.array /= scaleFactor
597 template.variance.array /= scaleFactor
598 _run_and_check_images(science, template, sources, statsCtrl,
599 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
600 _run_and_check_images(science, template, sources, statsCtrl,
601 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
602 _run_and_check_images(science, template, sources, statsCtrl,
603 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
604 _run_and_check_images(science, template, sources, statsCtrl,
605 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
607 def test_exposure_properties_convolve_template(self):
608 """Check that all necessary exposure metadata is included
609 when the template is convolved.
610 """
611 noiseLevel = 1.
612 seed = 37
613 rng = np.random.RandomState(seed)
614 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
615 psf = science.psf
616 psfAvgPos = psf.getAveragePosition()
617 psfSize = getPsfFwhm(science.psf)
618 psfImg = psf.computeKernelImage(psfAvgPos)
619 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
620 templateBorderSize=20, doApplyCalibration=True)
622 # Generate a random aperture correction map
623 apCorrMap = lsst.afw.image.ApCorrMap()
624 for name in ("a", "b", "c"):
625 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
626 science.info.setApCorrMap(apCorrMap)
628 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
629 config.mode = "convolveTemplate"
631 def _run_and_check_images(doDecorrelation):
632 """Check that the metadata is correct with or without decorrelation.
633 """
634 config.doDecorrelation = doDecorrelation
635 task = subtractImages.AlardLuptonSubtractTask(config=config)
636 output = task.run(template.clone(), science.clone(), sources)
637 psfOut = output.difference.psf
638 psfAvgPos = psfOut.getAveragePosition()
639 if doDecorrelation:
640 # Decorrelation requires recalculating the PSF,
641 # so it will not be the same as the input
642 psfOutSize = getPsfFwhm(science.psf)
643 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
644 else:
645 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
646 self.assertImagesAlmostEqual(psfImg, psfOutImg)
648 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
649 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
650 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
651 self.assertEqual(science.filter, output.difference.filter)
652 self.assertEqual(science.photoCalib, output.difference.photoCalib)
653 _run_and_check_images(doDecorrelation=True)
654 _run_and_check_images(doDecorrelation=False)
656 def test_exposure_properties_convolve_science(self):
657 """Check that all necessary exposure metadata is included
658 when the science image is convolved.
659 """
660 noiseLevel = 1.
661 seed = 37
662 rng = np.random.RandomState(seed)
663 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
664 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
665 templateBorderSize=20, doApplyCalibration=True)
666 psf = template.psf
667 psfAvgPos = psf.getAveragePosition()
668 psfSize = getPsfFwhm(template.psf)
669 psfImg = psf.computeKernelImage(psfAvgPos)
671 # Generate a random aperture correction map
672 apCorrMap = lsst.afw.image.ApCorrMap()
673 for name in ("a", "b", "c"):
674 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
675 science.info.setApCorrMap(apCorrMap)
677 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
678 config.mode = "convolveScience"
680 def _run_and_check_images(doDecorrelation):
681 """Check that the metadata is correct with or without decorrelation.
682 """
683 config.doDecorrelation = doDecorrelation
684 task = subtractImages.AlardLuptonSubtractTask(config=config)
685 output = task.run(template.clone(), science.clone(), sources)
686 if doDecorrelation:
687 # Decorrelation requires recalculating the PSF,
688 # so it will not be the same as the input
689 psfOutSize = getPsfFwhm(template.psf)
690 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
691 else:
692 psfOut = output.difference.psf
693 psfAvgPos = psfOut.getAveragePosition()
694 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
695 self.assertImagesAlmostEqual(psfImg, psfOutImg)
697 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
698 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
699 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
700 self.assertEqual(science.filter, output.difference.filter)
701 self.assertEqual(science.photoCalib, output.difference.photoCalib)
703 _run_and_check_images(doDecorrelation=True)
704 _run_and_check_images(doDecorrelation=False)
706 def _compare_apCorrMaps(self, a, b):
707 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
708 same addresses (i.e. so we can compare after serialization).
710 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
712 Parameters
713 ----------
714 a, b : `lsst.afw.image.ApCorrMap`
715 The two aperture correction maps to compare.
716 """
717 self.assertEqual(len(a), len(b))
718 for name, value in list(a.items()):
719 value2 = b.get(name)
720 self.assertIsNotNone(value2)
721 self.assertEqual(value.getBBox(), value2.getBBox())
722 self.assertFloatsAlmostEqual(
723 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
726def _makeStats(badMaskPlanes=None):
727 """Create a statistics control for configuring calculations on images.
729 Parameters
730 ----------
731 badMaskPlanes : `list` of `str`, optional
732 List of mask planes to exclude from calculations.
734 Returns
735 -------
736 statsControl : ` lsst.afw.math.StatisticsControl`
737 Statistics control object for configuring calculations on images.
738 """
739 if badMaskPlanes is None:
740 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR",
741 "BAD", "NO_DATA", "DETECTED_NEGATIVE")
742 statsControl = afwMath.StatisticsControl()
743 statsControl.setNumSigmaClip(3.)
744 statsControl.setNumIter(3)
745 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes))
746 return statsControl
749def _computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP):
750 """Calculate a robust mean of the variance plane of an exposure.
752 Parameters
753 ----------
754 image : `lsst.afw.image.Image`
755 Image or variance plane of an exposure to evaluate.
756 mask : `lsst.afw.image.Mask`
757 Mask plane to use for excluding pixels.
758 statsCtrl : `lsst.afw.math.StatisticsControl`
759 Statistics control object for configuring the calculation.
760 statistic : `lsst.afw.math.Property`, optional
761 The type of statistic to compute. Typical values are
762 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``.
764 Returns
765 -------
766 value : `float`
767 The result of the statistic calculated from the unflagged pixels.
768 """
769 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl)
770 return statObj.getValue(statistic)
773def setup_module(module):
774 lsst.utils.tests.init()
777class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
778 pass
781if __name__ == "__main__": 781 ↛ 782line 781 didn't jump to line 782, because the condition on line 781 was never true
782 lsst.utils.tests.init()
783 unittest.main()