Coverage for tests/test_subtractTask.py: 9%
393 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-01 02:47 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-01 02:47 -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
24import lsst.afw.math as afwMath
25import lsst.afw.table as afwTable
26import lsst.geom
27import lsst.ip.diffim.imagePsfMatch
28import lsst.meas.algorithms as measAlg
29import lsst.utils.tests
30import numpy as np
31from lsst.ip.diffim import subtractImages
32from lsst.ip.diffim.utils import (computeRobustStatistics, evaluateMeanPsfFwhm,
33 getPsfFwhm, makeStats, makeTestImage)
34from lsst.pex.config import FieldValidationError
35from lsst.pex.exceptions import InvalidParameterError
38class CustomCoaddPsf(measAlg.CoaddPsf):
39 """A custom CoaddPSF that overrides the getAveragePosition method.
40 """
41 def getAveragePosition(self):
42 return lsst.geom.Point2D(-10000, -10000)
45class AlardLuptonSubtractTest(lsst.utils.tests.TestCase):
47 def test_allowed_config_modes(self):
48 """Verify the allowable modes for convolution.
49 """
50 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
51 config.mode = 'auto'
52 config.mode = 'convolveScience'
53 config.mode = 'convolveTemplate'
55 with self.assertRaises(FieldValidationError):
56 config.mode = 'aotu'
58 def test_mismatched_template(self):
59 """Test that an error is raised if the template
60 does not fully contain the science image.
61 """
62 xSize = 200
63 ySize = 200
64 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
65 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
66 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
67 task = subtractImages.AlardLuptonSubtractTask(config=config)
68 with self.assertRaises(AssertionError):
69 task.run(template, science, sources)
71 def test_equal_images(self):
72 """Test that running with enough sources produces reasonable output,
73 with the same size psf in the template and science.
74 """
75 noiseLevel = 1.
76 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
77 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
78 templateBorderSize=20, doApplyCalibration=True)
79 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
80 config.doSubtractBackground = False
81 task = subtractImages.AlardLuptonSubtractTask(config=config)
82 output = task.run(template, science, sources)
83 # There shoud be no NaN values in the difference image
84 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
85 # Mean of difference image should be close to zero.
86 meanError = noiseLevel/np.sqrt(output.difference.image.array.size)
87 # Make sure to include pixels with the DETECTED mask bit set.
88 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
89 differenceMean = computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl)
90 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError)
91 # stddev of difference image should be close to expected value.
92 differenceStd = computeRobustStatistics(output.difference.image, output.difference.mask,
93 makeStats(), statistic=afwMath.STDEV)
94 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1)
96 def test_psf_size(self):
97 """Test that the image subtract task runs without failing, if
98 fwhmExposureBuffer and fwhmExposureGrid parameters are set.
99 """
100 noiseLevel = 1.
101 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
102 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
103 templateBorderSize=20, doApplyCalibration=True)
105 schema = afwTable.ExposureTable.makeMinimalSchema()
106 weightKey = schema.addField("weight", type="D", doc="Coadd weight")
107 exposureCatalog = afwTable.ExposureCatalog(schema)
108 kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
109 psf = measAlg.KernelPsf(kernel, template.getBBox().getCenter())
111 record = exposureCatalog.addNew()
112 record.setPsf(psf)
113 record.setWcs(template.wcs)
114 record.setD(weightKey, 1.0)
115 record.setBBox(template.getBBox())
117 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs)
118 template.setPsf(customPsf)
120 # Test that we get an exception if we simply get the FWHM at center.
121 with self.assertRaises(InvalidParameterError):
122 getPsfFwhm(template.psf, True)
124 with self.assertRaises(InvalidParameterError):
125 getPsfFwhm(template.psf, False)
127 # Test that evaluateMeanPsfFwhm runs successfully on the template.
128 evaluateMeanPsfFwhm(template, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
130 # Since the PSF is spatially invariant, the FWHM should be the same at
131 # all points in the science image.
132 fwhm1 = getPsfFwhm(science.psf, False)
133 fwhm2 = evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
134 self.assertAlmostEqual(fwhm1[0], fwhm2, places=13)
135 self.assertAlmostEqual(fwhm1[1], fwhm2, places=13)
137 self.assertAlmostEqual(evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05,
138 fwhmExposureGrid=10),
139 getPsfFwhm(science.psf, True), places=7
140 )
142 # Test that the image subtraction task runs successfully.
143 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
144 config.doSubtractBackground = False
145 task = subtractImages.AlardLuptonSubtractTask(config=config)
147 # Test that the task runs if we take the mean FWHM on a grid.
148 with self.assertLogs(level="INFO") as cm:
149 task.run(template, science, sources)
151 # Check that evaluateMeanPsfFwhm was called.
152 # This tests that getPsfFwhm failed raising InvalidParameterError,
153 # that is caught and handled appropriately.
154 logMessage = ("INFO:lsst.alardLuptonSubtract:Unable to evaluate PSF at the average position. "
155 "Evaluting PSF on a grid of points."
156 )
157 self.assertIn(logMessage, cm.output)
159 def test_auto_convolveTemplate(self):
160 """Test that auto mode gives the same result as convolveTemplate when
161 the template psf is the smaller.
162 """
163 noiseLevel = 1.
164 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
165 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
166 templateBorderSize=20, doApplyCalibration=True)
167 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
168 config.doSubtractBackground = False
169 config.mode = "convolveTemplate"
171 task = subtractImages.AlardLuptonSubtractTask(config=config)
172 output = task.run(template.clone(), science.clone(), sources)
174 config.mode = "auto"
175 task = subtractImages.AlardLuptonSubtractTask(config=config)
176 outputAuto = task.run(template, science, sources)
177 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
179 def test_auto_convolveScience(self):
180 """Test that auto mode gives the same result as convolveScience when
181 the science psf is the smaller.
182 """
183 noiseLevel = 1.
184 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
185 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
186 templateBorderSize=20, doApplyCalibration=True)
187 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
188 config.doSubtractBackground = False
189 config.mode = "convolveScience"
191 task = subtractImages.AlardLuptonSubtractTask(config=config)
192 output = task.run(template.clone(), science.clone(), sources)
194 config.mode = "auto"
195 task = subtractImages.AlardLuptonSubtractTask(config=config)
196 outputAuto = task.run(template, science, sources)
197 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
199 def test_science_better(self):
200 """Test that running with enough sources produces reasonable output,
201 with the science psf being smaller than the template.
202 """
203 statsCtrl = makeStats()
204 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
206 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
207 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
208 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
209 templateBorderSize=20, doApplyCalibration=True)
210 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
211 config.doSubtractBackground = False
212 config.mode = "convolveScience"
213 task = subtractImages.AlardLuptonSubtractTask(config=config)
214 output = task.run(template, science, sources)
215 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
216 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
217 # Mean of difference image should be close to zero.
218 nGoodPix = np.sum(np.isfinite(output.difference.image.array))
219 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix)
220 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
221 statsCtrlDetect)
223 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
224 # stddev of difference image should be close to expected value.
225 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
226 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
227 statsCtrl)
228 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
229 statsCtrl, statistic=afwMath.STDEV)
230 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
231 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
233 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
234 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
235 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
237 def test_template_better(self):
238 """Test that running with enough sources produces reasonable output,
239 with the template psf being smaller than the science.
240 """
241 statsCtrl = makeStats()
242 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
244 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
245 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
246 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
247 templateBorderSize=20, doApplyCalibration=True)
248 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
249 config.doSubtractBackground = False
250 task = subtractImages.AlardLuptonSubtractTask(config=config)
251 output = task.run(template, science, sources)
252 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
253 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
254 # There should be no NaNs in the image if we convolve the template with a buffer
255 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
256 # Mean of difference image should be close to zero.
257 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size)
259 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
260 statsCtrlDetect)
261 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
262 # stddev of difference image should be close to expected value.
263 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
264 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
265 statsCtrl)
266 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
267 statsCtrl, statistic=afwMath.STDEV)
268 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
269 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
271 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
272 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
273 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
275 def test_symmetry(self):
276 """Test that convolving the science and convolving the template are
277 symmetric: if the psfs are switched between them, the difference image
278 should be nearly the same.
279 """
280 noiseLevel = 1.
281 # Don't include a border for the template, in order to make the results
282 # comparable when we swap which image is treated as the "science" image.
283 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
284 noiseSeed=6, templateBorderSize=0)
285 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
286 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True)
287 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
288 config.mode = 'auto'
289 config.doSubtractBackground = False
290 task = subtractImages.AlardLuptonSubtractTask(config=config)
292 # The science image will be modified in place, so use a copy for the second run.
293 science_better = task.run(template.clone(), science.clone(), sources)
294 template_better = task.run(science, template, sources)
296 delta = template_better.difference.clone()
297 delta.image -= science_better.difference.image
298 delta.variance -= science_better.difference.variance
299 delta.mask.array -= science_better.difference.mask.array
301 statsCtrl = makeStats()
302 # Mean of delta should be very close to zero.
303 nGoodPix = np.sum(np.isfinite(delta.image.array))
304 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
305 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
306 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV)
307 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
308 # stddev of difference image should be close to expected value
309 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1)
311 def test_few_sources(self):
312 """Test with only 1 source, to check that we get a useful error.
313 """
314 xSize = 256
315 ySize = 256
316 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
317 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
318 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
319 task = subtractImages.AlardLuptonSubtractTask(config=config)
320 sources = sources[0:1]
321 with self.assertRaisesRegex(RuntimeError,
322 "Cannot compute PSF matching kernel: too few sources selected."):
323 task.run(template, science, sources)
325 def test_order_equal_images(self):
326 """Verify that the result is the same regardless of convolution mode
327 if the images are equivalent.
328 """
329 noiseLevel = .1
330 seed1 = 6
331 seed2 = 7
332 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
333 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
334 templateBorderSize=0, doApplyCalibration=True)
335 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
336 config1.mode = "convolveTemplate"
337 config1.doSubtractBackground = False
338 task1 = subtractImages.AlardLuptonSubtractTask(config=config1)
339 results_convolveTemplate = task1.run(template1, science1, sources1)
341 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1)
342 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
343 templateBorderSize=0, doApplyCalibration=True)
344 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
345 config2.mode = "convolveScience"
346 config2.doSubtractBackground = False
347 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
348 results_convolveScience = task2.run(template2, science2, sources2)
349 diff1 = science1.maskedImage.clone()
350 diff1 -= template1.maskedImage
351 diff2 = science2.maskedImage.clone()
352 diff2 -= template2.maskedImage
353 self.assertFloatsAlmostEqual(results_convolveTemplate.difference.image.array,
354 diff1.image.array,
355 atol=noiseLevel*5.)
356 self.assertFloatsAlmostEqual(results_convolveScience.difference.image.array,
357 diff2.image.array,
358 atol=noiseLevel*5.)
359 diffErr = noiseLevel*2
360 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage,
361 results_convolveScience.difference.maskedImage,
362 atol=diffErr*5.)
364 def test_background_subtraction(self):
365 """Check that we can recover the background,
366 and that it is subtracted correctly in the difference image.
367 """
368 noiseLevel = 1.
369 xSize = 512
370 ySize = 512
371 x0 = 123
372 y0 = 456
373 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
374 templateBorderSize=20,
375 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
376 doApplyCalibration=True)
377 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
379 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
380 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
381 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
382 background=background_model,
383 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
384 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
385 config.doSubtractBackground = True
387 config.makeKernel.kernel.name = "AL"
388 config.makeKernel.kernel.active.fitForBackground = True
389 config.makeKernel.kernel.active.spatialKernelOrder = 1
390 config.makeKernel.kernel.active.spatialBgOrder = 2
391 statsCtrl = makeStats()
393 def _run_and_check_images(config, statsCtrl, mode):
394 """Check that the fit background matches the input model.
395 """
396 config.mode = mode
397 task = subtractImages.AlardLuptonSubtractTask(config=config)
398 output = task.run(template.clone(), science.clone(), sources)
400 # We should be fitting the same number of parameters as were in the input model
401 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
403 # The parameters of the background fit should be close to the input model
404 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
405 np.array(params), rtol=0.3)
407 # stddev of difference image should be close to expected value.
408 # This will fail if we have mis-subtracted the background.
409 stdVal = computeRobustStatistics(output.difference.image, output.difference.mask,
410 statsCtrl, statistic=afwMath.STDEV)
411 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
413 _run_and_check_images(config, statsCtrl, "convolveTemplate")
414 _run_and_check_images(config, statsCtrl, "convolveScience")
416 def test_scale_variance_convolve_template(self):
417 """Check variance scaling of the image difference.
418 """
419 scienceNoiseLevel = 4.
420 templateNoiseLevel = 2.
421 scaleFactor = 1.345
422 # Make sure to include pixels with the DETECTED mask bit set.
423 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
425 def _run_and_check_images(science, template, sources, statsCtrl,
426 doDecorrelation, doScaleVariance, scaleFactor=1.):
427 """Check that the variance plane matches the expected value for
428 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
429 """
431 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
432 config.doSubtractBackground = False
433 config.doDecorrelation = doDecorrelation
434 config.doScaleVariance = doScaleVariance
435 task = subtractImages.AlardLuptonSubtractTask(config=config)
436 output = task.run(template.clone(), science.clone(), sources)
437 if doScaleVariance:
438 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
439 scaleFactor, atol=0.05)
440 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
441 scaleFactor, atol=0.05)
443 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
444 if doDecorrelation:
445 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
446 else:
447 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
448 output.matchedTemplate.mask,
449 statsCtrl)
451 if doScaleVariance:
452 templateNoise *= scaleFactor
453 scienceNoise *= scaleFactor
454 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
455 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
457 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
458 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
459 templateBorderSize=20, doApplyCalibration=True)
460 # Verify that the variance plane of the difference image is correct
461 # when the template and science variance planes are correct
462 _run_and_check_images(science, template, sources, statsCtrl,
463 doDecorrelation=True, doScaleVariance=True)
464 _run_and_check_images(science, template, sources, statsCtrl,
465 doDecorrelation=True, doScaleVariance=False)
466 _run_and_check_images(science, template, sources, statsCtrl,
467 doDecorrelation=False, doScaleVariance=True)
468 _run_and_check_images(science, template, sources, statsCtrl,
469 doDecorrelation=False, doScaleVariance=False)
471 # Verify that the variance plane of the difference image is correct
472 # when the template variance plane is incorrect
473 template.variance.array /= scaleFactor
474 science.variance.array /= scaleFactor
475 _run_and_check_images(science, template, sources, statsCtrl,
476 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
477 _run_and_check_images(science, template, sources, statsCtrl,
478 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
479 _run_and_check_images(science, template, sources, statsCtrl,
480 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
481 _run_and_check_images(science, template, sources, statsCtrl,
482 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
484 def test_scale_variance_convolve_science(self):
485 """Check variance scaling of the image difference.
486 """
487 scienceNoiseLevel = 4.
488 templateNoiseLevel = 2.
489 scaleFactor = 1.345
490 # Make sure to include pixels with the DETECTED mask bit set.
491 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
493 def _run_and_check_images(science, template, sources, statsCtrl,
494 doDecorrelation, doScaleVariance, scaleFactor=1.):
495 """Check that the variance plane matches the expected value for
496 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
497 """
499 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
500 config.mode = "convolveScience"
501 config.doSubtractBackground = False
502 config.doDecorrelation = doDecorrelation
503 config.doScaleVariance = doScaleVariance
504 task = subtractImages.AlardLuptonSubtractTask(config=config)
505 output = task.run(template.clone(), science.clone(), sources)
506 if doScaleVariance:
507 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
508 scaleFactor, atol=0.05)
509 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
510 scaleFactor, atol=0.05)
512 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
513 if doDecorrelation:
514 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
515 else:
516 scienceNoise = computeRobustStatistics(output.matchedScience.variance,
517 output.matchedScience.mask,
518 statsCtrl)
520 if doScaleVariance:
521 templateNoise *= scaleFactor
522 scienceNoise *= scaleFactor
524 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
525 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
527 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
528 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
529 templateBorderSize=20, doApplyCalibration=True)
530 # Verify that the variance plane of the difference image is correct
531 # when the template and science variance planes are correct
532 _run_and_check_images(science, template, sources, statsCtrl,
533 doDecorrelation=True, doScaleVariance=True)
534 _run_and_check_images(science, template, sources, statsCtrl,
535 doDecorrelation=True, doScaleVariance=False)
536 _run_and_check_images(science, template, sources, statsCtrl,
537 doDecorrelation=False, doScaleVariance=True)
538 _run_and_check_images(science, template, sources, statsCtrl,
539 doDecorrelation=False, doScaleVariance=False)
541 # Verify that the variance plane of the difference image is correct
542 # when the template and science variance planes are incorrect
543 science.variance.array /= scaleFactor
544 template.variance.array /= scaleFactor
545 _run_and_check_images(science, template, sources, statsCtrl,
546 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
547 _run_and_check_images(science, template, sources, statsCtrl,
548 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
549 _run_and_check_images(science, template, sources, statsCtrl,
550 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
551 _run_and_check_images(science, template, sources, statsCtrl,
552 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
554 def test_exposure_properties_convolve_template(self):
555 """Check that all necessary exposure metadata is included
556 when the template is convolved.
557 """
558 noiseLevel = 1.
559 seed = 37
560 rng = np.random.RandomState(seed)
561 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
562 psf = science.psf
563 psfAvgPos = psf.getAveragePosition()
564 psfSize = getPsfFwhm(science.psf)
565 psfImg = psf.computeKernelImage(psfAvgPos)
566 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
567 templateBorderSize=20, doApplyCalibration=True)
569 # Generate a random aperture correction map
570 apCorrMap = lsst.afw.image.ApCorrMap()
571 for name in ("a", "b", "c"):
572 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
573 science.info.setApCorrMap(apCorrMap)
575 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
576 config.mode = "convolveTemplate"
578 def _run_and_check_images(doDecorrelation):
579 """Check that the metadata is correct with or without decorrelation.
580 """
581 config.doDecorrelation = doDecorrelation
582 task = subtractImages.AlardLuptonSubtractTask(config=config)
583 output = task.run(template.clone(), science.clone(), sources)
584 psfOut = output.difference.psf
585 psfAvgPos = psfOut.getAveragePosition()
586 if doDecorrelation:
587 # Decorrelation requires recalculating the PSF,
588 # so it will not be the same as the input
589 psfOutSize = getPsfFwhm(science.psf)
590 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
591 else:
592 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
593 self.assertImagesAlmostEqual(psfImg, psfOutImg)
595 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
596 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
597 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
598 self.assertEqual(science.filter, output.difference.filter)
599 self.assertEqual(science.photoCalib, output.difference.photoCalib)
600 _run_and_check_images(doDecorrelation=True)
601 _run_and_check_images(doDecorrelation=False)
603 def test_exposure_properties_convolve_science(self):
604 """Check that all necessary exposure metadata is included
605 when the science image is convolved.
606 """
607 noiseLevel = 1.
608 seed = 37
609 rng = np.random.RandomState(seed)
610 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
611 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
612 templateBorderSize=20, doApplyCalibration=True)
613 psf = template.psf
614 psfAvgPos = psf.getAveragePosition()
615 psfSize = getPsfFwhm(template.psf)
616 psfImg = psf.computeKernelImage(psfAvgPos)
618 # Generate a random aperture correction map
619 apCorrMap = lsst.afw.image.ApCorrMap()
620 for name in ("a", "b", "c"):
621 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
622 science.info.setApCorrMap(apCorrMap)
624 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
625 config.mode = "convolveScience"
627 def _run_and_check_images(doDecorrelation):
628 """Check that the metadata is correct with or without decorrelation.
629 """
630 config.doDecorrelation = doDecorrelation
631 task = subtractImages.AlardLuptonSubtractTask(config=config)
632 output = task.run(template.clone(), science.clone(), sources)
633 if doDecorrelation:
634 # Decorrelation requires recalculating the PSF,
635 # so it will not be the same as the input
636 psfOutSize = getPsfFwhm(template.psf)
637 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
638 else:
639 psfOut = output.difference.psf
640 psfAvgPos = psfOut.getAveragePosition()
641 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
642 self.assertImagesAlmostEqual(psfImg, psfOutImg)
644 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
645 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
646 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
647 self.assertEqual(science.filter, output.difference.filter)
648 self.assertEqual(science.photoCalib, output.difference.photoCalib)
650 _run_and_check_images(doDecorrelation=True)
651 _run_and_check_images(doDecorrelation=False)
653 def _compare_apCorrMaps(self, a, b):
654 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
655 same addresses (i.e. so we can compare after serialization).
657 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
659 Parameters
660 ----------
661 a, b : `lsst.afw.image.ApCorrMap`
662 The two aperture correction maps to compare.
663 """
664 self.assertEqual(len(a), len(b))
665 for name, value in list(a.items()):
666 value2 = b.get(name)
667 self.assertIsNotNone(value2)
668 self.assertEqual(value.getBBox(), value2.getBBox())
669 self.assertFloatsAlmostEqual(
670 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
673def setup_module(module):
674 lsst.utils.tests.init()
677class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
678 pass
681if __name__ == "__main__": 681 ↛ 682line 681 didn't jump to line 682, because the condition on line 681 was never true
682 lsst.utils.tests.init()
683 unittest.main()