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

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# ------------------------------------------------------------------------------ 

7 

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 

12 

13 

14class TestColormap(unittest.TestCase): 

15 

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 

20 

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) 

26 

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)) 

31 

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)) 

36 

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)) 

41 

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))) 

52 

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))) 

63 

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))) 

71 

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)) 

79 

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))) 

87 

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])) 

96 

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__") 

101 

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))) 

113 

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) 

121 

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__") 

126 

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)) 

136 

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)) 

148 

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)) 

158 

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)) 

170 

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) 

177 

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__")) 

182 

183 

184if __name__ == "__main__": 

185 unittest.main()