Coverage for tests/test_display_matplotlib.py: 99%

265 statements  

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

1# 

2# Copyright 2008-2017 AURA/LSST. 

3# 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# 

7# This program is free software: you can redistribute it and/or modify 

8# it under the terms of the GNU General Public License as published by 

9# the Free Software Foundation, either version 3 of the License, or 

10# (at your option) any later version. 

11# 

12# This program is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the LSST License Statement and 

18# the GNU General Public License along with this program. If not, 

19# see <https://www.lsstcorp.org/LegalNotices/>. 

20# 

21""" 

22Tests for display_matplotlib 

23""" 

24import unittest 

25 

26import matplotlib 

27matplotlib.use("Agg") 

28import matplotlib.figure # noqa: E402 

29import matplotlib.text # noqa: E402 

30from matplotlib.backends.backend_agg import FigureCanvasAgg # noqa: E402 

31 

32import lsst.utils.tests # noqa: E402 

33import lsst.geom # noqa: E402 

34import lsst.afw.display as afwDisplay # noqa: E402 

35import lsst.afw.geom as afwGeom # noqa: E402 

36import lsst.afw.image as afwImage # noqa: E402 

37 

38 

39def makeTestWcs(): 

40 """Return a rotated TAN SkyWcs for testing.""" 

41 return afwGeom.makeSkyWcs( 

42 crpix=lsst.geom.Point2D(100, 75), 

43 crval=lsst.geom.SpherePoint(30.0, -45.0, lsst.geom.degrees), 

44 cdMatrix=afwGeom.makeCdMatrix(scale=3.0*lsst.geom.arcseconds, 

45 orientation=30*lsst.geom.degrees), 

46 ) 

47 

48 

49class AstFrameSetTestCase(lsst.utils.tests.TestCase): 

50 """Tests for the SkyWcs to pyast FrameSet conversion.""" 

51 

52 def testRoundTrip(self): 

53 """The pyast FrameSet must reproduce pixelToSky exactly.""" 

54 from lsst.display.matplotlib.wcsAxes import astFrameSetFromWcs 

55 

56 wcs = makeTestWcs() 

57 frameSet = astFrameSetFromWcs(wcs) 

58 for x, y in [(0.0, 0.0), (100.0, 75.0), (199.5, 149.5), (-20.0, 300.0)]: 

59 sky = wcs.pixelToSky(x, y) 

60 result = frameSet.tran([[x], [y]]) 

61 self.assertEqual(result[0][0], sky[0].asRadians()) 

62 self.assertEqual(result[1][0], sky[1].asRadians()) 

63 

64 

65def makeWcsAxes(**kwargs): 

66 """Create an Agg figure with pixel-coordinate axes and a manager. 

67 

68 Returns the (axes, manager) pair. 

69 """ 

70 from lsst.display.matplotlib.wcsAxes import WcsAxesManager, astFrameSetFromWcs 

71 

72 fig = matplotlib.figure.Figure(figsize=(8, 6)) 

73 FigureCanvasAgg(fig) 

74 ax = fig.add_subplot(111) 

75 ax.set_xlim(-0.5, 199.5) 

76 ax.set_ylim(-0.5, 149.5) 

77 wcs = makeTestWcs() 

78 manager = WcsAxesManager(ax, astFrameSetFromWcs(wcs), **kwargs) 

79 return ax, manager 

80 

81 

82class WcsAxesManagerTestCase(lsst.utils.tests.TestCase): 

83 """Tests for WcsAxesManager drawing and artist lifecycle.""" 

84 

85 def testDrawCreatesArtists(self): 

86 ax, manager = makeWcsAxes() 

87 self.assertGreater(len(manager.artists), 0) 

88 # All tracked artists really are in the axes. 

89 axesArtists = set(ax.lines) | set(ax.texts) 

90 for artist in manager.artists: 

91 self.assertIn(artist, axesArtists) 

92 # Sky axis labels come from the AST SkyFrame. 

93 texts = [a.get_text() for a in manager.artists 

94 if isinstance(a, matplotlib.text.Text)] 

95 self.assertIn("Right ascension", texts) 

96 self.assertIn("Declination", texts) 

97 

98 def testFurnitureHidden(self): 

99 ax, manager = makeWcsAxes() 

100 self.assertFalse(ax.xaxis.get_visible()) 

101 self.assertFalse(ax.yaxis.get_visible()) 

102 for spine in ax.spines.values(): 

103 self.assertFalse(spine.get_visible()) 

104 

105 def testTextNotClipped(self): 

106 ax, manager = makeWcsAxes() 

107 for artist in manager.artists: 

108 if isinstance(artist, matplotlib.text.Text): 

109 self.assertFalse(artist.get_clip_on()) 

110 

111 def testDecimalLabelsByDefault(self): 

112 ax, manager = makeWcsAxes() 

113 tickLabels = [a.get_text() for a in manager.artists 

114 if isinstance(a, matplotlib.text.Text) and 

115 a.get_text() not in ("Right ascension", "Declination")] 

116 self.assertGreater(len(tickLabels), 0) 

117 for label in tickLabels: 

118 self.assertNotIn(":", label) 

119 # AST chooses the precision; the labels must be decimal degrees 

120 # and adjacent labels must be distinct. 

121 self.assertTrue(any("." in label for label in tickLabels)) 

122 self.assertEqual(len(tickLabels), len(set(tickLabels))) 

123 

124 def testSexagesimalLabels(self): 

125 ax, manager = makeWcsAxes(useSexagesimal=True) 

126 tickLabels = [a.get_text() for a in manager.artists 

127 if isinstance(a, matplotlib.text.Text)] 

128 self.assertTrue(any(":" in label for label in tickLabels)) 

129 

130 def testRedrawOnLimitChange(self): 

131 ax, manager = makeWcsAxes() 

132 oldArtists = list(manager.artists) 

133 ax.set_xlim(50, 150) 

134 ax.set_ylim(40, 110) 

135 self.assertGreater(len(manager.artists), 0) 

136 self.assertNotEqual(manager.artists, oldArtists) 

137 # The old artists must be gone from the axes. 

138 axesArtists = set(ax.lines) | set(ax.texts) 

139 for artist in oldArtists: 

140 self.assertNotIn(artist, axesArtists) 

141 

142 def testDebouncedRedraw(self): 

143 """With a working timer, drag events coalesce into one redraw.""" 

144 from matplotlib.backend_bases import TimerBase 

145 

146 starts = [] 

147 

148 class FakeTimer(TimerBase): 

149 def _timer_start(self): 

150 starts.append(self) 

151 

152 def _timer_stop(self): 

153 pass 

154 

155 ax, manager = makeWcsAxes() 

156 canvas = ax.get_figure().canvas 

157 canvas.new_timer = lambda interval=None: FakeTimer(interval=interval) 

158 

159 oldArtists = list(manager.artists) 

160 for i in range(5): 

161 ax.set_xlim(-0.5 + i, 199.5 + i) 

162 ax.set_ylim(-0.5 + i, 149.5 + i) 

163 # No redraw yet: the debounce timer is pending. 

164 self.assertEqual(manager.artists, oldArtists) 

165 self.assertGreater(len(starts), 0) 

166 # Fire the pending timer: exactly one rebuild for the new view. 

167 manager._redrawTimer._on_timer() 

168 self.assertNotEqual(manager.artists, oldArtists) 

169 axesArtists = set(ax.lines) | set(ax.texts) 

170 for artist in oldArtists: 

171 self.assertNotIn(artist, axesArtists) 

172 

173 def testNoRedrawAfterRemove(self): 

174 ax, manager = makeWcsAxes() 

175 manager.remove() 

176 ax.set_xlim(50, 150) 

177 self.assertEqual(manager.artists, []) 

178 self.assertEqual(len(ax.lines), 0) 

179 

180 def testSetSexagesimal(self): 

181 ax, manager = makeWcsAxes() 

182 

183 def tickLabels(): 

184 return [a.get_text() for a in manager.artists 

185 if isinstance(a, matplotlib.text.Text)] 

186 

187 self.assertFalse(any(":" in label for label in tickLabels())) 

188 manager.setSexagesimal(True) 

189 self.assertTrue(any(":" in label for label in tickLabels())) 

190 manager.setSexagesimal(False) 

191 self.assertFalse(any(":" in label for label in tickLabels())) 

192 

193 def testConstructorFailureRestoresFurniture(self): 

194 from lsst.display.matplotlib.wcsAxes import WcsAxesManager, astFrameSetFromWcs 

195 

196 fig = matplotlib.figure.Figure(figsize=(8, 6)) 

197 FigureCanvasAgg(fig) 

198 ax = fig.add_subplot(111) 

199 ax.set_xlim(-0.5, 199.5) 

200 ax.set_ylim(-0.5, 149.5) 

201 with self.assertRaises(Exception): 

202 WcsAxesManager(ax, astFrameSetFromWcs(makeTestWcs()), 

203 extraOptions="NoSuchAttr=1") 

204 self.assertTrue(ax.xaxis.get_visible()) 

205 self.assertTrue(ax.yaxis.get_visible()) 

206 for spine in ax.spines.values(): 

207 self.assertTrue(spine.get_visible()) 

208 self.assertEqual(len(ax.lines) + len(ax.texts), 0) 

209 

210 def testRemove(self): 

211 ax, manager = makeWcsAxes() 

212 manager.remove() 

213 self.assertEqual(manager.artists, []) 

214 self.assertEqual(len(ax.lines), 0) 

215 self.assertEqual(len(ax.texts), 0) 

216 self.assertTrue(ax.xaxis.get_visible()) 

217 self.assertTrue(ax.yaxis.get_visible()) 

218 for spine in ax.spines.values(): 

219 self.assertTrue(spine.get_visible()) 

220 

221 

222class WcsAxesDisplayTestCase(lsst.utils.tests.TestCase): 

223 """Tests for WCS axes at the afwDisplay level.""" 

224 

225 frame = 100 # keep test figures separate from any other test 

226 

227 def setUp(self): 

228 afwDisplay.setDefaultBackend("matplotlib") 

229 # A distinct frame per test so each test gets a fresh figure. 

230 WcsAxesDisplayTestCase.frame += 1 

231 self.frame = WcsAxesDisplayTestCase.frame 

232 

233 def tearDown(self): 

234 afwDisplay.delAllDisplays() 

235 

236 @staticmethod 

237 def makeExposure(): 

238 exposure = afwImage.ExposureF(200, 150) 

239 exposure.image.array[:] = 1.0 

240 exposure.setWcs(makeTestWcs()) 

241 return exposure 

242 

243 def testWcsAxesByDefault(self): 

244 display = afwDisplay.Display(frame=self.frame) 

245 display.mtv(self.makeExposure()) 

246 impl = display._impl 

247 ax = impl._figure.gca() 

248 self.assertIn(ax, impl._wcsAxesManagers) 

249 self.assertGreater(len(impl._wcsAxesManagers[ax].artists), 0) 

250 self.assertFalse(ax.xaxis.get_visible()) 

251 

252 def testUseWcsAxesFalse(self): 

253 display = afwDisplay.Display(frame=self.frame, useWcsAxes=False) 

254 display.mtv(self.makeExposure()) 

255 impl = display._impl 

256 ax = impl._figure.gca() 

257 self.assertEqual(impl._wcsAxesManagers, {}) 

258 self.assertTrue(ax.xaxis.get_visible()) 

259 

260 def testNoWcsMeansPixelAxes(self): 

261 display = afwDisplay.Display(frame=self.frame) 

262 display.mtv(afwImage.ImageF(200, 150)) 

263 impl = display._impl 

264 ax = impl._figure.gca() 

265 self.assertEqual(impl._wcsAxesManagers, {}) 

266 self.assertTrue(ax.xaxis.get_visible()) 

267 

268 def testWcsGridOption(self): 

269 display = afwDisplay.Display(frame=self.frame, wcsGrid=True) 

270 display.mtv(self.makeExposure()) 

271 impl = display._impl 

272 ax = impl._figure.gca() 

273 manager = impl._wcsAxesManagers[ax] 

274 self.assertIn("Grid=1", manager._plotOptions()) 

275 

276 def testZoomRedraws(self): 

277 display = afwDisplay.Display(frame=self.frame) 

278 display.mtv(self.makeExposure()) 

279 impl = display._impl 

280 ax = impl._figure.gca() 

281 manager = impl._wcsAxesManagers[ax] 

282 oldArtists = list(manager.artists) 

283 display.zoom(4) 

284 display.pan(100, 75) 

285 self.assertGreater(len(manager.artists), 0) 

286 self.assertNotEqual(manager.artists, oldArtists) 

287 

288 def testErasePreservesWcsAxes(self): 

289 display = afwDisplay.Display(frame=self.frame) 

290 display.mtv(self.makeExposure()) 

291 impl = display._impl 

292 ax = impl._figure.gca() 

293 manager = impl._wcsAxesManagers[ax] 

294 nAstLines = sum(1 for a in manager.artists if a in set(ax.lines)) 

295 display.dot("+", 100, 75) 

296 self.assertGreater(len(ax.lines), nAstLines) 

297 display.erase() 

298 self.assertEqual(len(ax.lines), nAstLines) 

299 axesArtists = set(ax.lines) | set(ax.texts) 

300 for artist in manager.artists: 

301 self.assertIn(artist, axesArtists) 

302 

303 def testFallbackRestoresPixelAxes(self): 

304 display = afwDisplay.Display(frame=self.frame, astPlotOptions="NoSuchAttr=1") 

305 display.mtv(self.makeExposure()) 

306 impl = display._impl 

307 ax = impl._figure.gca() 

308 self.assertEqual(impl._wcsAxesManagers, {}) 

309 self.assertTrue(ax.xaxis.get_visible()) 

310 for spine in ax.spines.values(): 

311 self.assertTrue(spine.get_visible()) 

312 

313 def testEraseRemovesPatches(self): 

314 display = afwDisplay.Display(frame=self.frame) 

315 display.mtv(self.makeExposure()) 

316 ax = display._impl._figure.gca() 

317 display.dot("o", 100, 75, size=10) 

318 self.assertEqual(len(ax.patches), 1) 

319 display.erase() 

320 self.assertEqual(len(ax.patches), 0) 

321 

322 def testRepeatedMtvDoesNotLeak(self): 

323 display = afwDisplay.Display(frame=self.frame) 

324 display.mtv(self.makeExposure()) 

325 impl = display._impl 

326 ax = impl._figure.gca() 

327 nTexts = len(ax.texts) 

328 display.mtv(self.makeExposure()) 

329 ax = impl._figure.gca() 

330 self.assertEqual(len(impl._wcsAxesManagers), 1) 

331 self.assertEqual(len(ax.texts), nTexts) 

332 

333 def testUseSexagesimalRedraws(self): 

334 display = afwDisplay.Display(frame=self.frame) 

335 display.mtv(self.makeExposure()) 

336 impl = display._impl 

337 ax = impl._figure.gca() 

338 manager = impl._wcsAxesManagers[ax] 

339 

340 def hasColon(): 

341 return any(":" in a.get_text() for a in manager.artists 

342 if isinstance(a, matplotlib.text.Text)) 

343 

344 self.assertFalse(hasColon()) 

345 display.useSexagesimal(True) 

346 self.assertTrue(hasColon()) 

347 

348 def testCloseReleasesManagers(self): 

349 display = afwDisplay.Display(frame=self.frame) 

350 display.mtv(self.makeExposure()) 

351 impl = display._impl 

352 impl._close() 

353 self.assertEqual(impl._wcsAxesManagers, {}) 

354 

355 

356class DisplayMatplotlibTestCase(unittest.TestCase): 

357 

358 def setUp(self): 

359 pass 

360 

361 def tearDown(self): 

362 pass 

363 

364 def testSetAfwDisplayBackend(self): 

365 """Set the default backend to this package""" 

366 afwDisplay.setDefaultBackend("matplotlib") 

367 

368 def testSetImageColormap(self): 

369 """This is a stand-in for an eventual testcase for changing image 

370 colormap. 

371 

372 The basic outline should look something like: 

373 

374 afwDisplay.setDefaultBackend("matplotlib") 

375 display = afwDisplay.Display() 

376 display.setImageColormap('viridis') 

377 assert display._image_colormap == 'viridis' 

378 """ 

379 pass 

380 

381 

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

383 pass 

384 

385 

386def setup_module(module): 

387 lsst.utils.tests.init() 

388 

389 

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

391 lsst.utils.tests.init() 

392 unittest.main()