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

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 numpy as np 

26 

27import lsst.afw.image 

28import lsst.afw.table 

29import lsst.ip.diffim 

30import lsst.utils.tests 

31from lsst.images.convolution_kernels import ImageBasisConvolutionKernel 

32 

33from utils import makeTestImage 

34 

35 

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

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

38 

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

53 

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) 

66 

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 

74 

75 def testMaskPlaneNotInImage(self): 

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

77 zero, rather than raising. 

78 

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

87 

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

89 

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

93 

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

105 

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 

123 

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) 

129 

130 

131class _RecordingQuantumContext: 

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

133 def __init__(self): 

134 self.put_values = None 

135 

136 def get(self, refs): 

137 return dict(refs.__dict__) 

138 

139 def put(self, values, refs): 

140 self.put_values = values 

141 

142 

143def setup_module(module): 

144 lsst.utils.tests.init() 

145 

146 

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

148 pass 

149 

150 

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