1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright (c) 2024-2025, Harbers Bik LLC
use crate::{
error::Error,
math::{LineAB, Orientation},
};
use super::XYZ;
impl XYZ {
/// The Dominant Wavelength of a color point is the wavelength of spectral
/// color, obtained from the intersection of a line through a white point
/// and itself, with the spectral locus. Points on this line were
/// historically thought off as having the same hue, but that has been
/// proven wrong. This value has limited practical use, but is sometimes
/// used for color definition of LEDs.
///
/// The spectral locus, being the boundary of all possible colors in the CIE
/// 1931 diagram, collapses to one point beyond a wavelength of 699nm. As a
/// result, the maxium range of dominant wavelengths which can be obtained
/// is from 380 to 699 nanometer;
///
pub fn dominant_wavelength(&self, white: XYZ) -> Result<f64, Error> {
let mut sign = 1.0;
let wavelength_range = self.observer.spectral_locus_wavelength_range();
let mut low = *wavelength_range.start();
let mut high = *wavelength_range.end();
let mut mid = 540usize; // 200 fails, as its tail overlaps into the blue region
if white.observer != self.observer {
Err(Error::RequireSameObserver)
} else {
let chromaticity = self.chromaticity();
let [mut x, mut y] = [chromaticity.x(), chromaticity.y()];
let white_chromaticity = white.chromaticity();
// if color point is in the purple rotate it around the white point by 180ยบ, and give wavelength a negative value
let blue_edge = LineAB::new(
white_chromaticity.to_array(),
self.observer
.xyz_at_wavelength(low)
.unwrap()
.chromaticity()
.to_array(),
)
.unwrap();
let red_edge = LineAB::new(
white_chromaticity.to_array(),
self.observer
.xyz_at_wavelength(high)
.unwrap()
.chromaticity()
.to_array(),
)
.unwrap();
match (blue_edge.orientation(x, y), red_edge.orientation(x, y)) {
(Orientation::Colinear, _) => return Ok(380.0),
(_, Orientation::Colinear) => return Ok(699.0),
(Orientation::Left, Orientation::Right) => {
// mirror point into non-purple region
sign = -1.0;
x = 2.0 * white_chromaticity.x() - x;
y = 2.0 * white_chromaticity.y() - y;
}
_ => {} // do nothing
}
// start bisectional search
while high - low > 1 {
let bisect = LineAB::new(
white_chromaticity.to_array(),
self.observer
.xyz_at_wavelength(mid)
.unwrap()
.chromaticity()
.to_array(),
)
.unwrap();
// let a = bisect.angle_deg();
match bisect.orientation(x, y) {
Orientation::Left => high = mid,
Orientation::Right => low = mid,
Orientation::Colinear => {
low = mid;
high = mid;
}
}
mid = (low + high) / 2;
}
if low == high {
Ok(sign * low as f64)
} else {
let low_ab = LineAB::new(
white.chromaticity().to_array(),
self.observer
.xyz_at_wavelength(low)
.unwrap()
.chromaticity()
.to_array(),
)
.unwrap();
let dlow = low_ab.distance_with_sign(x, y);
let high_ab = LineAB::new(
white.chromaticity().to_array(),
self.observer
.xyz_at_wavelength(high)
.unwrap()
.chromaticity()
.to_array(),
)
.unwrap();
let dhigh = high_ab.distance_with_sign(x, y);
if dlow < 0.0 || dhigh > 0.0 {
// not ended up between two lines
let s = format!("bisection error in dominant wavelength search: {dlow} {low} {dhigh} {high}");
return Err(Error::ErrorString(s));
}
let dl = (dlow.abs() * high as f64 + dhigh.abs() * low as f64)
/ (dlow.abs() + dhigh.abs());
Ok(sign * dl)
}
}
}
}
#[cfg(test)]
mod xyz_test {
use crate::math::LineAB;
use crate::observer::Observer::Cie1931;
use approx::assert_ulps_eq;
#[test]
fn dominant_wavelength_test() {
let d65 = Cie1931.xyz_d65().set_illuminance(50.0);
// 550 nm
let sl = Cie1931
.xyz_at_wavelength(550)
.unwrap()
.set_illuminance(50.0);
let t = d65.try_add(sl).unwrap();
let dl = t.dominant_wavelength(d65).unwrap();
assert_ulps_eq!(dl, 550.0);
for wl in 380..=699usize {
let sl2 = Cie1931.xyz_at_wavelength(wl).unwrap();
//let [slx, sly] = sl2.chromaticity();
//println!("sl xy: {slx} {sly}");
let dl = sl2.dominant_wavelength(d65).unwrap();
assert_ulps_eq!(dl, wl as f64, epsilon = 1E-10);
}
}
#[test]
fn dominant_wavelength_purple_test() {
let d65 = Cie1931.xyz_d65();
let white_chromaticity = d65.chromaticity();
// get purple line
let xyzb = Cie1931.xyz_at_wavelength(380).unwrap();
let [xb, yb] = xyzb.chromaticity().to_array();
let xyzr = Cie1931.xyz_at_wavelength(699).unwrap();
let [xr, yr] = xyzr.chromaticity().to_array();
let line_t = LineAB::new([xb, yb], [xr, yr]).unwrap();
for wl in 380..=699usize {
let sl = Cie1931.xyz_at_wavelength(wl).unwrap();
let chromaticity = sl.chromaticity();
let line_u =
LineAB::new(chromaticity.to_array(), white_chromaticity.to_array()).unwrap();
let ([_xi, yi], t, _) = line_t.intersect(&line_u).unwrap();
if t > 0.0 && t < 1.0 {
// see https://en.wikipedia.org/wiki/CIE_1931_color_space#Mixing_colors_specified_with_the_CIE_xy_chromaticity_diagram
let b = xyzb.set_illuminance(100.0 * (yb * (yr - yi)));
let r = xyzr.set_illuminance(100.0 * (yr * (yi - yb)));
let s = b.try_add(r).unwrap();
let dl = s.dominant_wavelength(d65).unwrap();
assert_ulps_eq!(dl, -(wl as f64));
}
}
}
}