Coverage for tests/test_computeSpatiallySampledMetrics.py: 97%
82 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-13 09:35 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-13 09:35 +0000
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 types
23import unittest
25import numpy as np
27import lsst.afw.image
28import lsst.afw.table
29import lsst.ip.diffim
30import lsst.utils.tests
31from lsst.images.convolution_kernels import ImageBasisConvolutionKernel
33from utils import makeTestImage
36class ComputeSpatiallySampledMetricsTest(lsst.utils.tests.TestCase):
37 """Test the metrics that are sampled across the difference image."""
39 def setUp(self):
40 noiseLevel = 1.
41 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
42 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
43 templateBorderSize=20, doApplyCalibration=True)
44 config = lsst.ip.diffim.AlardLuptonSubtractTask.ConfigClass()
45 config.sourceSelector.signalToNoise.fluxField = "truth_instFlux"
46 config.sourceSelector.signalToNoise.errField = "truth_instFluxErr"
47 subtraction = lsst.ip.diffim.AlardLuptonSubtractTask(config=config).run(template, science, sources)
48 self.science = subtraction.matchedScience
49 self.template = template
50 self.difference = subtraction.difference
51 self.psfMatchingKernel = subtraction.psfMatchingKernel
52 self.diaSources = self._makeDiaSources()
54 @staticmethod
55 def _makeDiaSources():
56 """Return an empty catalog with the fields the metrics read from the
57 detected sources.
58 """
59 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
60 schema.addField("x", "D", "Centroid x.")
61 schema.addField("y", "D", "Centroid y.")
62 schema.addField("isDipole", "Flag", "Is this source a dipole?")
63 schema.addField("dipoleAngle", "D", "Dipole orientation.")
64 schema.addField("dipoleLength", "D", "Dipole separation.")
65 return lsst.afw.table.SourceCatalog(schema)
67 def _run(self, **kwargs):
68 """Run the metrics task on the images made in `setUp`."""
69 config = lsst.ip.diffim.SpatiallySampledMetricsTask.ConfigClass()
70 config.update(**kwargs)
71 task = lsst.ip.diffim.SpatiallySampledMetricsTask(config=config)
72 return task.run(self.science, self.template, self.difference, self.diaSources,
73 self.psfMatchingKernel).spatiallySampledMetrics
75 def testMaskPlaneNotInImage(self):
76 """A configured mask plane that is not registered has a fraction of
77 zero, rather than raising.
79 An image converted from `lsst.images.DifferenceImage` only defines the
80 planes that have pixels set, so INJECTED is missing from an ordinary
81 AP run.
82 """
83 present = "DETECTED"
84 missing = "NOT_A_MASK_PLANE"
85 self.assertIn(present, self.difference.mask.getMaskPlaneDict())
86 self.assertNotIn(missing, self.difference.mask.getMaskPlaneDict())
88 metrics = self._run(metricsMaskPlanes=[present, missing])
90 self.assertGreater(len(metrics), 0)
91 self.assertTrue(np.all(metrics[f"{missing.lower()}_mask_fraction"] == 0))
92 self.assertTrue(np.all(np.isfinite(metrics[f"{present.lower()}_mask_fraction"])))
94 def testConnections(self):
95 """``image_type`` sets the storage class of the kernel input only."""
96 for imageType, kernelStorageClass in (("legacy", "MatchingKernel"),
97 ("future", "ConvolutionKernel")):
98 with self.subTest(imageType=imageType):
99 config = lsst.ip.diffim.SpatiallySampledMetricsTask.ConfigClass()
100 config.image_type = imageType
101 connections = config.ConnectionsClass(config=config)
102 self.assertEqual(connections.psfMatchingKernel.storageClass, kernelStorageClass)
103 for name in ("science", "template", "difference"):
104 self.assertEqual(getattr(connections, name).storageClass, "ExposureF")
106 def testRunQuantumFuture(self):
107 """``runQuantum`` converts a future kernel back to the legacy type,
108 giving the same metrics as a legacy run.
109 """
110 inputs = {"science": self.science, "template": self.template, "difference": self.difference,
111 "diaSources": self.diaSources}
112 metrics = {}
113 for imageType, kernel in (
114 ("legacy", self.psfMatchingKernel),
115 ("future", ImageBasisConvolutionKernel.from_legacy(self.psfMatchingKernel)),
116 ):
117 config = lsst.ip.diffim.SpatiallySampledMetricsTask.ConfigClass()
118 config.image_type = imageType
119 task = lsst.ip.diffim.SpatiallySampledMetricsTask(config=config)
120 butlerQC = _RecordingQuantumContext()
121 task.runQuantum(butlerQC, types.SimpleNamespace(**inputs, psfMatchingKernel=kernel), None)
122 metrics[imageType] = butlerQC.put_values.spatiallySampledMetrics
124 self.assertGreater(len(metrics["legacy"]), 0)
125 for name in ("psfMatchingKernel_sum", "psfMatchingKernel_dx", "psfMatchingKernel_dy",
126 "psfMatchingKernel_residualNorm"):
127 np.testing.assert_allclose(metrics["future"][name], metrics["legacy"][name],
128 rtol=1e-6, err_msg=name)
131class _RecordingQuantumContext:
132 """Minimal `~lsst.pipe.base.QuantumContext` that records what was put."""
133 def __init__(self):
134 self.put_values = None
136 def get(self, refs):
137 return dict(refs.__dict__)
139 def put(self, values, refs):
140 self.put_values = values
143def setup_module(module):
144 lsst.utils.tests.init()
147class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
148 pass
151if __name__ == "__main__": 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true
152 lsst.utils.tests.init()
153 unittest.main()