Coverage for tests/test_scatterPlot.py: 89%

102 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-16 15:49 -0700

1# This file is part of analysis_drp. 

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 lsst.utils.tests 

29import matplotlib 

30import matplotlib.pyplot as plt 

31import numpy as np 

32import pandas as pd 

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

34from lsst.analysis.tools.actions.plot.scatterplotWithTwoHists import ( 

35 ScatterPlotStatsAction, 

36 ScatterPlotWithTwoHists, 

37) 

38from lsst.analysis.tools.actions.vector.mathActions import ConstantValue, DivideVector, SubtractVector 

39from lsst.analysis.tools.actions.vector.selectors import ( 

40 GalaxySelector, 

41 SnSelector, 

42 StarSelector, 

43 VectorSelector, 

44) 

45from lsst.analysis.tools.actions.vector.vectorActions import ConvertFluxToMag, DownselectVector, LoadVector 

46from lsst.analysis.tools.interfaces import AnalysisTool 

47from lsst.analysis.tools.math import sqrt 

48 

49matplotlib.use("Agg") 

50 

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

52filename_texts_ref = os.path.join(ROOT, "data", "test_scatterPlot_texts.txt") 

53path_lines_ref = os.path.join(ROOT, "data", "test_scatterPlot_lines") 

54 

55 

56class ScatterPlotWithTwoHistsTaskTestCase(lsst.utils.tests.TestCase): 

57 """ScatterPlotWithTwoHistsTask test case.""" 

58 

59 def setUp(self): 

60 self.testDir = tempfile.mkdtemp(dir=ROOT, prefix="test_output") 

61 

62 # Set up a quasi-plausible measurement catalog 

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

64 flux = 10 ** (-0.4 * (mag - (mag[-1] + 1))) 

65 rng = np.random.default_rng(0) 

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

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

68 flux_err = sqrt(flux_meas * (1 + extendedness)) 

69 good = (flux_meas / sqrt(flux * (1 + extendedness))) > 3 

70 extendedness = extendedness[good] 

71 flux = flux[good] 

72 flux_meas = flux_meas[good] 

73 flux_err = flux_err[good] 

74 

75 suffix_x, suffix_y, suffix_stat = "_x", "_y", "_stat" 

76 

77 # Configure the plot to show observed vs true mags 

78 action = ScatterPlotWithTwoHists( 

79 xAxisLabel="mag", 

80 yAxisLabel="mag meas - ref", 

81 magLabel="mag", 

82 plotTypes=[ 

83 "galaxies", 

84 "stars", 

85 ], 

86 xLims=(20, 30), 

87 yLims=(-1000, 1000), 

88 addSummaryPlot=False, 

89 # Make sure adding a suffix works to produce multiple plots 

90 suffix_x=suffix_x, 

91 suffix_y=suffix_y, 

92 suffix_stat=suffix_stat, 

93 ) 

94 plot = AnalysisTool() 

95 plot.produce.plot = action 

96 

97 # Load the relevant columns 

98 key_flux = "meas_Flux" 

99 plot.process.buildActions.fluxes_meas = LoadVector(vectorKey=key_flux) 

100 plot.process.buildActions.fluxes_err = LoadVector(vectorKey=f"{key_flux}Err") 

101 plot.process.buildActions.fluxes_ref = LoadVector(vectorKey="ref_Flux") 

102 plot.process.buildActions.mags_ref = ConvertFluxToMag( 

103 vectorKey=plot.process.buildActions.fluxes_ref.vectorKey 

104 ) 

105 

106 # Compute the y-axis quantity 

107 plot.process.buildActions.diff = SubtractVector( 

108 actionA=ConvertFluxToMag( 

109 vectorKey=plot.process.buildActions.fluxes_meas.vectorKey, returnMillimags=True 

110 ), 

111 actionB=DivideVector( 

112 actionA=plot.process.buildActions.mags_ref, 

113 actionB=ConstantValue(value=1e-3), 

114 ), 

115 ) 

116 

117 # Filter stars/galaxies, storing quantities separately 

118 plot.process.buildActions.galaxySelector = GalaxySelector(vectorKey="refExtendedness") 

119 plot.process.buildActions.starSelector = StarSelector(vectorKey="refExtendedness") 

120 for singular, plural in (("galaxy", "Galaxies"), ("star", "Stars")): 

121 setattr( 

122 plot.process.filterActions, 

123 f"x{plural}{suffix_x}", 

124 DownselectVector( 

125 vectorKey="mags_ref", selector=VectorSelector(vectorKey=f"{singular}Selector") 

126 ), 

127 ) 

128 setattr( 

129 plot.process.filterActions, 

130 f"y{plural}{suffix_y}", 

131 DownselectVector(vectorKey="diff", selector=VectorSelector(vectorKey=f"{singular}Selector")), 

132 ) 

133 setattr( 

134 plot.process.filterActions, 

135 f"flux{plural}", 

136 DownselectVector( 

137 vectorKey="fluxes_meas", selector=VectorSelector(vectorKey=f"{singular}Selector") 

138 ), 

139 ) 

140 setattr( 

141 plot.process.filterActions, 

142 f"fluxErr{plural}", 

143 DownselectVector( 

144 vectorKey="fluxes_err", selector=VectorSelector(vectorKey=f"{singular}Selector") 

145 ), 

146 ) 

147 

148 # Compute low/high SN summary stats 

149 statAction = ScatterPlotStatsAction( 

150 vectorKey=f"y{plural}{suffix_y}", 

151 fluxType=f"flux{plural}", 

152 highSNSelector=SnSelector(fluxType=f"flux{plural}", threshold=50), 

153 lowSNSelector=SnSelector(fluxType=f"flux{plural}", threshold=20), 

154 suffix=suffix_stat, 

155 ) 

156 setattr(plot.process.calculateActions, plural.lower(), statAction) 

157 

158 data = { 

159 "ref_Flux": flux, 

160 key_flux: flux_meas, 

161 f"{key_flux}Err": flux_err, 

162 "refExtendedness": extendedness, 

163 } 

164 

165 self.data = pd.DataFrame(data) 

166 print(self.data.columns) 

167 self.plot = plot 

168 self.plot.finalize() 

169 plotInfo = {key: "test" for key in ("plotName", "run", "tableName")} 

170 plotInfo["bands"] = [] 

171 self.plotInfo = plotInfo 

172 

173 def tearDown(self): 

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

175 shutil.rmtree(self.testDir, True) 

176 del self.data 

177 del self.plot 

178 del self.plotInfo 

179 del self.testDir 

180 

181 def test_ScatterPlotWithTwoHistsTask(self): 

182 plt.rcParams.update(plt.rcParamsDefault) 

183 result = self.plot( 

184 data=self.data, 

185 skymap=None, 

186 plotInfo=self.plotInfo, 

187 ) 

188 # unpack the result from the dictionary 

189 result = result[type(self.plot.produce.plot).__name__] 

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

191 

192 # Set to true to save plots as PNGs 

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

194 save_images = False 

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

196 result.savefig(os.path.join(ROOT, "data", "test_scatterPlot.png")) 

197 

198 texts, lines = get_and_remove_figure_text(result) 

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

200 result.savefig(os.path.join(ROOT, "data", "test_scatterPlot_unlabeled.png")) 

201 

202 # Set to true to re-generate reference data 

203 resave = False 

204 

205 # Compare line values 

206 for idx, line in enumerate(lines): 

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

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

209 np.savetxt(filename, line) 

210 arr = np.loadtxt(filename) 

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

212 # Plots are generally not expected to be that precise 

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

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

215 

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

217 newline = "\n" 

218 newline_replace = "[newline]" 

219 # Compare text labels 

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

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

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

223 

224 with open(filename_texts_ref, "r") as f: 

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

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

227 

228 self.assertEqual(texts_ref, texts_set) 

229 

230 

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

232 pass 

233 

234 

235def setup_module(module): 

236 lsst.utils.tests.init() 

237 

238 

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

240 lsst.utils.tests.init() 

241 unittest.main()