Coverage for tests/test_subtractTask.py: 9%
404 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-06 02:46 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-06 02:46 -0800
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_mismatched_template(self):
153 """Test that an error is raised if the template
154 does not fully contain the science image.
155 """
156 xSize = 200
157 ySize = 200
158 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
159 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
160 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
161 task = subtractImages.AlardLuptonSubtractTask(config=config)
162 with self.assertRaises(AssertionError):
163 task.run(template, science, sources)
165 def test_equal_images(self):
166 """Test that running with enough sources produces reasonable output,
167 with the same size psf in the template and science.
168 """
169 noiseLevel = 1.
170 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
171 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
172 templateBorderSize=20, doApplyCalibration=True)
173 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
174 config.doSubtractBackground = False
175 task = subtractImages.AlardLuptonSubtractTask(config=config)
176 output = task.run(template, science, sources)
177 # There shoud be no NaN values in the difference image
178 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
179 # Mean of difference image should be close to zero.
180 meanError = noiseLevel/np.sqrt(output.difference.image.array.size)
181 # Make sure to include pixels with the DETECTED mask bit set.
182 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
183 differenceMean = _computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl)
184 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError)
185 # stddev of difference image should be close to expected value.
186 differenceStd = _computeRobustStatistics(output.difference.image, output.difference.mask,
187 _makeStats(), statistic=afwMath.STDEV)
188 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1)
190 def test_auto_convolveTemplate(self):
191 """Test that auto mode gives the same result as convolveTemplate when
192 the template psf is the smaller.
193 """
194 noiseLevel = 1.
195 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
196 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
197 templateBorderSize=20, doApplyCalibration=True)
198 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
199 config.doSubtractBackground = False
200 config.mode = "convolveTemplate"
202 task = subtractImages.AlardLuptonSubtractTask(config=config)
203 output = task.run(template.clone(), science.clone(), sources)
205 config.mode = "auto"
206 task = subtractImages.AlardLuptonSubtractTask(config=config)
207 outputAuto = task.run(template, science, sources)
208 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
210 def test_auto_convolveScience(self):
211 """Test that auto mode gives the same result as convolveScience when
212 the science psf is the smaller.
213 """
214 noiseLevel = 1.
215 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
216 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
217 templateBorderSize=20, doApplyCalibration=True)
218 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
219 config.doSubtractBackground = False
220 config.mode = "convolveScience"
222 task = subtractImages.AlardLuptonSubtractTask(config=config)
223 output = task.run(template.clone(), science.clone(), sources)
225 config.mode = "auto"
226 task = subtractImages.AlardLuptonSubtractTask(config=config)
227 outputAuto = task.run(template, science, sources)
228 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
230 def test_science_better(self):
231 """Test that running with enough sources produces reasonable output,
232 with the science psf being smaller than the template.
233 """
234 statsCtrl = _makeStats()
235 statsCtrlDetect = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
237 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
238 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
239 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
240 templateBorderSize=20, doApplyCalibration=True)
241 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
242 config.doSubtractBackground = False
243 config.mode = "convolveScience"
244 task = subtractImages.AlardLuptonSubtractTask(config=config)
245 output = task.run(template, science, sources)
246 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
247 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
248 # Mean of difference image should be close to zero.
249 nGoodPix = np.sum(np.isfinite(output.difference.image.array))
250 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix)
251 diffimMean = _computeRobustStatistics(output.difference.image, output.difference.mask,
252 statsCtrlDetect)
254 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
255 # stddev of difference image should be close to expected value.
256 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
257 varianceMean = _computeRobustStatistics(output.difference.variance, output.difference.mask,
258 statsCtrl)
259 diffimStd = _computeRobustStatistics(output.difference.image, output.difference.mask,
260 statsCtrl, statistic=afwMath.STDEV)
261 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
262 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
264 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
265 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
266 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
268 def test_template_better(self):
269 """Test that running with enough sources produces reasonable output,
270 with the template psf being smaller than the science.
271 """
272 statsCtrl = _makeStats()
273 statsCtrlDetect = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
275 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
276 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
277 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
278 templateBorderSize=20, doApplyCalibration=True)
279 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
280 config.doSubtractBackground = False
281 task = subtractImages.AlardLuptonSubtractTask(config=config)
282 output = task.run(template, science, sources)
283 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
284 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
285 # There should be no NaNs in the image if we convolve the template with a buffer
286 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
287 # Mean of difference image should be close to zero.
288 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size)
290 diffimMean = _computeRobustStatistics(output.difference.image, output.difference.mask,
291 statsCtrlDetect)
292 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
293 # stddev of difference image should be close to expected value.
294 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
295 varianceMean = _computeRobustStatistics(output.difference.variance, output.difference.mask,
296 statsCtrl)
297 diffimStd = _computeRobustStatistics(output.difference.image, output.difference.mask,
298 statsCtrl, statistic=afwMath.STDEV)
299 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
300 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
302 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
303 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
304 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
306 def test_symmetry(self):
307 """Test that convolving the science and convolving the template are
308 symmetric: if the psfs are switched between them, the difference image
309 should be nearly the same.
310 """
311 noiseLevel = 1.
312 # Don't include a border for the template, in order to make the results
313 # comparable when we swap which image is treated as the "science" image.
314 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
315 noiseSeed=6, templateBorderSize=0)
316 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
317 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True)
318 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
319 config.mode = 'auto'
320 config.doSubtractBackground = False
321 task = subtractImages.AlardLuptonSubtractTask(config=config)
323 # The science image will be modified in place, so use a copy for the second run.
324 science_better = task.run(template.clone(), science.clone(), sources)
325 template_better = task.run(science, template, sources)
327 delta = template_better.difference.clone()
328 delta.image -= science_better.difference.image
329 delta.variance -= science_better.difference.variance
330 delta.mask.array -= science_better.difference.mask.array
332 statsCtrl = _makeStats()
333 # Mean of delta should be very close to zero.
334 nGoodPix = np.sum(np.isfinite(delta.image.array))
335 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
336 deltaMean = _computeRobustStatistics(delta.image, delta.mask, statsCtrl)
337 deltaStd = _computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV)
338 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
339 # stddev of difference image should be close to expected value
340 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1)
342 def test_few_sources(self):
343 """Test with only 1 source, to check that we get a useful error.
344 """
345 xSize = 256
346 ySize = 256
347 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
348 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
349 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
350 task = subtractImages.AlardLuptonSubtractTask(config=config)
351 sources = sources[0:1]
352 with self.assertRaisesRegex(RuntimeError,
353 "Cannot compute PSF matching kernel: too few sources selected."):
354 task.run(template, science, sources)
356 def test_order_equal_images(self):
357 """Verify that the result is the same regardless of convolution mode
358 if the images are equivalent.
359 """
360 noiseLevel = .1
361 seed1 = 6
362 seed2 = 7
363 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
364 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
365 templateBorderSize=0, doApplyCalibration=True)
366 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
367 config1.mode = "convolveTemplate"
368 config1.doSubtractBackground = False
369 task1 = subtractImages.AlardLuptonSubtractTask(config=config1)
370 results_convolveTemplate = task1.run(template1, science1, sources1)
372 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
373 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
374 templateBorderSize=0, doApplyCalibration=True)
375 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
376 config2.mode = "convolveScience"
377 config2.doSubtractBackground = False
378 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
379 results_convolveScience = task2.run(template2, science2, sources2)
380 diff1 = science1.maskedImage.clone()
381 diff1 -= template1.maskedImage
382 diff2 = science2.maskedImage.clone()
383 diff2 -= template2.maskedImage
384 self.assertFloatsAlmostEqual(results_convolveTemplate.difference.image.array,
385 diff1.image.array,
386 atol=noiseLevel*5.)
387 self.assertFloatsAlmostEqual(results_convolveScience.difference.image.array,
388 diff2.image.array,
389 atol=noiseLevel*5.)
390 diffErr = noiseLevel*2
391 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage,
392 results_convolveScience.difference.maskedImage,
393 atol=diffErr*5.)
395 def test_background_subtraction(self):
396 """Check that we can recover the background,
397 and that it is subtracted correctly in the difference image.
398 """
399 noiseLevel = 1.
400 xSize = 512
401 ySize = 512
402 x0 = 123
403 y0 = 456
404 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
405 templateBorderSize=20,
406 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
407 doApplyCalibration=True)
408 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
410 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
411 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
412 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
413 background=background_model,
414 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
415 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
416 config.doSubtractBackground = True
418 config.makeKernel.kernel.name = "AL"
419 config.makeKernel.kernel.active.fitForBackground = True
420 config.makeKernel.kernel.active.spatialKernelOrder = 1
421 config.makeKernel.kernel.active.spatialBgOrder = 2
422 statsCtrl = _makeStats()
424 def _run_and_check_images(config, statsCtrl, mode):
425 """Check that the fit background matches the input model.
426 """
427 config.mode = mode
428 task = subtractImages.AlardLuptonSubtractTask(config=config)
429 output = task.run(template.clone(), science.clone(), sources)
431 # We should be fitting the same number of parameters as were in the input model
432 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
434 # The parameters of the background fit should be close to the input model
435 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
436 np.array(params), rtol=0.3)
438 # stddev of difference image should be close to expected value.
439 # This will fail if we have mis-subtracted the background.
440 stdVal = _computeRobustStatistics(output.difference.image, output.difference.mask,
441 statsCtrl, statistic=afwMath.STDEV)
442 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
444 _run_and_check_images(config, statsCtrl, "convolveTemplate")
445 _run_and_check_images(config, statsCtrl, "convolveScience")
447 def test_scale_variance_convolve_template(self):
448 """Check variance scaling of the image difference.
449 """
450 scienceNoiseLevel = 4.
451 templateNoiseLevel = 2.
452 scaleFactor = 1.345
453 # Make sure to include pixels with the DETECTED mask bit set.
454 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
456 def _run_and_check_images(science, template, sources, statsCtrl,
457 doDecorrelation, doScaleVariance, scaleFactor=1.):
458 """Check that the variance plane matches the expected value for
459 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
460 """
462 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
463 config.doSubtractBackground = False
464 config.doDecorrelation = doDecorrelation
465 config.doScaleVariance = doScaleVariance
466 task = subtractImages.AlardLuptonSubtractTask(config=config)
467 output = task.run(template.clone(), science.clone(), sources)
468 if doScaleVariance:
469 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
470 scaleFactor, atol=0.05)
471 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
472 scaleFactor, atol=0.05)
474 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl)
475 if doDecorrelation:
476 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl)
477 else:
478 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance,
479 output.matchedTemplate.mask,
480 statsCtrl)
482 if doScaleVariance:
483 templateNoise *= scaleFactor
484 scienceNoise *= scaleFactor
485 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
486 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
488 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
489 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
490 templateBorderSize=20, doApplyCalibration=True)
491 # Verify that the variance plane of the difference image is correct
492 # when the template and science variance planes are correct
493 _run_and_check_images(science, template, sources, statsCtrl,
494 doDecorrelation=True, doScaleVariance=True)
495 _run_and_check_images(science, template, sources, statsCtrl,
496 doDecorrelation=True, doScaleVariance=False)
497 _run_and_check_images(science, template, sources, statsCtrl,
498 doDecorrelation=False, doScaleVariance=True)
499 _run_and_check_images(science, template, sources, statsCtrl,
500 doDecorrelation=False, doScaleVariance=False)
502 # Verify that the variance plane of the difference image is correct
503 # when the template variance plane is incorrect
504 template.variance.array /= scaleFactor
505 science.variance.array /= scaleFactor
506 _run_and_check_images(science, template, sources, statsCtrl,
507 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
508 _run_and_check_images(science, template, sources, statsCtrl,
509 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
510 _run_and_check_images(science, template, sources, statsCtrl,
511 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
512 _run_and_check_images(science, template, sources, statsCtrl,
513 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
515 def test_scale_variance_convolve_science(self):
516 """Check variance scaling of the image difference.
517 """
518 scienceNoiseLevel = 4.
519 templateNoiseLevel = 2.
520 scaleFactor = 1.345
521 # Make sure to include pixels with the DETECTED mask bit set.
522 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
524 def _run_and_check_images(science, template, sources, statsCtrl,
525 doDecorrelation, doScaleVariance, scaleFactor=1.):
526 """Check that the variance plane matches the expected value for
527 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
528 """
530 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
531 config.mode = "convolveScience"
532 config.doSubtractBackground = False
533 config.doDecorrelation = doDecorrelation
534 config.doScaleVariance = doScaleVariance
535 task = subtractImages.AlardLuptonSubtractTask(config=config)
536 output = task.run(template.clone(), science.clone(), sources)
537 if doScaleVariance:
538 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
539 scaleFactor, atol=0.05)
540 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
541 scaleFactor, atol=0.05)
543 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl)
544 if doDecorrelation:
545 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl)
546 else:
547 scienceNoise = _computeRobustStatistics(output.matchedScience.variance,
548 output.matchedScience.mask,
549 statsCtrl)
551 if doScaleVariance:
552 templateNoise *= scaleFactor
553 scienceNoise *= scaleFactor
555 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
556 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
558 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
559 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
560 templateBorderSize=20, doApplyCalibration=True)
561 # Verify that the variance plane of the difference image is correct
562 # when the template and science variance planes are correct
563 _run_and_check_images(science, template, sources, statsCtrl,
564 doDecorrelation=True, doScaleVariance=True)
565 _run_and_check_images(science, template, sources, statsCtrl,
566 doDecorrelation=True, doScaleVariance=False)
567 _run_and_check_images(science, template, sources, statsCtrl,
568 doDecorrelation=False, doScaleVariance=True)
569 _run_and_check_images(science, template, sources, statsCtrl,
570 doDecorrelation=False, doScaleVariance=False)
572 # Verify that the variance plane of the difference image is correct
573 # when the template and science variance planes are incorrect
574 science.variance.array /= scaleFactor
575 template.variance.array /= scaleFactor
576 _run_and_check_images(science, template, sources, statsCtrl,
577 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
578 _run_and_check_images(science, template, sources, statsCtrl,
579 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
580 _run_and_check_images(science, template, sources, statsCtrl,
581 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
582 _run_and_check_images(science, template, sources, statsCtrl,
583 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
585 def test_exposure_properties_convolve_template(self):
586 """Check that all necessary exposure metadata is included
587 when the template is convolved.
588 """
589 noiseLevel = 1.
590 seed = 37
591 rng = np.random.RandomState(seed)
592 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
593 psf = science.psf
594 psfAvgPos = psf.getAveragePosition()
595 psfSize = getPsfFwhm(science.psf)
596 psfImg = psf.computeKernelImage(psfAvgPos)
597 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
598 templateBorderSize=20, doApplyCalibration=True)
600 # Generate a random aperture correction map
601 apCorrMap = lsst.afw.image.ApCorrMap()
602 for name in ("a", "b", "c"):
603 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
604 science.info.setApCorrMap(apCorrMap)
606 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
607 config.mode = "convolveTemplate"
609 def _run_and_check_images(doDecorrelation):
610 """Check that the metadata is correct with or without decorrelation.
611 """
612 config.doDecorrelation = doDecorrelation
613 task = subtractImages.AlardLuptonSubtractTask(config=config)
614 output = task.run(template.clone(), science.clone(), sources)
615 psfOut = output.difference.psf
616 psfAvgPos = psfOut.getAveragePosition()
617 if doDecorrelation:
618 # Decorrelation requires recalculating the PSF,
619 # so it will not be the same as the input
620 psfOutSize = getPsfFwhm(science.psf)
621 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
622 else:
623 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
624 self.assertImagesAlmostEqual(psfImg, psfOutImg)
626 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
627 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
628 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
629 self.assertEqual(science.filter, output.difference.filter)
630 self.assertEqual(science.photoCalib, output.difference.photoCalib)
631 _run_and_check_images(doDecorrelation=True)
632 _run_and_check_images(doDecorrelation=False)
634 def test_exposure_properties_convolve_science(self):
635 """Check that all necessary exposure metadata is included
636 when the science image is convolved.
637 """
638 noiseLevel = 1.
639 seed = 37
640 rng = np.random.RandomState(seed)
641 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
642 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
643 templateBorderSize=20, doApplyCalibration=True)
644 psf = template.psf
645 psfAvgPos = psf.getAveragePosition()
646 psfSize = getPsfFwhm(template.psf)
647 psfImg = psf.computeKernelImage(psfAvgPos)
649 # Generate a random aperture correction map
650 apCorrMap = lsst.afw.image.ApCorrMap()
651 for name in ("a", "b", "c"):
652 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
653 science.info.setApCorrMap(apCorrMap)
655 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
656 config.mode = "convolveScience"
658 def _run_and_check_images(doDecorrelation):
659 """Check that the metadata is correct with or without decorrelation.
660 """
661 config.doDecorrelation = doDecorrelation
662 task = subtractImages.AlardLuptonSubtractTask(config=config)
663 output = task.run(template.clone(), science.clone(), sources)
664 if doDecorrelation:
665 # Decorrelation requires recalculating the PSF,
666 # so it will not be the same as the input
667 psfOutSize = getPsfFwhm(template.psf)
668 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
669 else:
670 psfOut = output.difference.psf
671 psfAvgPos = psfOut.getAveragePosition()
672 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
673 self.assertImagesAlmostEqual(psfImg, psfOutImg)
675 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
676 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
677 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
678 self.assertEqual(science.filter, output.difference.filter)
679 self.assertEqual(science.photoCalib, output.difference.photoCalib)
681 _run_and_check_images(doDecorrelation=True)
682 _run_and_check_images(doDecorrelation=False)
684 def _compare_apCorrMaps(self, a, b):
685 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
686 same addresses (i.e. so we can compare after serialization).
688 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
690 Parameters
691 ----------
692 a, b : `lsst.afw.image.ApCorrMap`
693 The two aperture correction maps to compare.
694 """
695 self.assertEqual(len(a), len(b))
696 for name, value in list(a.items()):
697 value2 = b.get(name)
698 self.assertIsNotNone(value2)
699 self.assertEqual(value.getBBox(), value2.getBBox())
700 self.assertFloatsAlmostEqual(
701 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
704def _makeStats(badMaskPlanes=None):
705 """Create a statistics control for configuring calculations on images.
707 Parameters
708 ----------
709 badMaskPlanes : `list` of `str`, optional
710 List of mask planes to exclude from calculations.
712 Returns
713 -------
714 statsControl : ` lsst.afw.math.StatisticsControl`
715 Statistics control object for configuring calculations on images.
716 """
717 if badMaskPlanes is None:
718 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR",
719 "BAD", "NO_DATA", "DETECTED_NEGATIVE")
720 statsControl = afwMath.StatisticsControl()
721 statsControl.setNumSigmaClip(3.)
722 statsControl.setNumIter(3)
723 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes))
724 return statsControl
727def _computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP):
728 """Calculate a robust mean of the variance plane of an exposure.
730 Parameters
731 ----------
732 image : `lsst.afw.image.Image`
733 Image or variance plane of an exposure to evaluate.
734 mask : `lsst.afw.image.Mask`
735 Mask plane to use for excluding pixels.
736 statsCtrl : `lsst.afw.math.StatisticsControl`
737 Statistics control object for configuring the calculation.
738 statistic : `lsst.afw.math.Property`, optional
739 The type of statistic to compute. Typical values are
740 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``.
742 Returns
743 -------
744 value : `float`
745 The result of the statistic calculated from the unflagged pixels.
746 """
747 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl)
748 return statObj.getValue(statistic)
751def setup_module(module):
752 lsst.utils.tests.init()
755class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
756 pass
759if __name__ == "__main__": 759 ↛ 760line 759 didn't jump to line 760, because the condition on line 759 was never true
760 lsst.utils.tests.init()
761 unittest.main()