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

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/>. 

21 

22import types 

23import unittest 

24 

25import astropy.units as u 

26import numpy as np 

27 

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 

36 

37from utils import makeTestExposureRecord, makeTestImage, makeTestVisitInfo 

38 

39 

40class ComputeSpatiallySampledMetricsTest(lsst.utils.tests.TestCase): 

41 """Test the metrics that are sampled across the difference image.""" 

42 

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() 

57 

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) 

70 

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 

78 

79 def testMaskPlaneNotInImage(self): 

80 """A configured mask plane that is not registered has a fraction of 

81 zero, rather than raising. 

82 

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()) 

91 

92 metrics = self._run(metricsMaskPlanes=[present, missing]) 

93 

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"]))) 

97 

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) 

115 

116 def _makeFutureInputs(self): 

117 """Convert the images and kernel made in `setUp` to the types the 

118 ``future`` mode reads. 

119 

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 

137 

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 

152 

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) 

157 

158 

159class _RecordingQuantumContext: 

160 """Minimal `~lsst.pipe.base.QuantumContext` that records what was put.""" 

161 def __init__(self): 

162 self.put_values = None 

163 

164 def get(self, refs): 

165 return dict(refs.__dict__) 

166 

167 def put(self, values, refs): 

168 self.put_values = values 

169 

170 

171def setup_module(module): 

172 lsst.utils.tests.init() 

173 

174 

175class MemoryTestCase(lsst.utils.tests.MemoryTestCase): 

176 pass 

177 

178 

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()