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