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
« 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
26import matplotlib
27matplotlib.use("Agg")
28import matplotlib.figure # noqa: E402
29import matplotlib.text # noqa: E402
30from matplotlib.backends.backend_agg import FigureCanvasAgg # noqa: E402
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
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 )
49class AstFrameSetTestCase(lsst.utils.tests.TestCase):
50 """Tests for the SkyWcs to pyast FrameSet conversion."""
52 def testRoundTrip(self):
53 """The pyast FrameSet must reproduce pixelToSky exactly."""
54 from lsst.display.matplotlib.wcsAxes import astFrameSetFromWcs
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())
65def makeWcsAxes(**kwargs):
66 """Create an Agg figure with pixel-coordinate axes and a manager.
68 Returns the (axes, manager) pair.
69 """
70 from lsst.display.matplotlib.wcsAxes import WcsAxesManager, astFrameSetFromWcs
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
82class WcsAxesManagerTestCase(lsst.utils.tests.TestCase):
83 """Tests for WcsAxesManager drawing and artist lifecycle."""
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)
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())
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())
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)))
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))
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)
142 def testDebouncedRedraw(self):
143 """With a working timer, drag events coalesce into one redraw."""
144 from matplotlib.backend_bases import TimerBase
146 starts = []
148 class FakeTimer(TimerBase):
149 def _timer_start(self):
150 starts.append(self)
152 def _timer_stop(self):
153 pass
155 ax, manager = makeWcsAxes()
156 canvas = ax.get_figure().canvas
157 canvas.new_timer = lambda interval=None: FakeTimer(interval=interval)
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)
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)
180 def testSetSexagesimal(self):
181 ax, manager = makeWcsAxes()
183 def tickLabels():
184 return [a.get_text() for a in manager.artists
185 if isinstance(a, matplotlib.text.Text)]
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()))
193 def testConstructorFailureRestoresFurniture(self):
194 from lsst.display.matplotlib.wcsAxes import WcsAxesManager, astFrameSetFromWcs
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)
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())
222class WcsAxesDisplayTestCase(lsst.utils.tests.TestCase):
223 """Tests for WCS axes at the afwDisplay level."""
225 frame = 100 # keep test figures separate from any other test
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
233 def tearDown(self):
234 afwDisplay.delAllDisplays()
236 @staticmethod
237 def makeExposure():
238 exposure = afwImage.ExposureF(200, 150)
239 exposure.image.array[:] = 1.0
240 exposure.setWcs(makeTestWcs())
241 return exposure
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())
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())
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())
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())
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)
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)
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())
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)
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)
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]
340 def hasColon():
341 return any(":" in a.get_text() for a in manager.artists
342 if isinstance(a, matplotlib.text.Text))
344 self.assertFalse(hasColon())
345 display.useSexagesimal(True)
346 self.assertTrue(hasColon())
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, {})
356class DisplayMatplotlibTestCase(unittest.TestCase):
358 def setUp(self):
359 pass
361 def tearDown(self):
362 pass
364 def testSetAfwDisplayBackend(self):
365 """Set the default backend to this package"""
366 afwDisplay.setDefaultBackend("matplotlib")
368 def testSetImageColormap(self):
369 """This is a stand-in for an eventual testcase for changing image
370 colormap.
372 The basic outline should look something like:
374 afwDisplay.setDefaultBackend("matplotlib")
375 display = afwDisplay.Display()
376 display.setImageColormap('viridis')
377 assert display._image_colormap == 'viridis'
378 """
379 pass
382class TestMemory(lsst.utils.tests.MemoryTestCase):
383 pass
386def setup_module(module):
387 lsst.utils.tests.init()
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()