Coverage for tests/test_subtractTask.py: 8%
542 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-01 03:20 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-01 03:20 -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
29from lsst.ip.diffim import subtractImages
30from lsst.pex.config import FieldValidationError
31import lsst.utils.tests
32import numpy as np
33from lsst.ip.diffim.utils import (computeRobustStatistics, computePSFNoiseEquivalentArea,
34 evaluateMeanPsfFwhm, getPsfFwhm, makeStats, makeTestImage)
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", "DETECTED", "DETECTED_NEGATIVE"))
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 bbox = results_convolveTemplate.difference.getBBox().clippedTo(
350 results_convolveScience.difference.getBBox())
351 diff1 = science1.maskedImage.clone()[bbox]
352 diff1 -= template1.maskedImage[bbox]
353 diff2 = science2.maskedImage.clone()[bbox]
354 diff2 -= template2.maskedImage[bbox]
355 self.assertFloatsAlmostEqual(results_convolveTemplate.difference[bbox].image.array,
356 diff1.image.array,
357 atol=noiseLevel*5.)
358 self.assertFloatsAlmostEqual(results_convolveScience.difference[bbox].image.array,
359 diff2.image.array,
360 atol=noiseLevel*5.)
361 diffErr = noiseLevel*2
362 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference[bbox].maskedImage,
363 results_convolveScience.difference[bbox].maskedImage,
364 atol=diffErr*5.)
366 def test_background_subtraction(self):
367 """Check that we can recover the background,
368 and that it is subtracted correctly in the difference image.
369 """
370 noiseLevel = 1.
371 xSize = 512
372 ySize = 512
373 x0 = 123
374 y0 = 456
375 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
376 templateBorderSize=20,
377 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
378 doApplyCalibration=True)
379 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
381 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
382 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
383 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
384 background=background_model,
385 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
386 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
387 config.doSubtractBackground = True
389 config.makeKernel.kernel.name = "AL"
390 config.makeKernel.kernel.active.fitForBackground = True
391 config.makeKernel.kernel.active.spatialKernelOrder = 1
392 config.makeKernel.kernel.active.spatialBgOrder = 2
393 statsCtrl = makeStats()
395 def _run_and_check_images(config, statsCtrl, mode):
396 """Check that the fit background matches the input model.
397 """
398 config.mode = mode
399 task = subtractImages.AlardLuptonSubtractTask(config=config)
400 output = task.run(template.clone(), science.clone(), sources)
402 # We should be fitting the same number of parameters as were in the input model
403 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
405 # The parameters of the background fit should be close to the input model
406 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
407 np.array(params), rtol=0.3)
409 # stddev of difference image should be close to expected value.
410 # This will fail if we have mis-subtracted the background.
411 stdVal = computeRobustStatistics(output.difference.image, output.difference.mask,
412 statsCtrl, statistic=afwMath.STDEV)
413 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
415 _run_and_check_images(config, statsCtrl, "convolveTemplate")
416 _run_and_check_images(config, statsCtrl, "convolveScience")
418 def test_scale_variance_convolve_template(self):
419 """Check variance scaling of the image difference.
420 """
421 scienceNoiseLevel = 4.
422 templateNoiseLevel = 2.
423 scaleFactor = 1.345
424 # Make sure to include pixels with the DETECTED mask bit set.
425 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
427 def _run_and_check_images(science, template, sources, statsCtrl,
428 doDecorrelation, doScaleVariance, scaleFactor=1.):
429 """Check that the variance plane matches the expected value for
430 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
431 """
433 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
434 config.doSubtractBackground = False
435 config.doDecorrelation = doDecorrelation
436 config.doScaleVariance = doScaleVariance
437 task = subtractImages.AlardLuptonSubtractTask(config=config)
438 output = task.run(template.clone(), science.clone(), sources)
439 if doScaleVariance:
440 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
441 scaleFactor, atol=0.05)
442 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
443 scaleFactor, atol=0.05)
445 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
446 if doDecorrelation:
447 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
448 else:
449 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
450 output.matchedTemplate.mask,
451 statsCtrl)
453 if doScaleVariance:
454 templateNoise *= scaleFactor
455 scienceNoise *= scaleFactor
456 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
457 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
459 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
460 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
461 templateBorderSize=20, doApplyCalibration=True)
462 # Verify that the variance plane of the difference image is correct
463 # when the template and science variance planes are correct
464 _run_and_check_images(science, template, sources, statsCtrl,
465 doDecorrelation=True, doScaleVariance=True)
466 _run_and_check_images(science, template, sources, statsCtrl,
467 doDecorrelation=True, doScaleVariance=False)
468 _run_and_check_images(science, template, sources, statsCtrl,
469 doDecorrelation=False, doScaleVariance=True)
470 _run_and_check_images(science, template, sources, statsCtrl,
471 doDecorrelation=False, doScaleVariance=False)
473 # Verify that the variance plane of the difference image is correct
474 # when the template variance plane is incorrect
475 template.variance.array /= scaleFactor
476 science.variance.array /= scaleFactor
477 _run_and_check_images(science, template, sources, statsCtrl,
478 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
479 _run_and_check_images(science, template, sources, statsCtrl,
480 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
481 _run_and_check_images(science, template, sources, statsCtrl,
482 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
483 _run_and_check_images(science, template, sources, statsCtrl,
484 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
486 def test_scale_variance_convolve_science(self):
487 """Check variance scaling of the image difference.
488 """
489 scienceNoiseLevel = 4.
490 templateNoiseLevel = 2.
491 scaleFactor = 1.345
492 # Make sure to include pixels with the DETECTED mask bit set.
493 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
495 def _run_and_check_images(science, template, sources, statsCtrl,
496 doDecorrelation, doScaleVariance, scaleFactor=1.):
497 """Check that the variance plane matches the expected value for
498 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
499 """
501 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
502 config.mode = "convolveScience"
503 config.doSubtractBackground = False
504 config.doDecorrelation = doDecorrelation
505 config.doScaleVariance = doScaleVariance
506 task = subtractImages.AlardLuptonSubtractTask(config=config)
507 output = task.run(template.clone(), science.clone(), sources)
508 if doScaleVariance:
509 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
510 scaleFactor, atol=0.05)
511 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
512 scaleFactor, atol=0.05)
514 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
515 if doDecorrelation:
516 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
517 else:
518 scienceNoise = computeRobustStatistics(output.matchedScience.variance,
519 output.matchedScience.mask,
520 statsCtrl)
522 if doScaleVariance:
523 templateNoise *= scaleFactor
524 scienceNoise *= scaleFactor
526 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
527 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
529 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
530 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
531 templateBorderSize=20, doApplyCalibration=True)
532 # Verify that the variance plane of the difference image is correct
533 # when the template and science variance planes are correct
534 _run_and_check_images(science, template, sources, statsCtrl,
535 doDecorrelation=True, doScaleVariance=True)
536 _run_and_check_images(science, template, sources, statsCtrl,
537 doDecorrelation=True, doScaleVariance=False)
538 _run_and_check_images(science, template, sources, statsCtrl,
539 doDecorrelation=False, doScaleVariance=True)
540 _run_and_check_images(science, template, sources, statsCtrl,
541 doDecorrelation=False, doScaleVariance=False)
543 # Verify that the variance plane of the difference image is correct
544 # when the template and science variance planes are incorrect
545 science.variance.array /= scaleFactor
546 template.variance.array /= scaleFactor
547 _run_and_check_images(science, template, sources, statsCtrl,
548 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
549 _run_and_check_images(science, template, sources, statsCtrl,
550 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
551 _run_and_check_images(science, template, sources, statsCtrl,
552 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
553 _run_and_check_images(science, template, sources, statsCtrl,
554 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
556 def test_exposure_properties_convolve_template(self):
557 """Check that all necessary exposure metadata is included
558 when the template is convolved.
559 """
560 noiseLevel = 1.
561 seed = 37
562 rng = np.random.RandomState(seed)
563 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
564 psf = science.psf
565 psfAvgPos = psf.getAveragePosition()
566 psfSize = getPsfFwhm(science.psf)
567 psfImg = psf.computeKernelImage(psfAvgPos)
568 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
569 templateBorderSize=20, doApplyCalibration=True)
571 # Generate a random aperture correction map
572 apCorrMap = lsst.afw.image.ApCorrMap()
573 for name in ("a", "b", "c"):
574 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
575 science.info.setApCorrMap(apCorrMap)
577 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
578 config.mode = "convolveTemplate"
580 def _run_and_check_images(doDecorrelation):
581 """Check that the metadata is correct with or without decorrelation.
582 """
583 config.doDecorrelation = doDecorrelation
584 task = subtractImages.AlardLuptonSubtractTask(config=config)
585 output = task.run(template.clone(), science.clone(), sources)
586 psfOut = output.difference.psf
587 psfAvgPos = psfOut.getAveragePosition()
588 if doDecorrelation:
589 # Decorrelation requires recalculating the PSF,
590 # so it will not be the same as the input
591 psfOutSize = getPsfFwhm(science.psf)
592 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
593 else:
594 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
595 self.assertImagesAlmostEqual(psfImg, psfOutImg)
597 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
598 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
599 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
600 self.assertEqual(science.filter, output.difference.filter)
601 self.assertEqual(science.photoCalib, output.difference.photoCalib)
602 _run_and_check_images(doDecorrelation=True)
603 _run_and_check_images(doDecorrelation=False)
605 def test_exposure_properties_convolve_science(self):
606 """Check that all necessary exposure metadata is included
607 when the science image is convolved.
608 """
609 noiseLevel = 1.
610 seed = 37
611 rng = np.random.RandomState(seed)
612 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
613 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
614 templateBorderSize=20, doApplyCalibration=True)
615 psf = template.psf
616 psfAvgPos = psf.getAveragePosition()
617 psfSize = getPsfFwhm(template.psf)
618 psfImg = psf.computeKernelImage(psfAvgPos)
620 # Generate a random aperture correction map
621 apCorrMap = lsst.afw.image.ApCorrMap()
622 for name in ("a", "b", "c"):
623 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
624 science.info.setApCorrMap(apCorrMap)
626 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
627 config.mode = "convolveScience"
629 def _run_and_check_images(doDecorrelation):
630 """Check that the metadata is correct with or without decorrelation.
631 """
632 config.doDecorrelation = doDecorrelation
633 task = subtractImages.AlardLuptonSubtractTask(config=config)
634 output = task.run(template.clone(), science.clone(), sources)
635 if doDecorrelation:
636 # Decorrelation requires recalculating the PSF,
637 # so it will not be the same as the input
638 psfOutSize = getPsfFwhm(template.psf)
639 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
640 else:
641 psfOut = output.difference.psf
642 psfAvgPos = psfOut.getAveragePosition()
643 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
644 self.assertImagesAlmostEqual(psfImg, psfOutImg)
646 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
647 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
648 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
649 self.assertEqual(science.filter, output.difference.filter)
650 self.assertEqual(science.photoCalib, output.difference.photoCalib)
652 _run_and_check_images(doDecorrelation=True)
653 _run_and_check_images(doDecorrelation=False)
655 def _compare_apCorrMaps(self, a, b):
656 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
657 same addresses (i.e. so we can compare after serialization).
659 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
661 Parameters
662 ----------
663 a, b : `lsst.afw.image.ApCorrMap`
664 The two aperture correction maps to compare.
665 """
666 self.assertEqual(len(a), len(b))
667 for name, value in list(a.items()):
668 value2 = b.get(name)
669 self.assertIsNotNone(value2)
670 self.assertEqual(value.getBBox(), value2.getBBox())
671 self.assertFloatsAlmostEqual(
672 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
675class AlardLuptonPreconvolveSubtractTest(lsst.utils.tests.TestCase):
677 def test_mismatched_template(self):
678 """Test that an error is raised if the template
679 does not fully contain the science image.
680 """
681 xSize = 200
682 ySize = 200
683 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
684 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
685 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
686 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
687 with self.assertRaises(AssertionError):
688 task.run(template, science, sources)
690 def test_equal_images(self):
691 """Test that running with enough sources produces reasonable output,
692 with the same size psf in the template and science.
693 """
694 noiseLevel = 1.
695 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
696 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
697 templateBorderSize=20, doApplyCalibration=True)
698 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
699 config.doSubtractBackground = False
700 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
701 output = task.run(template, science, sources)
702 # There shoud be no NaN values in the Score image
703 self.assertTrue(np.all(np.isfinite(output.scoreExposure.image.array)))
704 # Mean of Score image should be close to zero.
705 meanError = noiseLevel/np.sqrt(output.scoreExposure.image.array.size)
706 # Make sure to include pixels with the DETECTED mask bit set.
707 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
708 scoreMean = computeRobustStatistics(output.scoreExposure.image,
709 output.scoreExposure.mask,
710 statsCtrl)
711 self.assertFloatsAlmostEqual(scoreMean, 0, atol=5*meanError)
712 nea = computePSFNoiseEquivalentArea(science.psf)
713 # stddev of Score image should be close to expected value.
714 scoreStd = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
715 statsCtrl=statsCtrl, statistic=afwMath.STDEV)
716 self.assertFloatsAlmostEqual(scoreStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
718 def test_agnostic_template_psf(self):
719 """Test that the Score image is the same whether the template PSF is
720 larger or smaller than the science image PSF.
721 """
722 noiseLevel = .3
723 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel,
724 noiseSeed=6, templateBorderSize=0)
725 template1, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
726 noiseSeed=7, doApplyCalibration=True)
727 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
728 noiseSeed=8, doApplyCalibration=True)
729 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
730 config.doSubtractBackground = False
731 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
733 science_better = task.run(template1, science.clone(), sources)
734 template_better = task.run(template2, science, sources)
735 bbox = science_better.scoreExposure.getBBox().clippedTo(template_better.scoreExposure.getBBox())
737 delta = template_better.scoreExposure[bbox].clone()
738 delta.image -= science_better.scoreExposure[bbox].image
739 delta.variance -= science_better.scoreExposure[bbox].variance
740 delta.mask.array &= science_better.scoreExposure[bbox].mask.array
742 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
743 # Mean of delta should be very close to zero.
744 nGoodPix = np.sum(np.isfinite(delta.image.array))
745 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
746 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
747 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl,
748 statistic=afwMath.STDEV)
749 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
750 nea = computePSFNoiseEquivalentArea(science.psf)
751 # stddev of Score image should be close to expected value
752 self.assertFloatsAlmostEqual(deltaStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=.1)
754 def test_few_sources(self):
755 """Test with only 1 source, to check that we get a useful error.
756 """
757 xSize = 256
758 ySize = 256
759 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
760 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
761 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
762 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
763 sources = sources[0:1]
764 with self.assertRaisesRegex(RuntimeError,
765 "Cannot compute PSF matching kernel: too few sources selected."):
766 task.run(template, science, sources)
768 def test_background_subtraction(self):
769 """Check that we can recover the background,
770 and that it is subtracted correctly in the Score image.
771 """
772 noiseLevel = 1.
773 xSize = 512
774 ySize = 512
775 x0 = 123
776 y0 = 456
777 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
778 templateBorderSize=20,
779 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
780 doApplyCalibration=True)
781 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
783 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
784 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
785 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
786 background=background_model,
787 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
788 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
789 config.doSubtractBackground = True
791 config.makeKernel.kernel.name = "AL"
792 config.makeKernel.kernel.active.fitForBackground = True
793 config.makeKernel.kernel.active.spatialKernelOrder = 1
794 config.makeKernel.kernel.active.spatialBgOrder = 2
795 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
797 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
798 output = task.run(template.clone(), science.clone(), sources)
800 # We should be fitting the same number of parameters as were in the input model
801 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
803 # The parameters of the background fit should be close to the input model
804 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
805 np.array(params), rtol=0.2)
807 # stddev of Score image should be close to expected value.
808 # This will fail if we have mis-subtracted the background.
809 stdVal = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
810 statsCtrl, statistic=afwMath.STDEV)
811 # get the img psf Noise Equivalent Area value
812 nea = computePSFNoiseEquivalentArea(science.psf)
813 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
815 def test_scale_variance(self):
816 """Check variance scaling of the Score image.
817 """
818 scienceNoiseLevel = 4.
819 templateNoiseLevel = 2.
820 scaleFactor = 1.345
821 # Make sure to include pixels with the DETECTED mask bit set.
822 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
824 def _run_and_check_images(science, template, sources, statsCtrl,
825 doDecorrelation, doScaleVariance, scaleFactor=1.):
826 """Check that the variance plane matches the expected value for
827 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
828 """
830 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
831 config.doSubtractBackground = False
832 config.doDecorrelation = doDecorrelation
833 config.doScaleVariance = doScaleVariance
834 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
835 output = task.run(template.clone(), science.clone(), sources)
836 if doScaleVariance:
837 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
838 scaleFactor, atol=0.05)
839 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
840 scaleFactor, atol=0.05)
842 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
843 # get the img psf Noise Equivalent Area value
844 nea = computePSFNoiseEquivalentArea(science.psf)
845 scienceNoise /= nea
846 if doDecorrelation:
847 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
848 templateNoise /= nea
849 else:
850 # Don't divide by NEA in this case, since the template is convolved
851 # and in the same units as the Score exposure.
852 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
853 output.matchedTemplate.mask,
854 statsCtrl)
855 if doScaleVariance:
856 templateNoise *= scaleFactor
857 scienceNoise *= scaleFactor
858 varMean = computeRobustStatistics(output.scoreExposure.variance,
859 output.scoreExposure.mask,
860 statsCtrl)
861 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
863 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
864 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
865 templateBorderSize=20, doApplyCalibration=True)
866 # Verify that the variance plane of the Score image is correct
867 # when the template and science variance planes are correct
868 _run_and_check_images(science, template, sources, statsCtrl,
869 doDecorrelation=True, doScaleVariance=True)
870 _run_and_check_images(science, template, sources, statsCtrl,
871 doDecorrelation=True, doScaleVariance=False)
872 _run_and_check_images(science, template, sources, statsCtrl,
873 doDecorrelation=False, doScaleVariance=True)
874 _run_and_check_images(science, template, sources, statsCtrl,
875 doDecorrelation=False, doScaleVariance=False)
877 # Verify that the variance plane of the Score image is correct
878 # when the template variance plane is incorrect
879 template.variance.array /= scaleFactor
880 science.variance.array /= scaleFactor
881 _run_and_check_images(science, template, sources, statsCtrl,
882 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
883 _run_and_check_images(science, template, sources, statsCtrl,
884 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
885 _run_and_check_images(science, template, sources, statsCtrl,
886 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
887 _run_and_check_images(science, template, sources, statsCtrl,
888 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
890 def test_exposure_properties(self):
891 """Check that all necessary exposure metadata is included
892 with the Score image.
893 """
894 noiseLevel = 1.
895 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
896 psf = science.psf
897 psfAvgPos = psf.getAveragePosition()
898 psfSize = getPsfFwhm(science.psf)
899 psfImg = psf.computeKernelImage(psfAvgPos)
900 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
901 templateBorderSize=20, doApplyCalibration=True)
903 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
905 def _run_and_check_images(doDecorrelation):
906 """Check that the metadata is correct with or without decorrelation.
907 """
908 config.doDecorrelation = doDecorrelation
909 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
910 output = task.run(template.clone(), science.clone(), sources)
911 psfOut = output.scoreExposure.psf
912 psfAvgPos = psfOut.getAveragePosition()
913 if doDecorrelation:
914 # Decorrelation requires recalculating the PSF,
915 # so it will not be the same as the input
916 psfOutSize = getPsfFwhm(science.psf)
917 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
918 else:
919 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
920 self.assertImagesAlmostEqual(psfImg, psfOutImg)
922 # check PSF, WCS, bbox, filterLabel, photoCalib
923 self.assertWcsAlmostEqualOverBBox(science.wcs, output.scoreExposure.wcs, science.getBBox())
924 self.assertEqual(science.filter, output.scoreExposure.filter)
925 self.assertEqual(science.photoCalib, output.scoreExposure.photoCalib)
926 _run_and_check_images(doDecorrelation=True)
927 _run_and_check_images(doDecorrelation=False)
930def setup_module(module):
931 lsst.utils.tests.init()
934class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
935 pass
938if __name__ == "__main__": 938 ↛ 939line 938 didn't jump to line 939, because the condition on line 938 was never true
939 lsst.utils.tests.init()
940 unittest.main()