Coverage for tests/test_computeSpatiallySampledMetrics.py: 97%
99 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 09:17 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 09:17 +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 astropy.units as u
26import numpy as np
28import lsst.afw.cameraGeom.testUtils
29import lsst.afw.image
30import lsst.afw.table
31from lsst.daf.butler import DimensionUniverse
32import lsst.ip.diffim
33import lsst.utils.tests
34from lsst.images import DifferenceImage, VisitImage
35from lsst.images.convolution_kernels import ImageBasisConvolutionKernel
37from utils import makeTestExposureRecord, makeTestImage, makeTestVisitInfo
40class ComputeSpatiallySampledMetricsTest(lsst.utils.tests.TestCase):
41 """Test the metrics that are sampled across the difference image."""
43 def setUp(self):
44 noiseLevel = 1.
45 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6)
46 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7,
47 templateBorderSize=20, doApplyCalibration=True)
48 config = lsst.ip.diffim.AlardLuptonSubtractTask.ConfigClass()
49 config.sourceSelector.signalToNoise.fluxField = "truth_instFlux"
50 config.sourceSelector.signalToNoise.errField = "truth_instFluxErr"
51 subtraction = lsst.ip.diffim.AlardLuptonSubtractTask(config=config).run(template, science, sources)
52 self.science = subtraction.matchedScience
53 self.template = template
54 self.difference = subtraction.difference
55 self.psfMatchingKernel = subtraction.psfMatchingKernel
56 self.diaSources = self._makeDiaSources()
58 @staticmethod
59 def _makeDiaSources():
60 """Return an empty catalog with the fields the metrics read from the
61 detected sources.
62 """
63 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
64 schema.addField("x", "D", "Centroid x.")
65 schema.addField("y", "D", "Centroid y.")
66 schema.addField("isDipole", "Flag", "Is this source a dipole?")
67 schema.addField("dipoleAngle", "D", "Dipole orientation.")
68 schema.addField("dipoleLength", "D", "Dipole separation.")
69 return lsst.afw.table.SourceCatalog(schema)
71 def _run(self, **kwargs):
72 """Run the metrics task on the images made in `setUp`."""
73 config = lsst.ip.diffim.SpatiallySampledMetricsTask.ConfigClass()
74 config.update(**kwargs)
75 task = lsst.ip.diffim.SpatiallySampledMetricsTask(config=config)
76 return task.run(self.science, self.template, self.difference, self.diaSources,
77 self.psfMatchingKernel).spatiallySampledMetrics
79 def testMaskPlaneNotInImage(self):
80 """A configured mask plane that is not registered has a fraction of
81 zero, rather than raising.
83 An image converted from `lsst.images.DifferenceImage` only defines the
84 planes that have pixels set, so INJECTED is missing from an ordinary
85 AP run.
86 """
87 present = "DETECTED"
88 missing = "NOT_A_MASK_PLANE"
89 self.assertIn(present, self.difference.mask.getMaskPlaneDict())
90 self.assertNotIn(missing, self.difference.mask.getMaskPlaneDict())
92 metrics = self._run(metricsMaskPlanes=[present, missing])
94 self.assertGreater(len(metrics), 0)
95 self.assertTrue(np.all(metrics[f"{missing.lower()}_mask_fraction"] == 0))
96 self.assertTrue(np.all(np.isfinite(metrics[f"{present.lower()}_mask_fraction"])))
98 def testConnections(self):
99 """``image_type`` sets the storage class of the image and kernel
100 inputs.
101 """
102 expected = {
103 "legacy": {"science": "ExposureF", "template": "ExposureF", "difference": "ExposureF",
104 "psfMatchingKernel": "MatchingKernel"},
105 "future": {"science": "VisitImage", "template": "DifferenceImage",
106 "difference": "DifferenceImage", "psfMatchingKernel": "ConvolutionKernel"},
107 }
108 for imageType, storageClasses in expected.items():
109 with self.subTest(imageType=imageType):
110 config = lsst.ip.diffim.SpatiallySampledMetricsTask.ConfigClass()
111 config.image_type = imageType
112 connections = config.ConnectionsClass(config=config)
113 for name, storageClass in storageClasses.items():
114 self.assertEqual(getattr(connections, name).storageClass, storageClass)
116 def _makeFutureInputs(self):
117 """Convert the images and kernel made in `setUp` to the types the
118 ``future`` mode reads.
120 Returns
121 -------
122 inputs : `dict` [`str`, `object`]
123 The converted inputs, keyed by connection name.
124 """
125 detector = list(lsst.afw.cameraGeom.testUtils.CameraWrapper().camera)[0]
126 visit = 12345
127 exposureRecord = makeTestExposureRecord(DimensionUniverse(), visit=visit)
128 inputs = {}
129 for name, imageType in (("science", VisitImage), ("template", DifferenceImage),
130 ("difference", DifferenceImage)):
131 exposure = getattr(self, name).clone()
132 exposure.setDetector(detector)
133 exposure.info.setVisitInfo(makeTestVisitInfo(visit))
134 inputs[name] = imageType.from_legacy(exposure, unit=u.nJy, exposure_record=exposureRecord)
135 inputs["psfMatchingKernel"] = ImageBasisConvolutionKernel.from_legacy(self.psfMatchingKernel)
136 return inputs
138 def testRunQuantumFuture(self):
139 """``runQuantum`` converts the future inputs back to the legacy types,
140 giving the same metrics as a legacy run.
141 """
142 legacyInputs = {"science": self.science, "template": self.template, "difference": self.difference,
143 "psfMatchingKernel": self.psfMatchingKernel}
144 metrics = {}
145 for imageType, inputs in (("legacy", legacyInputs), ("future", self._makeFutureInputs())):
146 config = lsst.ip.diffim.SpatiallySampledMetricsTask.ConfigClass()
147 config.image_type = imageType
148 task = lsst.ip.diffim.SpatiallySampledMetricsTask(config=config)
149 butlerQC = _RecordingQuantumContext()
150 task.runQuantum(butlerQC, types.SimpleNamespace(**inputs, diaSources=self.diaSources), None)
151 metrics[imageType] = butlerQC.put_values.spatiallySampledMetrics
153 self.assertGreater(len(metrics["legacy"]), 0)
154 self.assertEqual(metrics["future"].colnames, metrics["legacy"].colnames)
155 for name in metrics["legacy"].colnames:
156 np.testing.assert_array_equal(metrics["future"][name], metrics["legacy"][name], err_msg=name)
159class _RecordingQuantumContext:
160 """Minimal `~lsst.pipe.base.QuantumContext` that records what was put."""
161 def __init__(self):
162 self.put_values = None
164 def get(self, refs):
165 return dict(refs.__dict__)
167 def put(self, values, refs):
168 self.put_values = values
171def setup_module(module):
172 lsst.utils.tests.init()
175class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
176 pass
179if __name__ == "__main__": 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true
180 lsst.utils.tests.init()
181 unittest.main()