Coverage for tests/test_guider.py: 22%

147 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-09 05:08 -0700

1# This file is part of summit_utils. 

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 os 

23import tempfile 

24import unittest 

25 

26import numpy as np 

27import pandas as pd 

28 

29import lsst.utils.tests 

30from lsst.daf.butler import Butler 

31from lsst.meas.algorithms.stamps import Stamps 

32from lsst.summit.utils.butlerUtils import makeDefaultButler 

33from lsst.summit.utils.guiders.metrics import GuiderMetricsBuilder 

34from lsst.summit.utils.guiders.plotting import GuiderPlotter 

35from lsst.summit.utils.guiders.reading import GuiderData, GuiderReader 

36from lsst.summit.utils.guiders.tracking import GuiderStarTracker 

37from lsst.summit.utils.utils import getSite 

38 

39 

40class GuiderTestCase(unittest.TestCase): 

41 """Tests of the run method with fake data.""" 

42 

43 def setUp(self) -> None: 

44 try: 

45 if getSite() == "jenkins": 

46 raise unittest.SkipTest("Skip running butler-driven tests in Jenkins.") 

47 self.butler = makeDefaultButler("LSSTCam") 

48 except FileNotFoundError: 

49 raise unittest.SkipTest("Skipping tests that require the LSSTCam butler repo.") 

50 self.assertIsInstance(self.butler, Butler) 

51 

52 self.dayObs = 20250629 

53 self.seqNum = 340 

54 self.expId = 2025062900340 

55 

56 self.reader = GuiderReader(self.butler, view="dvcs") 

57 self.guiderData = self.reader.get(dayObs=self.dayObs, seqNum=self.seqNum) 

58 self.tracker = GuiderStarTracker(self.guiderData) 

59 self.stars = self.tracker.trackGuiderStars(refCatalog=None) 

60 self.plotter = GuiderPlotter(self.guiderData, starsDf=self.stars) 

61 self.metricsBuilder = GuiderMetricsBuilder(self.stars, self.guiderData.nMissingStamps) 

62 

63 def test_types(self) -> None: 

64 self.assertIsInstance(self.guiderData.header, dict) 

65 self.assertIsInstance(self.guiderData.stampsMap, dict) 

66 

67 expectedKeys = ( 

68 "R00_SG0", 

69 "R00_SG1", 

70 "R04_SG0", 

71 "R04_SG1", 

72 "R40_SG0", 

73 "R40_SG1", 

74 "R44_SG0", 

75 "R44_SG1", 

76 ) 

77 self.assertTrue( 

78 all(key in self.guiderData.stampsMap for key in expectedKeys), 

79 "Not all expected guider datasets are present in the data.", 

80 ) 

81 

82 detName = "R00_SG0" 

83 single = self.guiderData[detName, 0] 

84 self.assertIsInstance(single, np.ndarray) 

85 stack = self.guiderData.getStampArrayCoadd(detName=detName) 

86 self.assertIsInstance(stack, np.ndarray) 

87 self.assertEqual(single.shape, (400, 400)) 

88 

89 return 

90 

91 def test_reading(self) -> None: 

92 """Test that the reader can read the expected data.""" 

93 self.assertTrue(self.guiderData.isMedianSubtracted, "isMedianSubtracted not set correctly") 

94 for detName in self.guiderData.guiderNames: 

95 single = self.guiderData[detName, 0] 

96 self.assertIsInstance(single, np.ndarray) 

97 self.assertEqual(single.shape, (400, 400)) 

98 self.assertLessEqual(abs(np.nanmedian(single)), 10, "median subtracted median is too high") 

99 

100 stack = self.guiderData.getStampArrayCoadd(detName=detName) 

101 self.assertIsInstance(stack, np.ndarray) 

102 self.assertEqual(stack.shape, (400, 400)) 

103 self.assertLessEqual(abs(np.nanmedian(stack)), 10, "stack median subtracted median is too high") 

104 

105 fullStamps = self.guiderData[detName] 

106 self.assertIsInstance(fullStamps, Stamps) 

107 

108 noMedianSubtracted = self.reader.get(dayObs=self.dayObs, seqNum=self.seqNum, doSubtractMedian=False) 

109 self.assertIsInstance(noMedianSubtracted, GuiderData) 

110 self.assertFalse(noMedianSubtracted.isMedianSubtracted, "isMedianSubtracted not set correctly") 

111 for detName in noMedianSubtracted.guiderNames: 

112 single = noMedianSubtracted[detName, 0] 

113 self.assertIsInstance(single, np.ndarray) 

114 self.assertEqual(single.shape, (400, 400)) 

115 self.assertGreater(abs(np.nanmedian(single)), 500, "median un-subtracted median is too low") 

116 

117 stack = noMedianSubtracted.getStampArrayCoadd(detName=detName) 

118 self.assertIsInstance(stack, np.ndarray) 

119 self.assertEqual(stack.shape, (400, 400)) 

120 self.assertGreater(abs(np.nanmedian(single)), 500, "median un-subtracted median is too low") 

121 

122 fullStamps = noMedianSubtracted[detName] 

123 self.assertIsInstance(fullStamps, Stamps) 

124 

125 def test_detection(self) -> None: 

126 self.assertIsInstance(self.stars, pd.DataFrame) 

127 requiredColumns = ( 

128 "xroi", 

129 "yroi", 

130 "dx", 

131 "dy", 

132 "dalt", 

133 "daz", 

134 "fwhm", 

135 "trackid", 

136 "expid", 

137 ) 

138 self.assertTrue( 

139 all(col in self.stars.columns for col in requiredColumns), 

140 "Not all required columns are present in the stars DataFrame.", 

141 ) 

142 

143 maxStampIndex = max(self.stars["stamp"]) 

144 nStamps = len(self.guiderData) # we should make an attribute for this 

145 

146 # we skip the first stamp, so the max index should be nStamps - 1 

147 self.assertEqual(maxStampIndex, nStamps - 1, "Did not get detections for all expected stamps") 

148 

149 def testPlotMosaicFullView(self) -> None: 

150 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

151 self.plotter.plotMosaic(stampNum=-1, cutoutSize=-1, plo=50, phi=98, saveAs=tmp.name) 

152 os.fsync(tmp.fileno()) # be strict 

153 size = os.path.getsize(tmp.name) 

154 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

155 

156 def testPlotMosaicZoomView(self) -> None: 

157 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

158 self.plotter.plotMosaic(stampNum=-1, cutoutSize=12, plo=50, phi=98, saveAs=tmp.name) 

159 os.fsync(tmp.fileno()) # be strict 

160 size = os.path.getsize(tmp.name) 

161 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

162 

163 def testPlotMosaicStampZoomView(self) -> None: 

164 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

165 self.plotter.plotMosaic(stampNum=4, cutoutSize=12, plo=50, phi=98, saveAs=tmp.name) 

166 os.fsync(tmp.fileno()) # be strict 

167 size = os.path.getsize(tmp.name) 

168 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

169 

170 def testStripPlotPsf(self) -> None: 

171 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

172 self.plotter.stripPlot(plotType="psf", saveAs=tmp.name) 

173 os.fsync(tmp.fileno()) 

174 size = os.path.getsize(tmp.name) 

175 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

176 

177 def testStripPlotCentroidAltAz(self) -> None: 

178 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

179 self.plotter.stripPlot(plotType="centroidAltAz", saveAs=tmp.name) 

180 os.fsync(tmp.fileno()) 

181 size = os.path.getsize(tmp.name) 

182 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

183 

184 def testStripPlotFlux(self) -> None: 

185 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

186 self.plotter.stripPlot(plotType="flux", saveAs=tmp.name) 

187 os.fsync(tmp.fileno()) 

188 size = os.path.getsize(tmp.name) 

189 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

190 

191 def testStripPlotShape(self) -> None: 

192 with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp: 

193 self.plotter.stripPlot(plotType="ellip", saveAs=tmp.name) 

194 os.fsync(tmp.fileno()) 

195 size = os.path.getsize(tmp.name) 

196 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

197 

198 def testMakeGif(self) -> None: 

199 with tempfile.NamedTemporaryFile(suffix=".gif", delete=True) as tmp: 

200 # test the crop and zoom as a gif 

201 self.plotter.makeAnimation(cutoutSize=14, plo=50, phi=98, saveAs=tmp.name) 

202 os.fsync(tmp.fileno()) 

203 size = os.path.getsize(tmp.name) 

204 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

205 

206 def testMakeMp4(self) -> None: 

207 with tempfile.NamedTemporaryFile(suffix=".mp4", delete=True) as tmp: 

208 # test the full frame as an mp4 

209 self.plotter.makeAnimation(cutoutSize=-1, plo=50, phi=98, saveAs=tmp.name) 

210 os.fsync(tmp.fileno()) 

211 size = os.path.getsize(tmp.name) 

212 self.assertGreater(size, 1000, f"{tmp.name} too small: {size} bytes") 

213 

214 def test_metrics(self) -> None: 

215 # Check that metrics can be built without error 

216 metrics = self.metricsBuilder.buildMetrics(self.expId) 

217 self.assertIsInstance(metrics, pd.DataFrame) 

218 self.assertGreater(len(metrics), 0, "Metrics DataFrame is empty") 

219 

220 # These are what's currently there. Why is the 8th guider missing? 

221 expectedColumns = ( 

222 "n_guiders", 

223 "n_stars", 

224 "n_missing_stamps", 

225 "n_measurements", 

226 "fraction_possible_measurements", 

227 "exptime", 

228 "R00_SG0", 

229 "R00_SG1", 

230 "R04_SG0", 

231 "R04_SG1", 

232 "R40_SG0", 

233 "R40_SG1", 

234 "R44_SG0", 

235 "R44_SG1", 

236 "az_drift_slope", 

237 "az_drift_intercept", 

238 "az_drift_trend_rmse", 

239 "az_drift_global_std", 

240 "az_drift_outlier_frac", 

241 "az_drift_slope_significance", 

242 "az_drift_nsize", 

243 "alt_drift_slope", 

244 "alt_drift_intercept", 

245 "alt_drift_trend_rmse", 

246 "alt_drift_global_std", 

247 "alt_drift_outlier_frac", 

248 "alt_drift_slope_significance", 

249 "alt_drift_nsize", 

250 "rotator_slope", 

251 "rotator_intercept", 

252 "rotator_trend_rmse", 

253 "rotator_global_std", 

254 "rotator_outlier_frac", 

255 "rotator_slope_significance", 

256 "rotator_nsize", 

257 "mag_slope", 

258 "mag_intercept", 

259 "mag_trend_rmse", 

260 "mag_global_std", 

261 "mag_outlier_frac", 

262 "mag_slope_significance", 

263 "mag_nsize", 

264 "psf_slope", 

265 "psf_intercept", 

266 "psf_trend_rmse", 

267 "psf_global_std", 

268 "psf_outlier_frac", 

269 "psf_slope_significance", 

270 "psf_nsize", 

271 ) 

272 for col in expectedColumns: 

273 self.assertIn(col, metrics.columns, f"Column {col} is missing from metrics DataFrame") 

274 

275 # check this runs without error 

276 self.metricsBuilder.printSummary() 

277 

278 

279class TestMemory(lsst.utils.tests.MemoryTestCase): 

280 pass 

281 

282 

283def setup_module(module): 

284 lsst.utils.tests.init() 

285 

286 

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

288 lsst.utils.tests.init() 

289 unittest.main()