Coverage for tests/test_completenessPlot.py: 87%

108 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-08 09:38 +0000

1# This file is part of analysis_tools. 

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 

22 

23import os 

24import shutil 

25import tempfile 

26import unittest 

27 

28import matplotlib 

29import matplotlib.pyplot as plt 

30import numpy as np 

31 

32import lsst.utils.tests 

33from lsst.analysis.tools.actions.plot import CompletenessHist 

34from lsst.analysis.tools.actions.plot.plotUtils import get_and_remove_figure_text 

35from lsst.analysis.tools.math import divide, sqrt 

36 

37# Set to True to debug plot test(s) 

38debug_plot = False 

39if not debug_plot: 39 ↛ 42line 39 didn't jump to line 42 because the condition on line 39 was always true

40 matplotlib.use("Agg") 

41 

42ROOT = os.path.abspath(os.path.dirname(__file__)) 

43filename_texts_ref = os.path.join(ROOT, "data", "test_completenessPlot_texts.txt") 

44path_lines_ref = os.path.join(ROOT, "data", "test_completenessPlot_lines") 

45 

46 

47class CompletenessPlotTestCase(lsst.utils.tests.TestCase): 

48 """CompletenessHist test case.""" 

49 

50 def setUp(self): 

51 self.testDir = tempfile.mkdtemp(dir=ROOT, prefix="test_output_completeness") 

52 

53 plot = CompletenessHist() 

54 

55 plot.action.action.selector_range_ref.vectorKey = "mag_ref" 

56 plot.action.action.selector_range_target.vectorKey = "mag_target" 

57 plot.action.bins.mag_low_max = 25500 

58 plot.action.bins.mag_interval = 100 

59 plot.action.bins.mag_width = 200 

60 

61 band = "r" 

62 

63 mag = 12.5 + 2.5 * np.log10(np.arange(10, 100000)) 

64 zeropoint = mag[-1] + 1 

65 flux = 10 ** (-0.4 * (mag - zeropoint)) 

66 rng = np.random.default_rng(0) 

67 # See usage below if changing this to allow nan/bad extendedness 

68 extendedness = 0.0 + (rng.uniform(size=len(mag)) < 0.99 * (mag - mag[0]) / (mag[-1] - mag[0])) 

69 flux_meas = flux + rng.normal(scale=sqrt(flux * (1 + extendedness))) 

70 flux_pos = flux_meas > 0 

71 flux_err = np.empty_like(flux_meas) 

72 flux_err[flux_pos] = sqrt(flux_meas[flux_pos]) 

73 flux_err[~flux_pos] = 0 

74 detect_sn = divide(flux_meas, flux_err) 

75 good = detect_sn > 3 

76 match_distance = np.full_like(detect_sn, np.nan) 

77 rand_norm_abs = np.abs(rng.normal(size=match_distance.size)) 

78 # make some unmatched as a function of S/N 

79 matched = rand_norm_abs < 0.2 * detect_sn 

80 match_distance[matched] = rand_norm_abs[matched] 

81 

82 flux_meas[~good] = np.nan 

83 

84 # estimated from a DC2 tract 

85 prob_ps = 0.5 * (1 + np.tanh((19.2 - mag) / 2.8)) 

86 is_ps = rng.uniform(size=len(prob_ps)) < prob_ps 

87 is_extended = rng.uniform(size=len(prob_ps)) > prob_ps 

88 is_extended[~good] = np.nan 

89 

90 # add some false detections 

91 false_positive = np.abs(rng.normal(size=match_distance.size)) > 0.5 * detect_sn 

92 flux_meas_extra = flux_meas[false_positive] 

93 nan_extra = np.full_like(flux_meas_extra, np.nan) 

94 action_hist = plot.action.action 

95 action_hist.key_mask_ref = "select_is_ref" 

96 action_hist.key_mask_target = "select_is_target" 

97 is_ps = np.concatenate((is_ps, nan_extra)) 

98 extendedness = np.concatenate((extendedness, extendedness[false_positive])) 

99 is_extended = np.concatenate((is_extended, is_extended[false_positive])) 

100 

101 data = { 

102 "mag_ref": np.concatenate((mag, nan_extra)), 

103 "mag_target": -2.5 * np.log10(np.concatenate((flux_meas, flux_meas_extra))) + zeropoint, 

104 "refcat_is_pointsource": is_ps, 

105 action_hist.key_match_distance: np.concatenate((match_distance, nan_extra)), 

106 action_hist.key_matched_class: is_ps == ~is_extended, 

107 action_hist.key_mask_ref: np.isfinite(is_ps), 

108 action_hist.key_mask_target: np.isfinite(extendedness), 

109 } 

110 

111 results = plot.action(data, band="r") 

112 data.update(results) 

113 self.band = "r" 

114 self.data = data 

115 self.plot = plot 

116 self.plotInfo = {"plotName": "compurity test", "run": "", "tableName": "", "bands": band} 

117 

118 def tearDown(self): 

119 if os.path.exists(self.testDir): 119 ↛ 121line 119 didn't jump to line 121 because the condition on line 119 was always true

120 shutil.rmtree(self.testDir, True) 

121 del self.data 

122 del self.plot 

123 del self.plotInfo 

124 del self.testDir 

125 

126 def test_completenessPlotWithTwoHistsTask(self): 

127 plt.rcParams.update(plt.rcParamsDefault) 

128 result = self.plot( 

129 data=self.data, 

130 band=self.band, 

131 plotInfo=self.plotInfo, 

132 ) 

133 self.assertTrue(isinstance(result, plt.Figure)) 

134 

135 to_compare = True 

136 if to_compare: 136 ↛ 175line 136 didn't jump to line 175 because the condition on line 136 was always true

137 # Set to true to save plots as PNGs 

138 # Use matplotlib.testing.compare.compare_images if needed 

139 save_images = False 

140 if save_images: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true

141 result.savefig(os.path.join(ROOT, "data", "test_completenessPlot.png")) 

142 

143 texts, lines = get_and_remove_figure_text(result) 

144 if save_images: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true

145 result.savefig(os.path.join(ROOT, "data", "test_completenessPlot_unlabeled.png")) 

146 

147 # Set to true to re-generate reference data 

148 resave = False 

149 

150 # Compare line values 

151 for idx, line in enumerate(lines): 

152 filename = os.path.join(path_lines_ref, f"line_{idx}.txt") 

153 if resave: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 np.savetxt(filename, line) 

155 arr = np.loadtxt(filename) 

156 # Differences of order 1e-12 possible between MacOS and Linux 

157 # Plots are generally not expected to be that precise 

158 # Differences to 1e-3 should not be visible with this test data 

159 # nans are possible here (divide by zero) 

160 self.assertFloatsAlmostEqual(arr, line, atol=1e-3, rtol=1e-4, ignoreNaNs=True) 

161 

162 # Ensure that newlines within labels are replaced by a sentinel 

163 newline = "\n" 

164 newline_replace = "[newline]" 

165 # Compare text labels 

166 if resave: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true

167 with open(filename_texts_ref, "w") as f: 

168 f.writelines(f"{text.strip().replace(newline, newline_replace)}\n" for text in texts) 

169 

170 with open(filename_texts_ref) as f: 

171 texts_ref = set(x.strip() for x in f.readlines()) 

172 texts_set = set(x.strip().replace(newline, newline_replace) for x in texts) 

173 

174 self.assertEqual(texts_ref, texts_set) 

175 if debug_plot: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true

176 plt.show() 

177 

178 

179class MemoryTester(lsst.utils.tests.MemoryTestCase): 

180 pass 

181 

182 

183def setup_module(module): 

184 lsst.utils.tests.init() 

185 

186 

187if __name__ == "__main__": 187 ↛ 188line 187 didn't jump to line 188 because the condition on line 187 was never true

188 lsst.utils.tests.init() 

189 unittest.main()