Skip to main content

colr_types/model/
spectral.rs

1//! Spectral color space models and traits.
2
3#![allow(clippy::excessive_precision)]
4
5use core::marker::PhantomData;
6
7use crate::BackingStore;
8use crate::observer::{Cie1931, StandardObserver};
9
10/// Identifies the physical quantity encoded by a spectral color value.
11///
12/// The four variants are mutually exclusive at the type level: a `Spectral<N, G, K>`
13/// can only carry one kind. The blanket traits [`Radiance`], [`Reflectance`],
14/// [`Transmittance`], and[`Bispectral`] are derived from this via blanket impls
15/// so bounds can name the concept rather than the marker.
16///
17/// Storage note: [`IsBispectral`] is the odd one out. The other three kinds store
18/// N independent spectral samples in `[f32; N]`. A bispectral measurement is an
19/// N×N matrix (or a reduced form of one) and cannot be meaningfully carried in
20/// `[f32; N]`. For that reason `Spectral<N, G, IsBispectral>` deliberately does
21/// not implement `BackingStore<[f32; N]>`. See [`IsBispectral`] for details.
22pub trait SpectralKind: 'static {}
23
24/// Spectral kind marker for spectral radiance, unbounded above.
25///
26/// Used for emissive spectra: light sources, illuminants, display primaries.
27/// Converting to XYZ is a direct dot product of the SPD with the CMFs.
28#[derive(Debug, Clone, Copy)]
29pub struct IsRadiance;
30
31/// Spectral kind marker for spectral reflectance in [0, 1].
32///
33/// Encodes the fraction of incident light reflected at each wavelength.
34/// Converting to XYZ requires an illuminant SPD; the dot product is
35/// (illuminant * reflectance) dot CMF, not just reflectance dot CMF.
36#[derive(Debug, Clone, Copy)]
37pub struct IsReflectance;
38
39/// Spectral kind marker for spectral transmittance in[0, 1].
40///
41/// Encodes the fraction of incident light transmitted at each wavelength.
42/// Physically identical in structure to reflectance; distinct at the type
43/// level because the two quantities are not interchangeable in a pipeline.
44#[derive(Debug, Clone, Copy)]
45pub struct IsTransmittance;
46
47/// Spectral kind marker for bispectral (fluorescent) reflectance.
48///
49/// A bispectral measurement captures re-emission: how much energy absorbed
50/// at one wavelength is re-emitted at another. The canonical representation
51/// is the Donaldson matrix, an N-by-N array where entry \[i\]\[j\] is the
52/// bispectral reflectance factor from band i to band j. Stokes' law
53/// (emitted wavelength >= absorbed wavelength) makes the upper triangle zero,
54/// but the full matrix is still N-by-N, not the N-by-1 vector the other kinds use.
55///
56/// Other compact representations exist (factored excitation/emission spectra,
57/// single-peak Gaussian approximations, lower-triangular packing) but they
58/// all have different storage shapes. Bispectral storage is intentionally left
59/// to a future explicit design; this marker is present to close the kind
60/// taxonomy and allow bounds to express "not bispectral".
61#[derive(Debug, Clone, Copy)]
62pub struct IsBispectral;
63
64impl SpectralKind for IsRadiance {}
65impl SpectralKind for IsReflectance {}
66impl SpectralKind for IsTransmittance {}
67impl SpectralKind for IsBispectral {}
68
69/// Implemented by any spectral model that declares a physical kind.
70///
71/// The blanket alias traits below (`Radiance`, `Reflectance`, etc.) let
72/// bounds name the concept without spelling out the associated type.
73pub trait SpectralSpace: 'static {
74    /// The physical quantity this space encodes.
75    type Kind: SpectralKind;
76}
77
78/// Blanket alias: any spectral space whose kind is[`IsRadiance`].
79pub trait Radiance: SpectralSpace<Kind = IsRadiance> {}
80impl<M: SpectralSpace<Kind = IsRadiance>> Radiance for M {}
81
82/// Blanket alias: any spectral space whose kind is [`IsReflectance`].
83pub trait Reflectance: SpectralSpace<Kind = IsReflectance> {}
84impl<M: SpectralSpace<Kind = IsReflectance>> Reflectance for M {}
85
86/// Blanket alias: any spectral space whose kind is [`IsTransmittance`].
87pub trait Transmittance: SpectralSpace<Kind = IsTransmittance> {}
88impl<M: SpectralSpace<Kind = IsTransmittance>> Transmittance for M {}
89
90/// Blanket alias: any spectral space whose kind is [`IsBispectral`].
91pub trait Bispectral: SpectralSpace<Kind = IsBispectral> {}
92impl<M: SpectralSpace<Kind = IsBispectral>> Bispectral for M {}
93
94/// A physical wavelength axis.
95///
96/// # Design notes
97///
98/// **BANDS is on the trait, not the implementing type.** Ideally BANDS would
99/// be an associated constant so that `G::BANDS` could be used as a const
100/// generic argument and the band count would be fully implied by the grid.
101/// Current Rust does not allow associated constants to appear as const generic
102/// arguments in type-level positions (e.g. `[f32; G::BANDS]`), so the band
103/// count is expressed as a const generic on the trait instead. Each concrete
104/// grid type is expected to implement this trait for exactly one value of BANDS
105/// (the one consistent with its physical definition), but the compiler cannot
106/// enforce that invariant today. Type aliases such as [`Spectral41Radiance`]
107/// exist precisely to hide the redundant BANDS argument from call sites.
108///
109/// **Separation of Physics and Psychophysics.** This trait represents only the
110/// physical wavelength axis. Human perception is layered on top via the
111/// [`ColorMatchingFunctions`] trait, which maps standard observers to grids.
112/// This allows a single physical spectrum to be integrated into XYZ under
113/// multiple different observers without changing the underlying spectral type.
114pub trait WavelengthGrid<const BANDS: usize>: 'static {
115    /// First wavelength in nanometers.
116    const START_NM: f32;
117    /// Wavelength interval in nanometers.
118    const STEP_NM: f32;
119}
120
121/// Color matching functions for a specific observer evaluated on a specific grid.
122pub trait ColorMatchingFunctions<const BANDS: usize, O: StandardObserver>:
123    WavelengthGrid<BANDS>
124{
125    /// x-bar(lambda) for `O` sampled at each band on this grid.
126    const CMF_X: &'static [f32; BANDS];
127    /// y-bar(lambda) for `O` sampled at each band on this grid.
128    const CMF_Y: &'static [f32; BANDS];
129    /// z-bar(lambda) for `O` sampled at each band on this grid.
130    const CMF_Z: &'static [f32; BANDS];
131}
132
133/// A spectral color value on grid `G` encoding physical quantity `K`.
134///
135/// The storage type is `[f32; BANDS]`. BANDS must match the single value for
136/// which `G` implements `WavelengthGrid`; the bound `G: WavelengthGrid<BANDS>`
137/// enforces consistency. Use the provided type aliases rather than naming
138/// `Spectral` directly.
139///
140/// `Spectral<N, G, IsBispectral>` does **not** implement `BackingStore<[f32; N]>`
141/// because bispectral data is N-by-N, not N-by-1. See [`IsBispectral`] for context.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
143pub struct Spectral<const BANDS: usize, G, K>(PhantomData<(G, K)>);
144
145impl<const BANDS: usize, G, K> SpectralSpace for Spectral<BANDS, G, K>
146where
147    G: WavelengthGrid<BANDS>,
148    K: SpectralKind,
149{
150    type Kind = K;
151}
152
153impl<const BANDS: usize, G> BackingStore<[f32; BANDS]> for Spectral<BANDS, G, IsRadiance> where
154    G: WavelengthGrid<BANDS>
155{
156}
157
158impl<const BANDS: usize, G> BackingStore<[f32; BANDS]> for Spectral<BANDS, G, IsReflectance> where
159    G: WavelengthGrid<BANDS>
160{
161}
162
163impl<const BANDS: usize, G> BackingStore<[f32; BANDS]> for Spectral<BANDS, G, IsTransmittance> where
164    G: WavelengthGrid<BANDS>
165{
166}
167
168/// CIE 10 nm spectral grid, 380–780 nm (41 bands).
169///
170/// Covers the full practical visible range at the coarser 10 nm step.
171/// Suitable for colorimetry where 10 nm resolution is acceptable and
172/// compact storage is preferred.
173#[derive(Debug, Clone, Copy)]
174pub struct Grid380_780_10nm;
175
176impl WavelengthGrid<41> for Grid380_780_10nm {
177    const START_NM: f32 = 380.0;
178    const STEP_NM: f32 = 10.0;
179}
180
181impl ColorMatchingFunctions<41, Cie1931> for Grid380_780_10nm {
182    #[rustfmt::skip]
183    const CMF_X: &'static [f32; 41] = &[
184        0.001368, 0.004243, 0.014310, 0.043510, 0.134380,
185        0.283900, 0.348280, 0.336200, 0.290800, 0.195360,
186        0.095640, 0.032010, 0.004900, 0.009300, 0.063270,
187        0.165500, 0.290400, 0.433450, 0.594500, 0.762100,
188        0.916300, 1.026300, 1.062200, 1.002600, 0.854450,
189        0.642400, 0.447900, 0.283500, 0.164900, 0.087400,
190        0.046770, 0.022700, 0.011359, 0.005790, 0.002899,
191        0.001440, 0.000690, 0.000332, 0.000166, 0.000083,
192        0.000042,
193    ];
194    #[rustfmt::skip]
195    const CMF_Y: &'static [f32; 41] = &[
196        0.000039, 0.000120, 0.000396, 0.001210, 0.004000,
197        0.011600, 0.023000, 0.038000, 0.060000, 0.090980,
198        0.139020, 0.208020, 0.323000, 0.503000, 0.710000,
199        0.862000, 0.954000, 0.994950, 0.995000, 0.952000,
200        0.870000, 0.757000, 0.631000, 0.503000, 0.381000,
201        0.265000, 0.175000, 0.107000, 0.061000, 0.032000,
202        0.017000, 0.008210, 0.004102, 0.002091, 0.001047,
203        0.000520, 0.000249, 0.000120, 0.000060, 0.000030,
204        0.000015,
205    ];
206    #[rustfmt::skip]
207    const CMF_Z: &'static[f32; 41] = &[
208        0.006450, 0.020050, 0.067850, 0.207400, 0.645600,
209        1.385600, 1.747060, 1.772110, 1.669200, 1.287640,
210        0.812950, 0.465180, 0.272000, 0.158200, 0.078250,
211        0.042160, 0.020300, 0.008750, 0.003900, 0.002100,
212        0.001650, 0.001100, 0.000800, 0.000340, 0.000190,
213        0.000050, 0.000020, 0.000000, 0.000000, 0.000000,
214        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
215        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
216        0.000000,
217    ];
218}
219
220/// CIE 10 nm spectral grid, 380–730 nm (36 bands).
221///
222/// The 36-band grid used in Stam's spectral rendering framework (1999).
223/// Drops the 740–780 nm tail, which contributes negligible energy under
224/// most natural and artificial illuminants.
225#[derive(Debug, Clone, Copy)]
226pub struct Grid380_730_10nm;
227
228impl WavelengthGrid<36> for Grid380_730_10nm {
229    const START_NM: f32 = 380.0;
230    const STEP_NM: f32 = 10.0;
231}
232
233impl ColorMatchingFunctions<36, Cie1931> for Grid380_730_10nm {
234    #[rustfmt::skip]
235    const CMF_X: &'static[f32; 36] = &[
236        0.001368, 0.004243, 0.014310, 0.043510, 0.134380,
237        0.283900, 0.348280, 0.336200, 0.290800, 0.195360,
238        0.095640, 0.032010, 0.004900, 0.009300, 0.063270,
239        0.165500, 0.290400, 0.433450, 0.594500, 0.762100,
240        0.916300, 1.026300, 1.062200, 1.002600, 0.854450,
241        0.642400, 0.447900, 0.283500, 0.164900, 0.087400,
242        0.046770, 0.022700, 0.011359, 0.005790, 0.002899,
243        0.001440,
244    ];
245    #[rustfmt::skip]
246    const CMF_Y: &'static [f32; 36] = &[
247        0.000039, 0.000120, 0.000396, 0.001210, 0.004000,
248        0.011600, 0.023000, 0.038000, 0.060000, 0.090980,
249        0.139020, 0.208020, 0.323000, 0.503000, 0.710000,
250        0.862000, 0.954000, 0.994950, 0.995000, 0.952000,
251        0.870000, 0.757000, 0.631000, 0.503000, 0.381000,
252        0.265000, 0.175000, 0.107000, 0.061000, 0.032000,
253        0.017000, 0.008210, 0.004102, 0.002091, 0.001047,
254        0.000520,
255    ];
256    #[rustfmt::skip]
257    const CMF_Z: &'static [f32; 36] = &[
258        0.006450, 0.020050, 0.067850, 0.207400, 0.645600,
259        1.385600, 1.747060, 1.772110, 1.669200, 1.287640,
260        0.812950, 0.465180, 0.272000, 0.158200, 0.078250,
261        0.042160, 0.020300, 0.008750, 0.003900, 0.002100,
262        0.001650, 0.001100, 0.000800, 0.000340, 0.000190,
263        0.000050, 0.000020, 0.000000, 0.000000, 0.000000,
264        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
265        0.000000,
266    ];
267}
268
269/// CIE 10 nm spectral grid, 400–700 nm (31 bands).
270///
271/// The ICC spectral measurement standard (ISO 13655). Also the basis of
272/// the Munsell atlas and most reflectance spectrophotometer outputs.
273#[derive(Debug, Clone, Copy)]
274pub struct Grid400_700_10nm;
275
276impl WavelengthGrid<31> for Grid400_700_10nm {
277    const START_NM: f32 = 400.0;
278    const STEP_NM: f32 = 10.0;
279}
280
281impl ColorMatchingFunctions<31, Cie1931> for Grid400_700_10nm {
282    #[rustfmt::skip]
283    const CMF_X: &'static [f32; 31] = &[
284        0.014310, 0.043510, 0.134380,
285        0.283900, 0.348280, 0.336200, 0.290800, 0.195360,
286        0.095640, 0.032010, 0.004900, 0.009300, 0.063270,
287        0.165500, 0.290400, 0.433450, 0.594500, 0.762100,
288        0.916300, 1.026300, 1.062200, 1.002600, 0.854450,
289        0.642400, 0.447900, 0.283500, 0.164900, 0.087400,
290        0.046770, 0.022700, 0.011359,
291    ];
292    #[rustfmt::skip]
293    const CMF_Y: &'static [f32; 31] = &[
294        0.000396, 0.001210, 0.004000,
295        0.011600, 0.023000, 0.038000, 0.060000, 0.090980,
296        0.139020, 0.208020, 0.323000, 0.503000, 0.710000,
297        0.862000, 0.954000, 0.994950, 0.995000, 0.952000,
298        0.870000, 0.757000, 0.631000, 0.503000, 0.381000,
299        0.265000, 0.175000, 0.107000, 0.061000, 0.032000,
300        0.017000, 0.008210, 0.004102,
301    ];
302    #[rustfmt::skip]
303    const CMF_Z: &'static [f32; 31] = &[
304        0.067850, 0.207400, 0.645600,
305        1.385600, 1.747060, 1.772110, 1.669200, 1.287640,
306        0.812950, 0.465180, 0.272000, 0.158200, 0.078250,
307        0.042160, 0.020300, 0.008750, 0.003900, 0.002100,
308        0.001650, 0.001100, 0.000800, 0.000340, 0.000190,
309        0.000050, 0.000020, 0.000000, 0.000000, 0.000000,
310        0.000000, 0.000000, 0.000000,
311    ];
312}
313
314/// CIE 5 nm spectral grid, 380–780 nm (81 bands).
315///
316/// The CIE practical standard grid (CIE 015:2018). Preferred when 10 nm
317/// sampling introduces visible interpolation artifacts, particularly in
318/// the blue-violet region where the CMFs change rapidly.
319#[derive(Debug, Clone, Copy)]
320pub struct Grid380_780_5nm;
321
322impl WavelengthGrid<81> for Grid380_780_5nm {
323    const START_NM: f32 = 380.0;
324    const STEP_NM: f32 = 5.0;
325}
326
327impl ColorMatchingFunctions<81, Cie1931> for Grid380_780_5nm {
328    #[rustfmt::skip]
329    const CMF_X: &'static [f32; 81] = &[
330        0.001368, 0.002236, 0.004243, 0.007650, 0.014310,
331        0.023190, 0.043510, 0.077630, 0.134380, 0.214770,
332        0.283900, 0.328500, 0.348280, 0.348060, 0.336200,
333        0.318700, 0.290800, 0.251100, 0.195360, 0.142100,
334        0.095640, 0.057950, 0.032010, 0.014700, 0.004900,
335        0.002400, 0.009300, 0.029100, 0.063270, 0.109600,
336        0.165500, 0.225750, 0.290400, 0.359700, 0.433450,
337        0.512050, 0.594500, 0.678400, 0.762100, 0.842500,
338        0.916300, 0.978600, 1.026300, 1.056700, 1.062200,
339        1.045600, 1.002600, 0.938400, 0.854450, 0.751400,
340        0.642400, 0.541900, 0.447900, 0.360800, 0.283500,
341        0.218700, 0.164900, 0.121200, 0.087400, 0.063600,
342        0.046770, 0.032900, 0.022700, 0.015840, 0.011359,
343        0.008111, 0.005790, 0.004109, 0.002899, 0.002049,
344        0.001440, 0.001000, 0.000690, 0.000476, 0.000332,
345        0.000235, 0.000166, 0.000117, 0.000083, 0.000059,
346        0.000042,
347    ];
348    #[rustfmt::skip]
349    const CMF_Y: &'static [f32; 81] = &[
350        0.000039, 0.000064, 0.000120, 0.000217, 0.000396,
351        0.000640, 0.001210, 0.002180, 0.004000, 0.007300,
352        0.011600, 0.016840, 0.023000, 0.029800, 0.038000,
353        0.048000, 0.060000, 0.073900, 0.090980, 0.112600,
354        0.139020, 0.169300, 0.208020, 0.258600, 0.323000,
355        0.407300, 0.503000, 0.608200, 0.710000, 0.793200,
356        0.862000, 0.914850, 0.954000, 0.980300, 0.994950,
357        1.000000, 0.995000, 0.978600, 0.952000, 0.915400,
358        0.870000, 0.816300, 0.757000, 0.694900, 0.631000,
359        0.566800, 0.503000, 0.441200, 0.381000, 0.321000,
360        0.265000, 0.217000, 0.175000, 0.138200, 0.107000,
361        0.081600, 0.061000, 0.044580, 0.032000, 0.023200,
362        0.017000, 0.011920, 0.008210, 0.005723, 0.004102,
363        0.002929, 0.002091, 0.001484, 0.001047, 0.000740,
364        0.000520, 0.000361, 0.000249, 0.000172, 0.000120,
365        0.000085, 0.000060, 0.000042, 0.000030, 0.000021,
366        0.000015,
367    ];
368    #[rustfmt::skip]
369    const CMF_Z: &'static [f32; 81] = &[
370        0.006450, 0.010550, 0.020050, 0.036210, 0.067850,
371        0.110200, 0.207400, 0.371300, 0.645600, 1.039050,
372        1.385600, 1.622960, 1.747060, 1.782600, 1.772110,
373        1.744100, 1.669200, 1.528100, 1.287640, 1.041900,
374        0.812950, 0.616200, 0.465180, 0.353300, 0.272000,
375        0.212300, 0.158200, 0.111700, 0.078250, 0.057250,
376        0.042160, 0.029840, 0.020300, 0.013400, 0.008750,
377        0.005750, 0.003900, 0.002750, 0.002100, 0.001800,
378        0.001650, 0.001400, 0.001100, 0.001000, 0.000800,
379        0.000600, 0.000340, 0.000240, 0.000190, 0.000100,
380        0.000050, 0.000030, 0.000020, 0.000010, 0.000000,
381        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
382        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
383        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
384        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
385        0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
386        0.000000,
387    ];
388}
389
390/// Spectral radiance on the 31-band ICC grid (400–700 nm, 10 nm).
391pub type Spectral31Radiance = Spectral<31, Grid400_700_10nm, IsRadiance>;
392/// Spectral reflectance on the 31-band ICC grid (400–700 nm, 10 nm).
393pub type Spectral31Reflectance = Spectral<31, Grid400_700_10nm, IsReflectance>;
394/// Spectral transmittance on the 31-band ICC grid (400–700 nm, 10 nm).
395pub type Spectral31Transmittance = Spectral<31, Grid400_700_10nm, IsTransmittance>;
396/// Spectral radiance on the 36-band Stam rendering grid (380–730 nm, 10 nm).
397pub type Spectral36Radiance = Spectral<36, Grid380_730_10nm, IsRadiance>;
398/// Spectral reflectance on the 36-band Stam rendering grid (380–730 nm, 10 nm).
399pub type Spectral36Reflectance = Spectral<36, Grid380_730_10nm, IsReflectance>;
400/// Spectral transmittance on the 36-band Stam rendering grid (380–730 nm, 10 nm).
401pub type Spectral36Transmittance = Spectral<36, Grid380_730_10nm, IsTransmittance>;
402/// Spectral radiance on the 41-band full-visible grid (380–780 nm, 10 nm).
403pub type Spectral41Radiance = Spectral<41, Grid380_780_10nm, IsRadiance>;
404/// Spectral reflectance on the 41-band full-visible grid (380–780 nm, 10 nm).
405pub type Spectral41Reflectance = Spectral<41, Grid380_780_10nm, IsReflectance>;
406/// Spectral transmittance on the 41-band full-visible grid (380–780 nm, 10 nm).
407pub type Spectral41Transmittance = Spectral<41, Grid380_780_10nm, IsTransmittance>;
408/// Spectral radiance on the 81-band CIE standard grid (380–780 nm, 5 nm).
409pub type Spectral81Radiance = Spectral<81, Grid380_780_5nm, IsRadiance>;
410/// Spectral reflectance on the 81-band CIE standard grid (380–780 nm, 5 nm).
411pub type Spectral81Reflectance = Spectral<81, Grid380_780_5nm, IsReflectance>;
412/// Spectral transmittance on the 81-band CIE standard grid (380–780 nm, 5 nm).
413pub type Spectral81Transmittance = Spectral<81, Grid380_780_5nm, IsTransmittance>;