Coverage for tests/test_subtractTask.py: 7%
614 statements
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-26 02:50 -0700
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-26 02:50 -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.meas.algorithms as measAlg
28from lsst.ip.diffim import subtractImages
29from lsst.pex.config import FieldValidationError
30import lsst.utils.tests
31import numpy as np
32from lsst.ip.diffim.utils import (computeRobustStatistics, computePSFNoiseEquivalentArea,
33 evaluateMeanPsfFwhm, getPsfFwhm, makeStats, makeTestImage)
34from lsst.pex.exceptions import InvalidParameterError
37class CustomCoaddPsf(measAlg.CoaddPsf):
38 """A custom CoaddPSF that overrides the getAveragePosition method.
39 """
40 def getAveragePosition(self):
41 return lsst.geom.Point2D(-10000, -10000)
44class AlardLuptonSubtractTest(lsst.utils.tests.TestCase):
46 def test_allowed_config_modes(self):
47 """Verify the allowable modes for convolution.
48 """
49 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
50 config.mode = 'auto'
51 config.mode = 'convolveScience'
52 config.mode = 'convolveTemplate'
54 with self.assertRaises(FieldValidationError):
55 config.mode = 'aotu'
57 def test_mismatched_template(self):
58 """Test that an error is raised if the template
59 does not fully contain the science image.
60 """
61 xSize = 200
62 ySize = 200
63 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
64 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
65 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
66 task = subtractImages.AlardLuptonSubtractTask(config=config)
67 with self.assertRaises(AssertionError):
68 task.run(template, science, sources)
70 def test_clear_template_mask(self):
71 noiseLevel = 1.
72 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
73 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
74 templateBorderSize=20, doApplyCalibration=True)
75 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"]
76 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
77 config.doSubtractBackground = False
78 config.mode = "convolveTemplate"
79 # Ensure that each each mask plane is set for some pixels
80 mask = template.mask
81 x0 = 50
82 x1 = 75
83 y0 = 150
84 y1 = 175
85 scienceMaskCheck = {}
86 for maskPlane in mask.getMaskPlaneDict().keys():
87 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0)
88 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane)
89 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
91 task = subtractImages.AlardLuptonSubtractTask(config=config)
92 output = task.run(template, science, sources)
93 # Verify that the template mask has been modified in place
94 for maskPlane in mask.getMaskPlaneDict().keys():
95 if maskPlane in diffimEmptyMaskPlanes:
96 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
97 elif maskPlane in config.preserveTemplateMask:
98 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
99 else:
100 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
101 # Mask planes set in the science image should also be set in the difference
102 # Except the "DETECTED" planes should have been cleared
103 diffimMask = output.difference.mask
104 for maskPlane, scienceSum in scienceMaskCheck.items():
105 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0)
106 if maskPlane in diffimEmptyMaskPlanes:
107 self.assertEqual(diffimSum, 0)
108 else:
109 self.assertTrue(diffimSum >= scienceSum)
111 def test_equal_images(self):
112 """Test that running with enough sources produces reasonable output,
113 with the same size psf in the template and science.
114 """
115 noiseLevel = 1.
116 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
117 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
118 templateBorderSize=20, doApplyCalibration=True)
119 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
120 config.doSubtractBackground = False
121 task = subtractImages.AlardLuptonSubtractTask(config=config)
122 output = task.run(template, science, sources)
123 # There shoud be no NaN values in the difference image
124 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
125 # Mean of difference image should be close to zero.
126 meanError = noiseLevel/np.sqrt(output.difference.image.array.size)
127 # Make sure to include pixels with the DETECTED mask bit set.
128 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA", "DETECTED", "DETECTED_NEGATIVE"))
129 differenceMean = computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl)
130 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError)
131 # stddev of difference image should be close to expected value.
132 differenceStd = computeRobustStatistics(output.difference.image, output.difference.mask,
133 makeStats(), statistic=afwMath.STDEV)
134 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1)
136 def test_psf_size(self):
137 """Test that the image subtract task runs without failing, if
138 fwhmExposureBuffer and fwhmExposureGrid parameters are set.
139 """
140 noiseLevel = 1.
141 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
142 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
143 templateBorderSize=20, doApplyCalibration=True)
145 schema = afwTable.ExposureTable.makeMinimalSchema()
146 weightKey = schema.addField("weight", type="D", doc="Coadd weight")
147 exposureCatalog = afwTable.ExposureCatalog(schema)
148 kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
149 psf = measAlg.KernelPsf(kernel, template.getBBox().getCenter())
151 record = exposureCatalog.addNew()
152 record.setPsf(psf)
153 record.setWcs(template.wcs)
154 record.setD(weightKey, 1.0)
155 record.setBBox(template.getBBox())
157 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs)
158 template.setPsf(customPsf)
160 # Test that we get an exception if we simply get the FWHM at center.
161 with self.assertRaises(InvalidParameterError):
162 getPsfFwhm(template.psf, True)
164 with self.assertRaises(InvalidParameterError):
165 getPsfFwhm(template.psf, False)
167 # Test that evaluateMeanPsfFwhm runs successfully on the template.
168 evaluateMeanPsfFwhm(template, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
170 # Since the PSF is spatially invariant, the FWHM should be the same at
171 # all points in the science image.
172 fwhm1 = getPsfFwhm(science.psf, False)
173 fwhm2 = evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05, fwhmExposureGrid=10)
174 self.assertAlmostEqual(fwhm1[0], fwhm2, places=13)
175 self.assertAlmostEqual(fwhm1[1], fwhm2, places=13)
177 self.assertAlmostEqual(evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05,
178 fwhmExposureGrid=10),
179 getPsfFwhm(science.psf, True), places=7
180 )
182 # Test that the image subtraction task runs successfully.
183 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
184 config.doSubtractBackground = False
185 task = subtractImages.AlardLuptonSubtractTask(config=config)
187 # Test that the task runs if we take the mean FWHM on a grid.
188 with self.assertLogs(level="INFO") as cm:
189 task.run(template, science, sources)
191 # Check that evaluateMeanPsfFwhm was called.
192 # This tests that getPsfFwhm failed raising InvalidParameterError,
193 # that is caught and handled appropriately.
194 logMessage = ("INFO:lsst.alardLuptonSubtract:Unable to evaluate PSF at the average position. "
195 "Evaluting PSF on a grid of points."
196 )
197 self.assertIn(logMessage, cm.output)
199 def test_auto_convolveTemplate(self):
200 """Test that auto mode gives the same result as convolveTemplate when
201 the template psf is the smaller.
202 """
203 noiseLevel = 1.
204 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
205 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
206 templateBorderSize=20, doApplyCalibration=True)
207 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
208 config.doSubtractBackground = False
209 config.mode = "convolveTemplate"
211 task = subtractImages.AlardLuptonSubtractTask(config=config)
212 output = task.run(template.clone(), science.clone(), sources)
214 config.mode = "auto"
215 task = subtractImages.AlardLuptonSubtractTask(config=config)
216 outputAuto = task.run(template, science, sources)
217 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
219 def test_auto_convolveScience(self):
220 """Test that auto mode gives the same result as convolveScience when
221 the science psf is the smaller.
222 """
223 noiseLevel = 1.
224 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
225 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
226 templateBorderSize=20, doApplyCalibration=True)
227 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
228 config.doSubtractBackground = False
229 config.mode = "convolveScience"
231 task = subtractImages.AlardLuptonSubtractTask(config=config)
232 output = task.run(template.clone(), science.clone(), sources)
234 config.mode = "auto"
235 task = subtractImages.AlardLuptonSubtractTask(config=config)
236 outputAuto = task.run(template, science, sources)
237 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage)
239 def test_science_better(self):
240 """Test that running with enough sources produces reasonable output,
241 with the science psf being smaller than the template.
242 """
243 statsCtrl = makeStats()
244 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
246 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
247 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
248 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
249 templateBorderSize=20, doApplyCalibration=True)
250 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
251 config.doSubtractBackground = False
252 config.mode = "convolveScience"
253 task = subtractImages.AlardLuptonSubtractTask(config=config)
254 output = task.run(template, science, sources)
255 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
256 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
257 # Mean of difference image should be close to zero.
258 nGoodPix = np.sum(np.isfinite(output.difference.image.array))
259 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix)
260 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
261 statsCtrlDetect)
263 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
264 # stddev of difference image should be close to expected value.
265 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
266 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
267 statsCtrl)
268 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
269 statsCtrl, statistic=afwMath.STDEV)
270 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
271 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
273 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
274 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
275 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
277 def test_template_better(self):
278 """Test that running with enough sources produces reasonable output,
279 with the template psf being smaller than the science.
280 """
281 statsCtrl = makeStats()
282 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
284 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel):
285 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
286 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
287 templateBorderSize=20, doApplyCalibration=True)
288 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
289 config.doSubtractBackground = False
290 task = subtractImages.AlardLuptonSubtractTask(config=config)
291 output = task.run(template, science, sources)
292 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05)
293 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05)
294 # There should be no NaNs in the image if we convolve the template with a buffer
295 self.assertTrue(np.all(np.isfinite(output.difference.image.array)))
296 # Mean of difference image should be close to zero.
297 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size)
299 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask,
300 statsCtrlDetect)
301 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError)
302 # stddev of difference image should be close to expected value.
303 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2)
304 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask,
305 statsCtrl)
306 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask,
307 statsCtrl, statistic=afwMath.STDEV)
308 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1)
309 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1)
311 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.)
312 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1)
313 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1)
315 def test_symmetry(self):
316 """Test that convolving the science and convolving the template are
317 symmetric: if the psfs are switched between them, the difference image
318 should be nearly the same.
319 """
320 noiseLevel = 1.
321 # Don't include a border for the template, in order to make the results
322 # comparable when we swap which image is treated as the "science" image.
323 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
324 noiseSeed=6, templateBorderSize=0, doApplyCalibration=True)
325 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
326 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True)
327 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
328 config.mode = 'auto'
329 config.doSubtractBackground = False
330 task = subtractImages.AlardLuptonSubtractTask(config=config)
332 # The science image will be modified in place, so use a copy for the second run.
333 science_better = task.run(template.clone(), science.clone(), sources)
334 template_better = task.run(science, template, sources)
336 delta = template_better.difference.clone()
337 delta.image -= science_better.difference.image
338 delta.variance -= science_better.difference.variance
339 delta.mask.array -= science_better.difference.mask.array
341 statsCtrl = makeStats()
342 # Mean of delta should be very close to zero.
343 nGoodPix = np.sum(np.isfinite(delta.image.array))
344 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
345 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
346 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV)
347 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
348 # stddev of difference image should be close to expected value
349 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1)
351 def test_few_sources(self):
352 """Test with only 1 source, to check that we get a useful error.
353 """
354 xSize = 256
355 ySize = 256
356 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
357 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
358 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
359 task = subtractImages.AlardLuptonSubtractTask(config=config)
360 sources = sources[0:1]
361 with self.assertRaisesRegex(RuntimeError,
362 "Cannot compute PSF matching kernel: too few sources selected."):
363 task.run(template, science, sources)
365 def test_order_equal_images(self):
366 """Verify that the result is the same regardless of convolution mode
367 if the images are equivalent.
368 """
369 noiseLevel = .1
370 seed1 = 6
371 seed2 = 7
372 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1,
373 clearEdgeMask=True)
374 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
375 templateBorderSize=0, doApplyCalibration=True,
376 clearEdgeMask=True)
377 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
378 config1.mode = "convolveTemplate"
379 config1.doSubtractBackground = False
380 task1 = subtractImages.AlardLuptonSubtractTask(config=config1)
381 results_convolveTemplate = task1.run(template1, science1, sources1)
383 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1,
384 clearEdgeMask=True)
385 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2,
386 templateBorderSize=0, doApplyCalibration=True,
387 clearEdgeMask=True)
388 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass()
389 config2.mode = "convolveScience"
390 config2.doSubtractBackground = False
391 task2 = subtractImages.AlardLuptonSubtractTask(config=config2)
392 results_convolveScience = task2.run(template2, science2, sources2)
393 bbox = results_convolveTemplate.difference.getBBox().clippedTo(
394 results_convolveScience.difference.getBBox())
395 diff1 = science1.maskedImage.clone()[bbox]
396 diff1 -= template1.maskedImage[bbox]
397 diff2 = science2.maskedImage.clone()[bbox]
398 diff2 -= template2.maskedImage[bbox]
399 self.assertFloatsAlmostEqual(results_convolveTemplate.difference[bbox].image.array,
400 diff1.image.array,
401 atol=noiseLevel*5.)
402 self.assertFloatsAlmostEqual(results_convolveScience.difference[bbox].image.array,
403 diff2.image.array,
404 atol=noiseLevel*5.)
405 diffErr = noiseLevel*2
406 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference[bbox].maskedImage,
407 results_convolveScience.difference[bbox].maskedImage,
408 atol=diffErr*5.)
410 def test_background_subtraction(self):
411 """Check that we can recover the background,
412 and that it is subtracted correctly in the difference image.
413 """
414 noiseLevel = 1.
415 xSize = 512
416 ySize = 512
417 x0 = 123
418 y0 = 456
419 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
420 templateBorderSize=20,
421 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
422 doApplyCalibration=True)
423 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
425 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
426 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
427 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
428 background=background_model,
429 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
430 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
431 config.doSubtractBackground = True
433 config.makeKernel.kernel.name = "AL"
434 config.makeKernel.kernel.active.fitForBackground = True
435 config.makeKernel.kernel.active.spatialKernelOrder = 1
436 config.makeKernel.kernel.active.spatialBgOrder = 2
437 statsCtrl = makeStats()
439 def _run_and_check_images(config, statsCtrl, mode):
440 """Check that the fit background matches the input model.
441 """
442 config.mode = mode
443 task = subtractImages.AlardLuptonSubtractTask(config=config)
444 output = task.run(template.clone(), science.clone(), sources)
446 # We should be fitting the same number of parameters as were in the input model
447 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
449 # The parameters of the background fit should be close to the input model
450 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
451 np.array(params), rtol=0.3)
453 # stddev of difference image should be close to expected value.
454 # This will fail if we have mis-subtracted the background.
455 stdVal = computeRobustStatistics(output.difference.image, output.difference.mask,
456 statsCtrl, statistic=afwMath.STDEV)
457 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1)
459 _run_and_check_images(config, statsCtrl, "convolveTemplate")
460 _run_and_check_images(config, statsCtrl, "convolveScience")
462 def test_scale_variance_convolve_template(self):
463 """Check variance scaling of the image difference.
464 """
465 scienceNoiseLevel = 4.
466 templateNoiseLevel = 2.
467 scaleFactor = 1.345
468 # Make sure to include pixels with the DETECTED mask bit set.
469 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
471 def _run_and_check_images(science, template, sources, statsCtrl,
472 doDecorrelation, doScaleVariance, scaleFactor=1.):
473 """Check that the variance plane matches the expected value for
474 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
475 """
477 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
478 config.doSubtractBackground = False
479 config.doDecorrelation = doDecorrelation
480 config.doScaleVariance = doScaleVariance
481 task = subtractImages.AlardLuptonSubtractTask(config=config)
482 output = task.run(template.clone(), science.clone(), sources)
483 if doScaleVariance:
484 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
485 scaleFactor, atol=0.05)
486 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
487 scaleFactor, atol=0.05)
489 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
490 if doDecorrelation:
491 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
492 else:
493 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
494 output.matchedTemplate.mask,
495 statsCtrl)
497 if doScaleVariance:
498 templateNoise *= scaleFactor
499 scienceNoise *= scaleFactor
500 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
501 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
503 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
504 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
505 templateBorderSize=20, doApplyCalibration=True)
506 # Verify that the variance plane of the difference image is correct
507 # when the template and science variance planes are correct
508 _run_and_check_images(science, template, sources, statsCtrl,
509 doDecorrelation=True, doScaleVariance=True)
510 _run_and_check_images(science, template, sources, statsCtrl,
511 doDecorrelation=True, doScaleVariance=False)
512 _run_and_check_images(science, template, sources, statsCtrl,
513 doDecorrelation=False, doScaleVariance=True)
514 _run_and_check_images(science, template, sources, statsCtrl,
515 doDecorrelation=False, doScaleVariance=False)
517 # Verify that the variance plane of the difference image is correct
518 # when the template variance plane is incorrect
519 template.variance.array /= scaleFactor
520 science.variance.array /= scaleFactor
521 _run_and_check_images(science, template, sources, statsCtrl,
522 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
523 _run_and_check_images(science, template, sources, statsCtrl,
524 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
525 _run_and_check_images(science, template, sources, statsCtrl,
526 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
527 _run_and_check_images(science, template, sources, statsCtrl,
528 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
530 def test_scale_variance_convolve_science(self):
531 """Check variance scaling of the image difference.
532 """
533 scienceNoiseLevel = 4.
534 templateNoiseLevel = 2.
535 scaleFactor = 1.345
536 # Make sure to include pixels with the DETECTED mask bit set.
537 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
539 def _run_and_check_images(science, template, sources, statsCtrl,
540 doDecorrelation, doScaleVariance, scaleFactor=1.):
541 """Check that the variance plane matches the expected value for
542 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
543 """
545 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
546 config.mode = "convolveScience"
547 config.doSubtractBackground = False
548 config.doDecorrelation = doDecorrelation
549 config.doScaleVariance = doScaleVariance
550 task = subtractImages.AlardLuptonSubtractTask(config=config)
551 output = task.run(template.clone(), science.clone(), sources)
552 if doScaleVariance:
553 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
554 scaleFactor, atol=0.05)
555 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
556 scaleFactor, atol=0.05)
558 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
559 if doDecorrelation:
560 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
561 else:
562 scienceNoise = computeRobustStatistics(output.matchedScience.variance,
563 output.matchedScience.mask,
564 statsCtrl)
566 if doScaleVariance:
567 templateNoise *= scaleFactor
568 scienceNoise *= scaleFactor
570 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl)
571 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
573 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6)
574 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
575 templateBorderSize=20, doApplyCalibration=True)
576 # Verify that the variance plane of the difference image is correct
577 # when the template and science variance planes are correct
578 _run_and_check_images(science, template, sources, statsCtrl,
579 doDecorrelation=True, doScaleVariance=True)
580 _run_and_check_images(science, template, sources, statsCtrl,
581 doDecorrelation=True, doScaleVariance=False)
582 _run_and_check_images(science, template, sources, statsCtrl,
583 doDecorrelation=False, doScaleVariance=True)
584 _run_and_check_images(science, template, sources, statsCtrl,
585 doDecorrelation=False, doScaleVariance=False)
587 # Verify that the variance plane of the difference image is correct
588 # when the template and science variance planes are incorrect
589 science.variance.array /= scaleFactor
590 template.variance.array /= scaleFactor
591 _run_and_check_images(science, template, sources, statsCtrl,
592 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
593 _run_and_check_images(science, template, sources, statsCtrl,
594 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
595 _run_and_check_images(science, template, sources, statsCtrl,
596 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
597 _run_and_check_images(science, template, sources, statsCtrl,
598 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
600 def test_exposure_properties_convolve_template(self):
601 """Check that all necessary exposure metadata is included
602 when the template is convolved.
603 """
604 noiseLevel = 1.
605 seed = 37
606 rng = np.random.RandomState(seed)
607 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6)
608 psf = science.psf
609 psfAvgPos = psf.getAveragePosition()
610 psfSize = getPsfFwhm(science.psf)
611 psfImg = psf.computeKernelImage(psfAvgPos)
612 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
613 templateBorderSize=20, doApplyCalibration=True)
615 # Generate a random aperture correction map
616 apCorrMap = lsst.afw.image.ApCorrMap()
617 for name in ("a", "b", "c"):
618 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
619 science.info.setApCorrMap(apCorrMap)
621 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
622 config.mode = "convolveTemplate"
624 def _run_and_check_images(doDecorrelation):
625 """Check that the metadata is correct with or without decorrelation.
626 """
627 config.doDecorrelation = doDecorrelation
628 task = subtractImages.AlardLuptonSubtractTask(config=config)
629 output = task.run(template.clone(), science.clone(), sources)
630 psfOut = output.difference.psf
631 psfAvgPos = psfOut.getAveragePosition()
632 if doDecorrelation:
633 # Decorrelation requires recalculating the PSF,
634 # so it will not be the same as the input
635 psfOutSize = getPsfFwhm(science.psf)
636 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
637 else:
638 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
639 self.assertImagesAlmostEqual(psfImg, psfOutImg)
641 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
642 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
643 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
644 self.assertEqual(science.filter, output.difference.filter)
645 self.assertEqual(science.photoCalib, output.difference.photoCalib)
646 _run_and_check_images(doDecorrelation=True)
647 _run_and_check_images(doDecorrelation=False)
649 def test_exposure_properties_convolve_science(self):
650 """Check that all necessary exposure metadata is included
651 when the science image is convolved.
652 """
653 noiseLevel = 1.
654 seed = 37
655 rng = np.random.RandomState(seed)
656 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6)
657 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7,
658 templateBorderSize=20, doApplyCalibration=True)
659 psf = template.psf
660 psfAvgPos = psf.getAveragePosition()
661 psfSize = getPsfFwhm(template.psf)
662 psfImg = psf.computeKernelImage(psfAvgPos)
664 # Generate a random aperture correction map
665 apCorrMap = lsst.afw.image.ApCorrMap()
666 for name in ("a", "b", "c"):
667 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3)))
668 science.info.setApCorrMap(apCorrMap)
670 config = subtractImages.AlardLuptonSubtractTask.ConfigClass()
671 config.mode = "convolveScience"
673 def _run_and_check_images(doDecorrelation):
674 """Check that the metadata is correct with or without decorrelation.
675 """
676 config.doDecorrelation = doDecorrelation
677 task = subtractImages.AlardLuptonSubtractTask(config=config)
678 output = task.run(template.clone(), science.clone(), sources)
679 if doDecorrelation:
680 # Decorrelation requires recalculating the PSF,
681 # so it will not be the same as the input
682 psfOutSize = getPsfFwhm(template.psf)
683 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
684 else:
685 psfOut = output.difference.psf
686 psfAvgPos = psfOut.getAveragePosition()
687 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
688 self.assertImagesAlmostEqual(psfImg, psfOutImg)
690 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction
691 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap())
692 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox())
693 self.assertEqual(science.filter, output.difference.filter)
694 self.assertEqual(science.photoCalib, output.difference.photoCalib)
696 _run_and_check_images(doDecorrelation=True)
697 _run_and_check_images(doDecorrelation=False)
699 def _compare_apCorrMaps(self, a, b):
700 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the
701 same addresses (i.e. so we can compare after serialization).
703 This function is taken from ``ApCorrMapTestCase`` in afw/tests/.
705 Parameters
706 ----------
707 a, b : `lsst.afw.image.ApCorrMap`
708 The two aperture correction maps to compare.
709 """
710 self.assertEqual(len(a), len(b))
711 for name, value in list(a.items()):
712 value2 = b.get(name)
713 self.assertIsNotNone(value2)
714 self.assertEqual(value.getBBox(), value2.getBBox())
715 self.assertFloatsAlmostEqual(
716 value.getCoefficients(), value2.getCoefficients(), rtol=0.0)
719class AlardLuptonPreconvolveSubtractTest(lsst.utils.tests.TestCase):
721 def test_mismatched_template(self):
722 """Test that an error is raised if the template
723 does not fully contain the science image.
724 """
725 xSize = 200
726 ySize = 200
727 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20)
728 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True)
729 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
730 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
731 with self.assertRaises(AssertionError):
732 task.run(template, science, sources)
734 def test_equal_images(self):
735 """Test that running with enough sources produces reasonable output,
736 with the same size psf in the template and science.
737 """
738 noiseLevel = 1.
739 xSize = 400
740 ySize = 400
741 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6,
742 xSize=xSize, ySize=ySize)
743 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7,
744 templateBorderSize=20, doApplyCalibration=True,
745 xSize=xSize, ySize=ySize)
746 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
747 config.doSubtractBackground = False
748 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
749 output = task.run(template, science, sources)
750 # There shoud be no NaN values in the Score image
751 self.assertTrue(np.all(np.isfinite(output.scoreExposure.image.array)))
752 # Mean of Score image should be close to zero.
753 meanError = noiseLevel/np.sqrt(output.scoreExposure.image.array.size)
754 # Make sure to include pixels with the DETECTED mask bit set.
755 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
756 scoreMean = computeRobustStatistics(output.scoreExposure.image,
757 output.scoreExposure.mask,
758 statsCtrl)
759 self.assertFloatsAlmostEqual(scoreMean, 0, atol=5*meanError)
760 nea = computePSFNoiseEquivalentArea(science.psf)
761 # stddev of Score image should be close to expected value.
762 scoreStd = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
763 statsCtrl=statsCtrl, statistic=afwMath.STDEV)
764 self.assertFloatsAlmostEqual(scoreStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
766 def test_clear_template_mask(self):
767 noiseLevel = 1.
768 xSize = 400
769 ySize = 400
770 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6,
771 xSize=xSize, ySize=ySize)
772 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
773 templateBorderSize=20, doApplyCalibration=True,
774 xSize=xSize, ySize=ySize)
775 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"]
776 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
777 config.doSubtractBackground = False # Ensure that each each mask plane is set for some pixels
778 mask = template.mask
779 x0 = 50
780 x1 = 75
781 y0 = 150
782 y1 = 175
783 scienceMaskCheck = {}
784 for maskPlane in mask.getMaskPlaneDict().keys():
785 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0)
786 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane)
787 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
789 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
790 output = task.run(template, science, sources)
791 # Verify that the template mask has been modified in place
792 for maskPlane in mask.getMaskPlaneDict().keys():
793 if maskPlane in diffimEmptyMaskPlanes:
794 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
795 elif maskPlane in config.preserveTemplateMask:
796 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0))
797 else:
798 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0))
799 # Mask planes set in the science image should also be set in the difference
800 # Except the "DETECTED" planes should have been cleared
801 diffimMask = output.scoreExposure.mask
802 for maskPlane, scienceSum in scienceMaskCheck.items():
803 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0)
804 if maskPlane in diffimEmptyMaskPlanes:
805 self.assertEqual(diffimSum, 0)
806 else:
807 self.assertTrue(diffimSum >= scienceSum)
809 def test_agnostic_template_psf(self):
810 """Test that the Score image is the same whether the template PSF is
811 larger or smaller than the science image PSF.
812 """
813 noiseLevel = .3
814 xSize = 400
815 ySize = 400
816 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel,
817 noiseSeed=6, templateBorderSize=0,
818 xSize=xSize, ySize=ySize)
819 template1, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel,
820 noiseSeed=7, doApplyCalibration=True,
821 xSize=xSize, ySize=ySize)
822 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel,
823 noiseSeed=8, doApplyCalibration=True,
824 xSize=xSize, ySize=ySize)
825 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
826 config.doSubtractBackground = False
827 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
829 science_better = task.run(template1, science.clone(), sources)
830 template_better = task.run(template2, science, sources)
831 bbox = science_better.scoreExposure.getBBox().clippedTo(template_better.scoreExposure.getBBox())
833 delta = template_better.scoreExposure[bbox].clone()
834 delta.image -= science_better.scoreExposure[bbox].image
835 delta.variance -= science_better.scoreExposure[bbox].variance
836 delta.mask.array &= science_better.scoreExposure[bbox].mask.array
838 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
839 # Mean of delta should be very close to zero.
840 nGoodPix = np.sum(np.isfinite(delta.image.array))
841 meanError = 2*noiseLevel/np.sqrt(nGoodPix)
842 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl)
843 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl,
844 statistic=afwMath.STDEV)
845 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError)
846 nea = computePSFNoiseEquivalentArea(science.psf)
847 # stddev of Score image should be close to expected value
848 self.assertFloatsAlmostEqual(deltaStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=.1)
850 def test_few_sources(self):
851 """Test with only 1 source, to check that we get a useful error.
852 """
853 xSize = 256
854 ySize = 256
855 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize)
856 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True)
857 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
858 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
859 sources = sources[0:1]
860 with self.assertRaisesRegex(RuntimeError,
861 "Cannot compute PSF matching kernel: too few sources selected."):
862 task.run(template, science, sources)
864 def test_background_subtraction(self):
865 """Check that we can recover the background,
866 and that it is subtracted correctly in the Score image.
867 """
868 noiseLevel = 1.
869 xSize = 512
870 ySize = 512
871 x0 = 123
872 y0 = 456
873 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
874 templateBorderSize=20,
875 xSize=xSize, ySize=ySize, x0=x0, y0=y0,
876 doApplyCalibration=True)
877 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0]
879 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize))
880 background_model = afwMath.Chebyshev1Function2D(params, bbox2D)
881 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6,
882 background=background_model,
883 xSize=xSize, ySize=ySize, x0=x0, y0=y0)
884 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
885 config.doSubtractBackground = True
887 config.makeKernel.kernel.name = "AL"
888 config.makeKernel.kernel.active.fitForBackground = True
889 config.makeKernel.kernel.active.spatialKernelOrder = 1
890 config.makeKernel.kernel.active.spatialBgOrder = 2
891 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
893 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
894 output = task.run(template.clone(), science.clone(), sources)
896 # We should be fitting the same number of parameters as were in the input model
897 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters())
899 # The parameters of the background fit should be close to the input model
900 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()),
901 np.array(params), rtol=0.2)
903 # stddev of Score image should be close to expected value.
904 # This will fail if we have mis-subtracted the background.
905 stdVal = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask,
906 statsCtrl, statistic=afwMath.STDEV)
907 # get the img psf Noise Equivalent Area value
908 nea = computePSFNoiseEquivalentArea(science.psf)
909 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1)
911 def test_scale_variance(self):
912 """Check variance scaling of the Score image.
913 """
914 scienceNoiseLevel = 4.
915 templateNoiseLevel = 2.
916 scaleFactor = 1.345
917 xSize = 400
918 ySize = 400
919 # Make sure to include pixels with the DETECTED mask bit set.
920 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA"))
922 def _run_and_check_images(science, template, sources, statsCtrl,
923 doDecorrelation, doScaleVariance, scaleFactor=1.):
924 """Check that the variance plane matches the expected value for
925 different configurations of ``doDecorrelation`` and ``doScaleVariance``.
926 """
928 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
929 config.doSubtractBackground = False
930 config.doDecorrelation = doDecorrelation
931 config.doScaleVariance = doScaleVariance
932 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
933 output = task.run(template.clone(), science.clone(), sources)
934 if doScaleVariance:
935 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"],
936 scaleFactor, atol=0.05)
937 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"],
938 scaleFactor, atol=0.05)
940 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl)
941 # get the img psf Noise Equivalent Area value
942 nea = computePSFNoiseEquivalentArea(science.psf)
943 scienceNoise /= nea
944 if doDecorrelation:
945 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl)
946 templateNoise /= nea
947 else:
948 # Don't divide by NEA in this case, since the template is convolved
949 # and in the same units as the Score exposure.
950 templateNoise = computeRobustStatistics(output.matchedTemplate.variance,
951 output.matchedTemplate.mask,
952 statsCtrl)
953 if doScaleVariance:
954 templateNoise *= scaleFactor
955 scienceNoise *= scaleFactor
956 varMean = computeRobustStatistics(output.scoreExposure.variance,
957 output.scoreExposure.mask,
958 statsCtrl)
959 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1)
961 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6,
962 xSize=xSize, ySize=ySize)
963 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7,
964 templateBorderSize=20, doApplyCalibration=True,
965 xSize=xSize, ySize=ySize)
966 # Verify that the variance plane of the Score image is correct
967 # when the template and science variance planes are correct
968 _run_and_check_images(science, template, sources, statsCtrl,
969 doDecorrelation=True, doScaleVariance=True)
970 _run_and_check_images(science, template, sources, statsCtrl,
971 doDecorrelation=True, doScaleVariance=False)
972 _run_and_check_images(science, template, sources, statsCtrl,
973 doDecorrelation=False, doScaleVariance=True)
974 _run_and_check_images(science, template, sources, statsCtrl,
975 doDecorrelation=False, doScaleVariance=False)
977 # Verify that the variance plane of the Score image is correct
978 # when the template variance plane is incorrect
979 template.variance.array /= scaleFactor
980 science.variance.array /= scaleFactor
981 _run_and_check_images(science, template, sources, statsCtrl,
982 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor)
983 _run_and_check_images(science, template, sources, statsCtrl,
984 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor)
985 _run_and_check_images(science, template, sources, statsCtrl,
986 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor)
987 _run_and_check_images(science, template, sources, statsCtrl,
988 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor)
990 def test_exposure_properties(self):
991 """Check that all necessary exposure metadata is included
992 with the Score image.
993 """
994 noiseLevel = 1.
995 xSize = 400
996 ySize = 400
997 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6,
998 xSize=xSize, ySize=ySize)
999 psf = science.psf
1000 psfAvgPos = psf.getAveragePosition()
1001 psfSize = getPsfFwhm(science.psf)
1002 psfImg = psf.computeKernelImage(psfAvgPos)
1003 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
1004 templateBorderSize=20, doApplyCalibration=True,
1005 xSize=xSize, ySize=ySize)
1007 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass()
1009 def _run_and_check_images(doDecorrelation):
1010 """Check that the metadata is correct with or without decorrelation.
1011 """
1012 config.doDecorrelation = doDecorrelation
1013 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config)
1014 output = task.run(template.clone(), science.clone(), sources)
1015 psfOut = output.scoreExposure.psf
1016 psfAvgPos = psfOut.getAveragePosition()
1017 if doDecorrelation:
1018 # Decorrelation requires recalculating the PSF,
1019 # so it will not be the same as the input
1020 psfOutSize = getPsfFwhm(science.psf)
1021 self.assertFloatsAlmostEqual(psfSize, psfOutSize)
1022 else:
1023 psfOutImg = psfOut.computeKernelImage(psfAvgPos)
1024 self.assertImagesAlmostEqual(psfImg, psfOutImg)
1026 # check PSF, WCS, bbox, filterLabel, photoCalib
1027 self.assertWcsAlmostEqualOverBBox(science.wcs, output.scoreExposure.wcs, science.getBBox())
1028 self.assertEqual(science.filter, output.scoreExposure.filter)
1029 self.assertEqual(science.photoCalib, output.scoreExposure.photoCalib)
1030 _run_and_check_images(doDecorrelation=True)
1031 _run_and_check_images(doDecorrelation=False)
1034def setup_module(module):
1035 lsst.utils.tests.init()
1038class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
1039 pass
1042if __name__ == "__main__": 1042 ↛ 1043line 1042 didn't jump to line 1043, because the condition on line 1042 was never true
1043 lsst.utils.tests.init()
1044 unittest.main()