Coverage for tests/test_subtractTask.py: 7%
605 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-08 01:36 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-08 01:36 -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
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_clear_template_mask(self):
72 noiseLevel = 1.
73 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
74 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
75 templateBorderSize=20, doApplyCalibration=True)
76 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"]
77 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
78 config.doSubtractBackground = False
79 config.mode = "convolveTemplate"
80 # Ensure that each each mask plane is set for some pixels
81 mask = template.mask
82 x0 = 50
83 x1 = 75
84 y0 = 150
85 y1 = 175
86 scienceMaskCheck = {}
87 for maskPlane in mask.getMaskPlaneDict().keys():
88 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0)
89 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane)
90 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
92 task = subtractImages.AlardLuptonSubtractTask(config=config)
93 output = task.run(template, science, sources)
94 # Verify that the template mask has been modified in place
95 for maskPlane in mask.getMaskPlaneDict().keys():
96 if maskPlane in diffimEmptyMaskPlanes:
97 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
98 elif maskPlane in config.preserveTemplateMask:
99 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
100 else:
101 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
102 # Mask planes set in the science image should also be set in the difference
103 # Except the "DETECTED" planes should have been cleared
104 diffimMask = output.difference.mask
105 for maskPlane, scienceSum in scienceMaskCheck.items():
106 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0)
107 if maskPlane in diffimEmptyMaskPlanes:
108 self.assertEqual(diffimSum, 0)
109 else:
110 self.assertTrue(diffimSum >= scienceSum)
112 def test_equal_images(self):
113 """Test that running with enough sources produces reasonable output,
114 with the same size psf in the template and science.
115 """
116 noiseLevel = 1.
117 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
118 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
119 templateBorderSize=20, doApplyCalibration=True)
120 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
121 config.doSubtractBackground = False
122 task = subtractImages.AlardLuptonSubtractTask(config=config)
123 output = task.run(template, science, sources)
124 # There shoud be no NaN values in the difference image
125 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
126 # Mean of difference image should be close to zero.
127 meanError = noiseLevel/np.sqrt(output.difference.image.array.size)
128 # Make sure to include pixels with the DETECTED mask bit set.
129 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA", "DETECTED", "DETECTED_NEGATIVE"))
130 differenceMean = computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl)
131 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError)
132 # stddev of difference image should be close to expected value.
133 differenceStd = computeRobustStatistics(output.difference.image, output.difference.mask,
134 makeStats(), statistic=afwMath.STDEV)
135 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1)
137 def test_psf_size(self):
138 """Test that the image subtract task runs without failing, if
139 fwhmExposureBuffer and fwhmExposureGrid parameters are set.
140 """
141 noiseLevel = 1.
142 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
143 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
144 templateBorderSize=20, doApplyCalibration=True)
146 schema = afwTable.ExposureTable.makeMinimalSchema()
147 weightKey = schema.addField("weight", type="D", doc="Coadd weight")
148 exposureCatalog = afwTable.ExposureCatalog(schema)
149 kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
150 psf = measAlg.KernelPsf(kernel, template.getBBox().getCenter())
152 record = exposureCatalog.addNew()
153 record.setPsf(psf)
154 record.setWcs(template.wcs)
155 record.setD(weightKey, 1.0)
156 record.setBBox(template.getBBox())
158 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs)
159 template.setPsf(customPsf)
161 # Test that we get an exception if we simply get the FWHM at center.
162 with self.assertRaises(InvalidParameterError):
163 getPsfFwhm(template.psf, True)
165 with self.assertRaises(InvalidParameterError):
166 getPsfFwhm(template.psf, False)
168 # Test that evaluateMeanPsfFwhm runs successfully on the template.
169 evaluateMeanPsfFwhm(template, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
171 # Since the PSF is spatially invariant, the FWHM should be the same at
172 # all points in the science image.
173 fwhm1 = getPsfFwhm(science.psf, False)
174 fwhm2 = evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
175 self.assertAlmostEqual(fwhm1[0], fwhm2, places=13)
176 self.assertAlmostEqual(fwhm1[1], fwhm2, places=13)
178 self.assertAlmostEqual(evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05,
179 fwhmExposureGrid=10),
180 getPsfFwhm(science.psf, True), places=7
181 )
183 # Test that the image subtraction task runs successfully.
184 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
185 config.doSubtractBackground = False
186 task = subtractImages.AlardLuptonSubtractTask(config=config)
188 # Test that the task runs if we take the mean FWHM on a grid.
189 with self.assertLogs(level="INFO") as cm:
190 task.run(template, science, sources)
192 # Check that evaluateMeanPsfFwhm was called.
193 # This tests that getPsfFwhm failed raising InvalidParameterError,
194 # that is caught and handled appropriately.
195 logMessage = ("INFO:lsst.alardLuptonSubtract:Unable to evaluate PSF at the average position. "
196 "Evaluting PSF on a grid of points."
197 )
198 self.assertIn(logMessage, cm.output)
200 def test_auto_convolveTemplate(self):
201 """Test that auto mode gives the same result as convolveTemplate when
202 the template psf is the smaller.
203 """
204 noiseLevel = 1.
205 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
206 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
207 templateBorderSize=20, doApplyCalibration=True)
208 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
209 config.doSubtractBackground = False
210 config.mode = "convolveTemplate"
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"
232 task = subtractImages.AlardLuptonSubtractTask(config=config)
233 output = task.run(template.clone(), science.clone(), sources)
235 config.mode = "auto"
236 task = subtractImages.AlardLuptonSubtractTask(config=config)
237 outputAuto = task.run(template, science, sources)
238 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
240 def test_science_better(self):
241 """Test that running with enough sources produces reasonable output,
242 with the science psf being smaller than the template.
243 """
244 statsCtrl = makeStats()
245 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
247 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
248 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
249 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
250 templateBorderSize=20, doApplyCalibration=True)
251 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
252 config.doSubtractBackground = False
253 config.mode = "convolveScience"
254 task = subtractImages.AlardLuptonSubtractTask(config=config)
255 output = task.run(template, science, sources)
256 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
257 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
258 # Mean of difference image should be close to zero.
259 nGoodPix = np.sum(np.isfinite(output.difference.image.array))
260 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix)
261 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
262 statsCtrlDetect)
264 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
265 # stddev of difference image should be close to expected value.
266 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
267 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
268 statsCtrl)
269 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
270 statsCtrl, statistic=afwMath.STDEV)
271 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
272 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
274 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
275 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
276 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
278 def test_template_better(self):
279 """Test that running with enough sources produces reasonable output,
280 with the template psf being smaller than the science.
281 """
282 statsCtrl = makeStats()
283 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
285 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
286 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
287 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
288 templateBorderSize=20, doApplyCalibration=True)
289 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
290 config.doSubtractBackground = False
291 task = subtractImages.AlardLuptonSubtractTask(config=config)
292 output = task.run(template, science, sources)
293 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
294 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
295 # There should be no NaNs in the image if we convolve the template with a buffer
296 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
297 # Mean of difference image should be close to zero.
298 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size)
300 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
301 statsCtrlDetect)
302 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
303 # stddev of difference image should be close to expected value.
304 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
305 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
306 statsCtrl)
307 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
308 statsCtrl, statistic=afwMath.STDEV)
309 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
310 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
312 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
313 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
314 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
316 def test_symmetry(self):
317 """Test that convolving the science and convolving the template are
318 symmetric: if the psfs are switched between them, the difference image
319 should be nearly the same.
320 """
321 noiseLevel = 1.
322 # Don't include a border for the template, in order to make the results
323 # comparable when we swap which image is treated as the "science" image.
324 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
325 noiseSeed=6, templateBorderSize=0, doApplyCalibration=True)
326 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
327 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True)
328 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
329 config.mode = 'auto'
330 config.doSubtractBackground = False
331 task = subtractImages.AlardLuptonSubtractTask(config=config)
333 # The science image will be modified in place, so use a copy for the second run.
334 science_better = task.run(template.clone(), science.clone(), sources)
335 template_better = task.run(science, template, sources)
337 delta = template_better.difference.clone()
338 delta.image -= science_better.difference.image
339 delta.variance -= science_better.difference.variance
340 delta.mask.array -= science_better.difference.mask.array
342 statsCtrl = makeStats()
343 # Mean of delta should be very close to zero.
344 nGoodPix = np.sum(np.isfinite(delta.image.array))
345 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
346 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
347 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV)
348 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
349 # stddev of difference image should be close to expected value
350 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1)
352 def test_few_sources(self):
353 """Test with only 1 source, to check that we get a useful error.
354 """
355 xSize = 256
356 ySize = 256
357 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
358 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
359 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
360 task = subtractImages.AlardLuptonSubtractTask(config=config)
361 sources = sources[0:1]
362 with self.assertRaisesRegex(RuntimeError,
363 "Cannot compute PSF matching kernel: too few sources selected."):
364 task.run(template, science, sources)
366 def test_order_equal_images(self):
367 """Verify that the result is the same regardless of convolution mode
368 if the images are equivalent.
369 """
370 noiseLevel = .1
371 seed1 = 6
372 seed2 = 7
373 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1,
374 clearEdgeMask=True)
375 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
376 templateBorderSize=0, doApplyCalibration=True,
377 clearEdgeMask=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 clearEdgeMask=True)
386 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
387 templateBorderSize=0, doApplyCalibration=True,
388 clearEdgeMask=True)
389 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
390 config2.mode = "convolveScience"
391 config2.doSubtractBackground = False
392 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
393 results_convolveScience = task2.run(template2, science2, sources2)
394 bbox = results_convolveTemplate.difference.getBBox().clippedTo(
395 results_convolveScience.difference.getBBox())
396 diff1 = science1.maskedImage.clone()[bbox]
397 diff1 -= template1.maskedImage[bbox]
398 diff2 = science2.maskedImage.clone()[bbox]
399 diff2 -= template2.maskedImage[bbox]
400 self.assertFloatsAlmostEqual(results_convolveTemplate.difference[bbox].image.array,
401 diff1.image.array,
402 atol=noiseLevel*5.)
403 self.assertFloatsAlmostEqual(results_convolveScience.difference[bbox].image.array,
404 diff2.image.array,
405 atol=noiseLevel*5.)
406 diffErr = noiseLevel*2
407 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference[bbox].maskedImage,
408 results_convolveScience.difference[bbox].maskedImage,
409 atol=diffErr*5.)
411 def test_background_subtraction(self):
412 """Check that we can recover the background,
413 and that it is subtracted correctly in the difference image.
414 """
415 noiseLevel = 1.
416 xSize = 512
417 ySize = 512
418 x0 = 123
419 y0 = 456
420 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
421 templateBorderSize=20,
422 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
423 doApplyCalibration=True)
424 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
426 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
427 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
428 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
429 background=background_model,
430 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
431 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
432 config.doSubtractBackground = True
434 config.makeKernel.kernel.name = "AL"
435 config.makeKernel.kernel.active.fitForBackground = True
436 config.makeKernel.kernel.active.spatialKernelOrder = 1
437 config.makeKernel.kernel.active.spatialBgOrder = 2
438 statsCtrl = makeStats()
440 def _run_and_check_images(config, statsCtrl, mode):
441 """Check that the fit background matches the input model.
442 """
443 config.mode = mode
444 task = subtractImages.AlardLuptonSubtractTask(config=config)
445 output = task.run(template.clone(), science.clone(), sources)
447 # We should be fitting the same number of parameters as were in the input model
448 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
450 # The parameters of the background fit should be close to the input model
451 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
452 np.array(params), rtol=0.3)
454 # stddev of difference image should be close to expected value.
455 # This will fail if we have mis-subtracted the background.
456 stdVal = computeRobustStatistics(output.difference.image, output.difference.mask,
457 statsCtrl, statistic=afwMath.STDEV)
458 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
460 _run_and_check_images(config, statsCtrl, "convolveTemplate")
461 _run_and_check_images(config, statsCtrl, "convolveScience")
463 def test_scale_variance_convolve_template(self):
464 """Check variance scaling of the image difference.
465 """
466 scienceNoiseLevel = 4.
467 templateNoiseLevel = 2.
468 scaleFactor = 1.345
469 # Make sure to include pixels with the DETECTED mask bit set.
470 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
472 def _run_and_check_images(science, template, sources, statsCtrl,
473 doDecorrelation, doScaleVariance, scaleFactor=1.):
474 """Check that the variance plane matches the expected value for
475 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
476 """
478 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
479 config.doSubtractBackground = False
480 config.doDecorrelation = doDecorrelation
481 config.doScaleVariance = doScaleVariance
482 task = subtractImages.AlardLuptonSubtractTask(config=config)
483 output = task.run(template.clone(), science.clone(), sources)
484 if doScaleVariance:
485 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
486 scaleFactor, atol=0.05)
487 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
488 scaleFactor, atol=0.05)
490 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
491 if doDecorrelation:
492 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
493 else:
494 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
495 output.matchedTemplate.mask,
496 statsCtrl)
498 if doScaleVariance:
499 templateNoise *= scaleFactor
500 scienceNoise *= scaleFactor
501 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
502 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
504 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
505 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
506 templateBorderSize=20, doApplyCalibration=True)
507 # Verify that the variance plane of the difference image is correct
508 # when the template and science variance planes are correct
509 _run_and_check_images(science, template, sources, statsCtrl,
510 doDecorrelation=True, doScaleVariance=True)
511 _run_and_check_images(science, template, sources, statsCtrl,
512 doDecorrelation=True, doScaleVariance=False)
513 _run_and_check_images(science, template, sources, statsCtrl,
514 doDecorrelation=False, doScaleVariance=True)
515 _run_and_check_images(science, template, sources, statsCtrl,
516 doDecorrelation=False, doScaleVariance=False)
518 # Verify that the variance plane of the difference image is correct
519 # when the template variance plane is incorrect
520 template.variance.array /= scaleFactor
521 science.variance.array /= scaleFactor
522 _run_and_check_images(science, template, sources, statsCtrl,
523 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
524 _run_and_check_images(science, template, sources, statsCtrl,
525 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
526 _run_and_check_images(science, template, sources, statsCtrl,
527 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
528 _run_and_check_images(science, template, sources, statsCtrl,
529 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
531 def test_scale_variance_convolve_science(self):
532 """Check variance scaling of the image difference.
533 """
534 scienceNoiseLevel = 4.
535 templateNoiseLevel = 2.
536 scaleFactor = 1.345
537 # Make sure to include pixels with the DETECTED mask bit set.
538 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
540 def _run_and_check_images(science, template, sources, statsCtrl,
541 doDecorrelation, doScaleVariance, scaleFactor=1.):
542 """Check that the variance plane matches the expected value for
543 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
544 """
546 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
547 config.mode = "convolveScience"
548 config.doSubtractBackground = False
549 config.doDecorrelation = doDecorrelation
550 config.doScaleVariance = doScaleVariance
551 task = subtractImages.AlardLuptonSubtractTask(config=config)
552 output = task.run(template.clone(), science.clone(), sources)
553 if doScaleVariance:
554 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
555 scaleFactor, atol=0.05)
556 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
557 scaleFactor, atol=0.05)
559 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
560 if doDecorrelation:
561 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
562 else:
563 scienceNoise = computeRobustStatistics(output.matchedScience.variance,
564 output.matchedScience.mask,
565 statsCtrl)
567 if doScaleVariance:
568 templateNoise *= scaleFactor
569 scienceNoise *= scaleFactor
571 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
572 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
574 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
575 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
576 templateBorderSize=20, doApplyCalibration=True)
577 # Verify that the variance plane of the difference image is correct
578 # when the template and science variance planes are correct
579 _run_and_check_images(science, template, sources, statsCtrl,
580 doDecorrelation=True, doScaleVariance=True)
581 _run_and_check_images(science, template, sources, statsCtrl,
582 doDecorrelation=True, doScaleVariance=False)
583 _run_and_check_images(science, template, sources, statsCtrl,
584 doDecorrelation=False, doScaleVariance=True)
585 _run_and_check_images(science, template, sources, statsCtrl,
586 doDecorrelation=False, doScaleVariance=False)
588 # Verify that the variance plane of the difference image is correct
589 # when the template and science variance planes are incorrect
590 science.variance.array /= scaleFactor
591 template.variance.array /= scaleFactor
592 _run_and_check_images(science, template, sources, statsCtrl,
593 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
594 _run_and_check_images(science, template, sources, statsCtrl,
595 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
596 _run_and_check_images(science, template, sources, statsCtrl,
597 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
598 _run_and_check_images(science, template, sources, statsCtrl,
599 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
601 def test_exposure_properties_convolve_template(self):
602 """Check that all necessary exposure metadata is included
603 when the template is convolved.
604 """
605 noiseLevel = 1.
606 seed = 37
607 rng = np.random.RandomState(seed)
608 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
609 psf = science.psf
610 psfAvgPos = psf.getAveragePosition()
611 psfSize = getPsfFwhm(science.psf)
612 psfImg = psf.computeKernelImage(psfAvgPos)
613 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
614 templateBorderSize=20, doApplyCalibration=True)
616 # Generate a random aperture correction map
617 apCorrMap = lsst.afw.image.ApCorrMap()
618 for name in ("a", "b", "c"):
619 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
620 science.info.setApCorrMap(apCorrMap)
622 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
623 config.mode = "convolveTemplate"
625 def _run_and_check_images(doDecorrelation):
626 """Check that the metadata is correct with or without decorrelation.
627 """
628 config.doDecorrelation = doDecorrelation
629 task = subtractImages.AlardLuptonSubtractTask(config=config)
630 output = task.run(template.clone(), science.clone(), sources)
631 psfOut = output.difference.psf
632 psfAvgPos = psfOut.getAveragePosition()
633 if doDecorrelation:
634 # Decorrelation requires recalculating the PSF,
635 # so it will not be the same as the input
636 psfOutSize = getPsfFwhm(science.psf)
637 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
638 else:
639 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
640 self.assertImagesAlmostEqual(psfImg, psfOutImg)
642 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
643 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
644 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
645 self.assertEqual(science.filter, output.difference.filter)
646 self.assertEqual(science.photoCalib, output.difference.photoCalib)
647 _run_and_check_images(doDecorrelation=True)
648 _run_and_check_images(doDecorrelation=False)
650 def test_exposure_properties_convolve_science(self):
651 """Check that all necessary exposure metadata is included
652 when the science image is convolved.
653 """
654 noiseLevel = 1.
655 seed = 37
656 rng = np.random.RandomState(seed)
657 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
658 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
659 templateBorderSize=20, doApplyCalibration=True)
660 psf = template.psf
661 psfAvgPos = psf.getAveragePosition()
662 psfSize = getPsfFwhm(template.psf)
663 psfImg = psf.computeKernelImage(psfAvgPos)
665 # Generate a random aperture correction map
666 apCorrMap = lsst.afw.image.ApCorrMap()
667 for name in ("a", "b", "c"):
668 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
669 science.info.setApCorrMap(apCorrMap)
671 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
672 config.mode = "convolveScience"
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)
720class AlardLuptonPreconvolveSubtractTest(lsst.utils.tests.TestCase):
722 def test_mismatched_template(self):
723 """Test that an error is raised if the template
724 does not fully contain the science image.
725 """
726 xSize = 200
727 ySize = 200
728 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
729 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
730 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
731 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
732 with self.assertRaises(AssertionError):
733 task.run(template, science, sources)
735 def test_equal_images(self):
736 """Test that running with enough sources produces reasonable output,
737 with the same size psf in the template and science.
738 """
739 noiseLevel = 1.
740 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
741 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
742 templateBorderSize=20, doApplyCalibration=True)
743 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
744 config.doSubtractBackground = False
745 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
746 output = task.run(template, science, sources)
747 # There shoud be no NaN values in the Score image
748 self.assertTrue(np.all(np.isfinite(output.scoreExposure.image.array)))
749 # Mean of Score image should be close to zero.
750 meanError = noiseLevel/np.sqrt(output.scoreExposure.image.array.size)
751 # Make sure to include pixels with the DETECTED mask bit set.
752 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
753 scoreMean = computeRobustStatistics(output.scoreExposure.image,
754 output.scoreExposure.mask,
755 statsCtrl)
756 self.assertFloatsAlmostEqual(scoreMean, 0, atol=5*meanError)
757 nea = computePSFNoiseEquivalentArea(science.psf)
758 # stddev of Score image should be close to expected value.
759 scoreStd = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
760 statsCtrl=statsCtrl, statistic=afwMath.STDEV)
761 self.assertFloatsAlmostEqual(scoreStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
763 def test_clear_template_mask(self):
764 noiseLevel = 1.
765 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
766 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
767 templateBorderSize=20, doApplyCalibration=True)
768 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"]
769 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
770 config.doSubtractBackground = False # Ensure that each each mask plane is set for some pixels
771 mask = template.mask
772 x0 = 50
773 x1 = 75
774 y0 = 150
775 y1 = 175
776 scienceMaskCheck = {}
777 for maskPlane in mask.getMaskPlaneDict().keys():
778 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0)
779 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane)
780 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
782 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
783 output = task.run(template, science, sources)
784 # Verify that the template mask has been modified in place
785 for maskPlane in mask.getMaskPlaneDict().keys():
786 if maskPlane in diffimEmptyMaskPlanes:
787 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
788 elif maskPlane in config.preserveTemplateMask:
789 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
790 else:
791 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
792 # Mask planes set in the science image should also be set in the difference
793 # Except the "DETECTED" planes should have been cleared
794 diffimMask = output.scoreExposure.mask
795 for maskPlane, scienceSum in scienceMaskCheck.items():
796 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0)
797 if maskPlane in diffimEmptyMaskPlanes:
798 self.assertEqual(diffimSum, 0)
799 else:
800 self.assertTrue(diffimSum >= scienceSum)
802 def test_agnostic_template_psf(self):
803 """Test that the Score image is the same whether the template PSF is
804 larger or smaller than the science image PSF.
805 """
806 noiseLevel = .3
807 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel,
808 noiseSeed=6, templateBorderSize=0)
809 template1, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
810 noiseSeed=7, doApplyCalibration=True)
811 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
812 noiseSeed=8, doApplyCalibration=True)
813 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
814 config.doSubtractBackground = False
815 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
817 science_better = task.run(template1, science.clone(), sources)
818 template_better = task.run(template2, science, sources)
819 bbox = science_better.scoreExposure.getBBox().clippedTo(template_better.scoreExposure.getBBox())
821 delta = template_better.scoreExposure[bbox].clone()
822 delta.image -= science_better.scoreExposure[bbox].image
823 delta.variance -= science_better.scoreExposure[bbox].variance
824 delta.mask.array &= science_better.scoreExposure[bbox].mask.array
826 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
827 # Mean of delta should be very close to zero.
828 nGoodPix = np.sum(np.isfinite(delta.image.array))
829 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
830 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
831 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl,
832 statistic=afwMath.STDEV)
833 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
834 nea = computePSFNoiseEquivalentArea(science.psf)
835 # stddev of Score image should be close to expected value
836 self.assertFloatsAlmostEqual(deltaStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=.1)
838 def test_few_sources(self):
839 """Test with only 1 source, to check that we get a useful error.
840 """
841 xSize = 256
842 ySize = 256
843 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
844 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
845 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
846 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
847 sources = sources[0:1]
848 with self.assertRaisesRegex(RuntimeError,
849 "Cannot compute PSF matching kernel: too few sources selected."):
850 task.run(template, science, sources)
852 def test_background_subtraction(self):
853 """Check that we can recover the background,
854 and that it is subtracted correctly in the Score image.
855 """
856 noiseLevel = 1.
857 xSize = 512
858 ySize = 512
859 x0 = 123
860 y0 = 456
861 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
862 templateBorderSize=20,
863 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
864 doApplyCalibration=True)
865 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
867 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
868 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
869 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
870 background=background_model,
871 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
872 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
873 config.doSubtractBackground = True
875 config.makeKernel.kernel.name = "AL"
876 config.makeKernel.kernel.active.fitForBackground = True
877 config.makeKernel.kernel.active.spatialKernelOrder = 1
878 config.makeKernel.kernel.active.spatialBgOrder = 2
879 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
881 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
882 output = task.run(template.clone(), science.clone(), sources)
884 # We should be fitting the same number of parameters as were in the input model
885 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
887 # The parameters of the background fit should be close to the input model
888 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
889 np.array(params), rtol=0.2)
891 # stddev of Score image should be close to expected value.
892 # This will fail if we have mis-subtracted the background.
893 stdVal = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
894 statsCtrl, statistic=afwMath.STDEV)
895 # get the img psf Noise Equivalent Area value
896 nea = computePSFNoiseEquivalentArea(science.psf)
897 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
899 def test_scale_variance(self):
900 """Check variance scaling of the Score image.
901 """
902 scienceNoiseLevel = 4.
903 templateNoiseLevel = 2.
904 scaleFactor = 1.345
905 # Make sure to include pixels with the DETECTED mask bit set.
906 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
908 def _run_and_check_images(science, template, sources, statsCtrl,
909 doDecorrelation, doScaleVariance, scaleFactor=1.):
910 """Check that the variance plane matches the expected value for
911 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
912 """
914 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
915 config.doSubtractBackground = False
916 config.doDecorrelation = doDecorrelation
917 config.doScaleVariance = doScaleVariance
918 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
919 output = task.run(template.clone(), science.clone(), sources)
920 if doScaleVariance:
921 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
922 scaleFactor, atol=0.05)
923 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
924 scaleFactor, atol=0.05)
926 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
927 # get the img psf Noise Equivalent Area value
928 nea = computePSFNoiseEquivalentArea(science.psf)
929 scienceNoise /= nea
930 if doDecorrelation:
931 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
932 templateNoise /= nea
933 else:
934 # Don't divide by NEA in this case, since the template is convolved
935 # and in the same units as the Score exposure.
936 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
937 output.matchedTemplate.mask,
938 statsCtrl)
939 if doScaleVariance:
940 templateNoise *= scaleFactor
941 scienceNoise *= scaleFactor
942 varMean = computeRobustStatistics(output.scoreExposure.variance,
943 output.scoreExposure.mask,
944 statsCtrl)
945 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
947 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
948 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
949 templateBorderSize=20, doApplyCalibration=True)
950 # Verify that the variance plane of the Score image is correct
951 # when the template and science variance planes are correct
952 _run_and_check_images(science, template, sources, statsCtrl,
953 doDecorrelation=True, doScaleVariance=True)
954 _run_and_check_images(science, template, sources, statsCtrl,
955 doDecorrelation=True, doScaleVariance=False)
956 _run_and_check_images(science, template, sources, statsCtrl,
957 doDecorrelation=False, doScaleVariance=True)
958 _run_and_check_images(science, template, sources, statsCtrl,
959 doDecorrelation=False, doScaleVariance=False)
961 # Verify that the variance plane of the Score image is correct
962 # when the template variance plane is incorrect
963 template.variance.array /= scaleFactor
964 science.variance.array /= scaleFactor
965 _run_and_check_images(science, template, sources, statsCtrl,
966 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
967 _run_and_check_images(science, template, sources, statsCtrl,
968 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
969 _run_and_check_images(science, template, sources, statsCtrl,
970 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
971 _run_and_check_images(science, template, sources, statsCtrl,
972 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
974 def test_exposure_properties(self):
975 """Check that all necessary exposure metadata is included
976 with the Score image.
977 """
978 noiseLevel = 1.
979 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
980 psf = science.psf
981 psfAvgPos = psf.getAveragePosition()
982 psfSize = getPsfFwhm(science.psf)
983 psfImg = psf.computeKernelImage(psfAvgPos)
984 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
985 templateBorderSize=20, doApplyCalibration=True)
987 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
989 def _run_and_check_images(doDecorrelation):
990 """Check that the metadata is correct with or without decorrelation.
991 """
992 config.doDecorrelation = doDecorrelation
993 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
994 output = task.run(template.clone(), science.clone(), sources)
995 psfOut = output.scoreExposure.psf
996 psfAvgPos = psfOut.getAveragePosition()
997 if doDecorrelation:
998 # Decorrelation requires recalculating the PSF,
999 # so it will not be the same as the input
1000 psfOutSize = getPsfFwhm(science.psf)
1001 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
1002 else:
1003 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
1004 self.assertImagesAlmostEqual(psfImg, psfOutImg)
1006 # check PSF, WCS, bbox, filterLabel, photoCalib
1007 self.assertWcsAlmostEqualOverBBox(science.wcs, output.scoreExposure.wcs, science.getBBox())
1008 self.assertEqual(science.filter, output.scoreExposure.filter)
1009 self.assertEqual(science.photoCalib, output.scoreExposure.photoCalib)
1010 _run_and_check_images(doDecorrelation=True)
1011 _run_and_check_images(doDecorrelation=False)
1014def setup_module(module):
1015 lsst.utils.tests.init()
1018class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
1019 pass
1022if __name__ == "__main__": 1022 ↛ 1023line 1022 didn't jump to line 1023, because the condition on line 1022 was never true
1023 lsst.utils.tests.init()
1024 unittest.main()