Skip to content

Commit bebc2de

Browse files
committed
fix wl_to_xyz cmf clipping; add spectral tests; bump 0.9.0
1 parent 1767f9e commit bebc2de

6 files changed

Lines changed: 66 additions & 3 deletions

File tree

28 Bytes
Loading

doc/source/about_release_notes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Release notes
22
=============
33

4+
.. rubric:: v 0.9.0 a - Sep. 2026
5+
6+
* Fixed :meth:`.Spectral.wl_to_xyz` clipping color-matching values to [0, 1]. CIE 1931 CMFs legitimately exceed 1.0 (z-bar peaks at 1.78, x-bar at 1.06); clipping distorted chromaticity, worst in blue-violet (e.g. 450nm x shifted 0.157 to 0.245)
7+
* Added spectral regression tests
8+
* Re-rendered visible spectrum sweep example with corrected values
9+
410
.. rubric:: v 0.8.0 a - May 2025
511

612
* Reworked util module

doc/source/html/analyzer.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
};
7979

8080
const matIdentity = new THREE.Matrix3().set(
81-
1.0, 0.0, 1.0,
81+
1.0, 0.0, 0.0,
8282
0.0, 1.0, 0.0,
8383
0.0, 0.0, 1.0
8484
);

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "tinycio"
3-
version = "0.8.2"
3+
version = "0.9.0"
44
authors = [
55
{ name="Sam Izdat", email="ghsamizdat@gmail.com" },
66
]

src/tinycio/spectral.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def wl_to_xyz(cls, wl:float) -> Float3:
154154
t = (wl - wl_values[closest_idx]) / (wl_values[second_closest_idx] - wl_values[closest_idx])
155155
interpolated_xyz = closest_xyz + t * (second_closest_xyz - closest_xyz)
156156

157-
return Float3(interpolated_xyz).clip(0., 1.)
157+
return Float3(interpolated_xyz)
158158

159159
@classmethod
160160
def wl_to_srgb_linear(cls, wl:float, normalize:bool=False, lum_scale:float=0.25) -> Float3:

tests/test_spectral.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import unittest
2+
import numpy as np
3+
from tinycio import Spectral
4+
5+
6+
class TestSpectral(unittest.TestCase):
7+
8+
def setUp(self):
9+
pass
10+
11+
def tearDown(self):
12+
pass
13+
14+
def test_cm_table_exceeds_one(self):
15+
# CIE 1931 2-degree CMFs legitimately exceed 1.0;
16+
# documents why wl_to_xyz must not clip to [0, 1].
17+
tab = Spectral.cm_table()
18+
self.assertGreater(float(tab[:, 0].max()), 1.0) # x-bar peaks at 1.0622 (600nm)
19+
self.assertGreater(float(tab[:, 2].max()), 1.5) # z-bar peaks at 1.7826 (445nm)
20+
self.assertAlmostEqual(float(tab[:, 0].max()), 1.0622, places=4)
21+
self.assertAlmostEqual(float(tab[:, 2].max()), 1.7826, places=4)
22+
23+
def test_wl_to_xyz_no_clip(self):
24+
# Regression test: wl_to_xyz must return raw CMF values,
25+
# not clipped to [0, 1]. Spot-check wavelengths where Z/X exceed 1.
26+
for wl, expected in [(445., (0.3481, 0.0298, 1.7826)),
27+
(450., (0.3362, 0.0380, 1.7721)),
28+
(600., (1.0622, 0.6310, 0.0008))]:
29+
np.testing.assert_allclose(np.asarray(Spectral.wl_to_xyz(wl)),
30+
np.asarray(expected), atol=1e-4)
31+
32+
def test_wl_to_xyz_chromaticity_450(self):
33+
# Clipping Z 1.7721 -> 1.0 shifts x 0.1566 -> 0.2447; guard the ratio.
34+
x, y, z = [float(v) for v in Spectral.wl_to_xyz(450.)]
35+
self.assertAlmostEqual(x / (x + y + z), 0.1566, places=4)
36+
37+
def test_wl_to_xyz_matches_table_on_grid(self):
38+
# Every 5nm grid point must round-trip the embedded table exactly.
39+
tab = Spectral.cm_table()
40+
for i in range(tab.shape[0]):
41+
wl = 380. + 5. * i
42+
if wl >= 780:
43+
break
44+
np.testing.assert_allclose(np.asarray(Spectral.wl_to_xyz(wl)),
45+
np.asarray(tab[i]), atol=1e-6)
46+
47+
def test_wl_to_xyz_interpolation_midpoint(self):
48+
# Off-grid wavelengths linearly interpolate between neighbors.
49+
tab = Spectral.cm_table()
50+
i = int((445. - 380.) / 5.)
51+
expected = 0.5 * (np.asarray(tab[i]) + np.asarray(tab[i + 1]))
52+
np.testing.assert_allclose(np.asarray(Spectral.wl_to_xyz(447.5)),
53+
expected, atol=1e-6)
54+
55+
56+
if __name__ == '__main__':
57+
unittest.main()

0 commit comments

Comments
 (0)