Coverage for pyrc \ tests \ test_color.py: 89%
131 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-13 16:59 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-13 16:59 +0200
1# -------------------------------------------------------------------------------
2# Copyright (C) 2026 Joel Kimmich, Tim Jourdan
3# ------------------------------------------------------------------------------
4# License
5# This file is part of PyRC, distributed under GPL-3.0-or-later.
6# ------------------------------------------------------------------------------
8import unittest
9import numpy as np
10from pyrc.core.visualization.color.color import available, value_to_rgb, make_color_mapper, get_palette, cycle_palette, \
11 CMAPS
14class TestColormap(unittest.TestCase):
16 def _first_of_kind(self, kind: str) -> str | None:
17 """Return the first colormap name of the given kind, or None."""
18 names = available(kind)
19 return names[0] if names else None
21 def test_colormaps_loaded(self):
22 """At least one colormap is discovered and loaded at import time."""
23 names = available()
24 self.assertIsInstance(names, tuple)
25 self.assertGreater(len(names), 0)
27 def test_available_filter_uniform(self):
28 """available('uniform') returns only uniform-kind entries."""
29 names = available("uniform")
30 self.assertTrue(all(CMAPS[n][0] == "uniform" for n in names))
32 def test_available_filter_discrete(self):
33 """available('discrete') returns only discrete-kind entries."""
34 names = available("discrete")
35 self.assertTrue(all(CMAPS[n][0] == "discrete" for n in names))
37 def test_available_filter_explicit(self):
38 """available('explicit') returns only explicit-kind entries."""
39 names = available("explicit")
40 self.assertTrue(all(CMAPS[n][0] == "explicit" for n in names))
42 def test_value_to_rgb_shape_and_range_scalar(self):
43 """Scalar input yields shape (3,) float32 in [0, 1]."""
44 cmap = self._first_of_kind("uniform") or self._first_of_kind("explicit")
45 if cmap is None:
46 self.skipTest("No gradient colormap available")
47 rgb = value_to_rgb(0.5, cmap=cmap)
48 self.assertEqual(rgb.shape, (3,))
49 self.assertEqual(rgb.dtype, np.float32)
50 self.assertTrue(np.all(np.isfinite(rgb)))
51 self.assertTrue(np.all((rgb >= 0.0) & (rgb <= 1.0)))
53 def test_value_to_rgb_shape_vector(self):
54 """Vector input of length N yields shape (N, 3) float32."""
55 cmap = self._first_of_kind("uniform") or self._first_of_kind("explicit")
56 if cmap is None:
57 self.skipTest("No gradient colormap available")
58 v = np.array([0.0, 0.25, 0.5, 0.75, 1.0], dtype=np.float32)
59 rgb = value_to_rgb(v, cmap=cmap)
60 self.assertEqual(rgb.shape, (5, 3))
61 self.assertEqual(rgb.dtype, np.float32)
62 self.assertTrue(np.all(np.isfinite(rgb)))
64 def test_clipping(self):
65 """Values outside [0, 1] are clipped to the boundary colours."""
66 cmap = self._first_of_kind("uniform") or self._first_of_kind("explicit")
67 if cmap is None:
68 self.skipTest("No gradient colormap available")
69 self.assertTrue(np.allclose(value_to_rgb(-123.0, cmap=cmap), value_to_rgb(0.0, cmap=cmap)))
70 self.assertTrue(np.allclose(value_to_rgb(999.0, cmap=cmap), value_to_rgb(1.0, cmap=cmap)))
72 def test_make_mapper(self):
73 """make_color_mapper returns a callable producing correct output shape."""
74 cmap = self._first_of_kind("uniform") or self._first_of_kind("explicit")
75 if cmap is None:
76 self.skipTest("No gradient colormap available")
77 f = make_color_mapper(cmap)
78 self.assertEqual(f(np.array([0.1, 0.2], dtype=np.float32)).shape, (2, 3))
80 def test_deterministic(self):
81 """Identical inputs always produce identical RGB outputs."""
82 cmap = self._first_of_kind("uniform") or self._first_of_kind("explicit")
83 if cmap is None:
84 self.skipTest("No gradient colormap available")
85 v = np.linspace(0, 1, 11, dtype=np.float32)
86 self.assertTrue(np.array_equal(value_to_rgb(v, cmap=cmap), value_to_rgb(v, cmap=cmap)))
88 def test_endpoints_uniform(self):
89 """v=0 and v=1 map exactly to the first and last table rows."""
90 cmap = self._first_of_kind("uniform")
91 if cmap is None:
92 self.skipTest("No uniform colormap available")
93 rgb_table = CMAPS[cmap][1]
94 self.assertTrue(np.allclose(value_to_rgb(0.0, cmap=cmap), rgb_table[0]))
95 self.assertTrue(np.allclose(value_to_rgb(1.0, cmap=cmap), rgb_table[-1]))
97 def test_unknown_cmap_raises(self):
98 """Requesting an unknown colormap name raises KeyError."""
99 with self.assertRaises(KeyError):
100 value_to_rgb(0.5, cmap="__does_not_exist__")
102 def test_get_palette_shape_and_range(self):
103 """Discrete palette is a finite (N, 3) float32 array in [0, 1]."""
104 cmap = self._first_of_kind("discrete")
105 if cmap is None:
106 self.skipTest("No discrete palette available")
107 palette = get_palette(cmap)
108 self.assertIsInstance(palette, np.ndarray)
109 self.assertEqual(palette.ndim, 2)
110 self.assertEqual(palette.shape[1], 3)
111 self.assertEqual(palette.dtype, np.float32)
112 self.assertTrue(np.all((palette >= 0.0) & (palette <= 1.0)))
114 def test_get_palette_raises_for_gradient(self):
115 """get_palette raises TypeError when called on a gradient colormap."""
116 cmap = self._first_of_kind("uniform") or self._first_of_kind("explicit")
117 if cmap is None:
118 self.skipTest("No gradient colormap available")
119 with self.assertRaises(TypeError):
120 get_palette(cmap)
122 def test_get_palette_raises_unknown(self):
123 """get_palette raises KeyError for an unknown colormap name."""
124 with self.assertRaises(KeyError):
125 get_palette("__does_not_exist__")
127 def test_cycle_palette_discrete_yields_correct_colors(self):
128 """Generator yields palette swatches in file order."""
129 cmap = self._first_of_kind("discrete")
130 if cmap is None:
131 self.skipTest("No discrete palette available")
132 palette = get_palette(cmap)
133 gen = cycle_palette(cmap)
134 for expected in palette:
135 self.assertTrue(np.array_equal(next(gen), expected))
137 def test_cycle_palette_discrete_wraps(self):
138 """After exhausting all swatches the generator restarts from the first."""
139 cmap = self._first_of_kind("discrete")
140 if cmap is None:
141 self.skipTest("No discrete palette available")
142 n = get_palette(cmap).shape[0]
143 gen = cycle_palette(cmap)
144 first = next(gen).copy()
145 for _ in range(n - 1):
146 next(gen)
147 self.assertTrue(np.array_equal(next(gen), first))
149 def test_cycle_palette_uniform_yields_correct_colors(self):
150 """Generator yields uniform table rows in order."""
151 cmap = self._first_of_kind("uniform")
152 if cmap is None:
153 self.skipTest("No uniform colormap available")
154 rgb_table = CMAPS[cmap][1]
155 gen = cycle_palette(cmap)
156 for expected in rgb_table:
157 self.assertTrue(np.array_equal(next(gen), expected))
159 def test_cycle_palette_uniform_wraps(self):
160 """After exhausting all table rows the generator restarts from the first."""
161 cmap = self._first_of_kind("uniform")
162 if cmap is None:
163 self.skipTest("No uniform colormap available")
164 n = CMAPS[cmap][1].shape[0]
165 gen = cycle_palette(cmap)
166 first = next(gen).copy()
167 for _ in range(n - 1):
168 next(gen)
169 self.assertTrue(np.array_equal(next(gen), first))
171 def test_cycle_palette_yields_float32(self):
172 """Each value yielded by the generator has dtype float32."""
173 cmap = self._first_of_kind("discrete") or self._first_of_kind("uniform")
174 if cmap is None:
175 self.skipTest("No colormap available")
176 self.assertEqual(next(cycle_palette(cmap)).dtype, np.float32)
178 def test_cycle_palette_unknown_raises(self):
179 """cycle_palette raises KeyError for an unknown colormap name."""
180 with self.assertRaises(KeyError):
181 next(cycle_palette("__does_not_exist__"))
184if __name__ == "__main__":
185 unittest.main()