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