1use crate::color::{xyz_to_yxy, Observer};
2use crate::error::{LuxError, LuxResult};
3use crate::illuminants::cct_to_xyz;
4use crate::spectrum::{getwlr, Spectrum, WavelengthGrid};
5
6pub const DEFAULT_WL_GRID: WavelengthGrid = WavelengthGrid {
7 start: 360.0,
8 end: 830.0,
9 step: 1.0,
10};
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct RoundedTriangleParams {
14 pub peakwl: f64,
15 pub fwhm: Option<f64>,
16 pub rounding: f64,
17 pub min_v: f64,
18 pub max_v: f64,
19 pub fw: f64,
20 pub rw: f64,
21}
22
23impl Default for RoundedTriangleParams {
24 fn default() -> Self {
25 Self {
26 peakwl: 530.0,
27 fwhm: Some(100.0),
28 rounding: 0.5,
29 min_v: 0.0,
30 max_v: 1.0,
31 fw: 100.0,
32 rw: 100.0,
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub struct MonoLedParams {
39 pub peakwl: f64,
40 pub fwhm: f64,
41 pub strength_shoulder: f64,
42 pub bw_order: f64,
43}
44
45impl Default for MonoLedParams {
46 fn default() -> Self {
47 Self {
48 peakwl: 530.0,
49 fwhm: 20.0,
50 strength_shoulder: 2.0,
51 bw_order: -1.0,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
57pub struct PhosphorLedParams {
58 pub peakwl: f64,
59 pub fwhm: f64,
60 pub bw_order: f64,
61 pub strength_shoulder: f64,
62 pub strength_ph: Option<f64>,
63 pub peakwl_ph1: f64,
64 pub fwhm_ph1: f64,
65 pub strength_ph1: f64,
66 pub peakwl_ph2: f64,
67 pub fwhm_ph2: f64,
68 pub strength_ph2: Option<f64>,
69 pub use_piecewise_fcn: bool,
70}
71
72impl Default for PhosphorLedParams {
73 fn default() -> Self {
74 Self {
75 peakwl: 450.0,
76 fwhm: 20.0,
77 bw_order: -1.0,
78 strength_shoulder: 2.0,
79 strength_ph: Some(0.0),
80 peakwl_ph1: 530.0,
81 fwhm_ph1: 80.0,
82 strength_ph1: 1.0,
83 peakwl_ph2: 560.0,
84 fwhm_ph2: 80.0,
85 strength_ph2: None,
86 use_piecewise_fcn: false,
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
92pub struct PhosphorLedComponents {
93 pub spd: Spectrum,
94 pub components: Spectrum,
95}
96
97pub fn gaussian_spd(
99 peakwls: &[f64],
100 fwhms: &[f64],
101 grid: Option<WavelengthGrid>,
102) -> LuxResult<Spectrum> {
103 if peakwls.is_empty() || fwhms.is_empty() {
104 return Err(LuxError::EmptyInput);
105 }
106 if fwhms.len() != 1 && fwhms.len() != peakwls.len() {
107 return Err(LuxError::InvalidGridSpec);
108 }
109
110 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
111 let wavelengths = getwlr(grid)?;
112 let num_peaks = peakwls.len();
113 let fwhm_to_sig = 1.0 / (2.0 * (2.0 * 2.0f64.ln()).sqrt());
114 let mut spectra = Vec::with_capacity(num_peaks);
115
116 for i in 0..num_peaks {
117 let peakwl = peakwls[i];
118 let fwhm = if fwhms.len() == 1 { fwhms[0] } else { fwhms[i] };
119 let sig = fwhm * fwhm_to_sig;
120
121 let mut values = Vec::with_capacity(wavelengths.len());
122 for &wl in &wavelengths {
123 let val = (-0.5 * ((wl - peakwl) / sig).powi(2)).exp();
124 values.push(val);
125 }
126 spectra.push(values);
127 }
128
129 Spectrum::new(wavelengths, spectra)
130}
131
132pub fn lorentzian2_spd(
134 peakwls: &[f64],
135 fwhms: &[f64],
136 grid: Option<WavelengthGrid>,
137) -> LuxResult<Spectrum> {
138 if peakwls.is_empty() || fwhms.is_empty() {
139 return Err(LuxError::EmptyInput);
140 }
141 if fwhms.len() != 1 && fwhms.len() != peakwls.len() {
142 return Err(LuxError::InvalidGridSpec);
143 }
144
145 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
146 let wavelengths = getwlr(grid)?;
147 let num_peaks = peakwls.len();
148 let n = 2.0 * (2.0f64.sqrt() - 1.0).sqrt();
149 let mut spectra = Vec::with_capacity(num_peaks);
150
151 for i in 0..num_peaks {
152 let peakwl = peakwls[i];
153 let fwhm = if fwhms.len() == 1 { fwhms[0] } else { fwhms[i] };
154
155 let mut values = Vec::with_capacity(wavelengths.len());
156 for &wl in &wavelengths {
157 let val = (1.0 + (n * (wl - peakwl) / fwhm).powi(2)).powf(-2.0);
158 values.push(val);
159 }
160 spectra.push(values);
161 }
162
163 Spectrum::new(wavelengths, spectra)
164}
165
166pub fn butterworth_spd(
168 peakwls: &[f64],
169 fwhms: &[f64],
170 bw_orders: &[f64],
171 grid: Option<WavelengthGrid>,
172) -> LuxResult<Spectrum> {
173 if peakwls.is_empty() || fwhms.is_empty() || bw_orders.is_empty() {
174 return Err(LuxError::EmptyInput);
175 }
176 if (fwhms.len() != 1 && fwhms.len() != peakwls.len())
177 || (bw_orders.len() != 1 && bw_orders.len() != peakwls.len())
178 {
179 return Err(LuxError::InvalidGridSpec);
180 }
181
182 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
183 let wavelengths = getwlr(grid)?;
184 let num_peaks = peakwls.len();
185 let mut spectra = Vec::with_capacity(num_peaks);
186
187 for i in 0..num_peaks {
188 let peakwl = peakwls[i];
189 let fwhm = if fwhms.len() == 1 { fwhms[0] } else { fwhms[i] };
190 let bw_order = if bw_orders.len() == 1 { bw_orders[0] } else { bw_orders[i] };
191
192 let mut values = Vec::with_capacity(wavelengths.len());
193 for &wl in &wavelengths {
194 let val = 1.0 / (1.0 + (2.0 * (wl - peakwl) / fwhm).abs().powf(2.0 * bw_order));
195 values.push(val);
196 }
197 spectra.push(values);
198 }
199
200 Spectrum::new(wavelengths, spectra)
201}
202
203pub fn roundedtriangle_spd(
205 params: &[RoundedTriangleParams],
206 grid: Option<WavelengthGrid>,
207) -> LuxResult<Spectrum> {
208 if params.is_empty() {
209 return Err(LuxError::EmptyInput);
210 }
211
212 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
213 let wavelengths = getwlr(grid)?;
214 let num_peaks = params.len();
215 let mut spectra = Vec::with_capacity(num_peaks);
216
217 for param in params {
218 let peakwl = param.peakwl.abs();
219 let rounding = param.rounding.abs();
220 let min_v = param.min_v.abs();
221 let max_v = param.max_v.abs();
222
223 let (fw, rw) = match param.fwhm {
224 Some(fwhm) => {
225 let width = fwhm.abs() / (rounding / 4.0 + 1.0);
226 (width, width)
227 }
228 None => (param.fw.abs(), param.rw.abs()),
229 };
230
231 let eps = 1e-308;
232 let r_param = if rounding == 0.0 { eps } else { rounding };
233
234 let mut values = Vec::with_capacity(wavelengths.len());
235 for &wl in &wavelengths {
236 let wlp = wl - peakwl;
237 let x = if wlp < 0.0 { wlp / fw } else { wlp / rw };
238 let abs_x = x.abs();
239
240 let rraw = if abs_x < r_param / 2.0 {
241 1.0 - r_param / 4.0 - (1.0 / r_param) * x.powi(2)
242 } else if abs_x >= r_param / 2.0 && abs_x < 1.0 - r_param / 2.0 {
243 1.0 - abs_x
244 } else if abs_x >= 1.0 - r_param / 2.0 && abs_x < 1.0 + r_param / 2.0 {
245 1.0 / (2.0 * r_param) * (abs_x - (1.0 + r_param / 2.0)).powi(2)
246 } else {
247 0.0
248 };
249
250 let spd_val = min_v + (max_v - min_v) * rraw / (1.0 - rounding / 4.0);
251 values.push(spd_val);
252 }
253 spectra.push(values);
254 }
255
256 Spectrum::new(wavelengths, spectra)
257}
258
259pub fn mono_led_spd(
261 params: &[MonoLedParams],
262 grid: Option<WavelengthGrid>,
263) -> LuxResult<Spectrum> {
264 if params.is_empty() {
265 return Err(LuxError::EmptyInput);
266 }
267
268 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
269 let wavelengths = getwlr(grid)?;
270 let num_peaks = params.len();
271 let mut spectra = Vec::with_capacity(num_peaks);
272
273 for param in params {
274 let peakwl = param.peakwl;
275 let fwhm = param.fwhm;
276 let strength_shoulder = param.strength_shoulder;
277 let bw_order = param.bw_order;
278
279 let mut values = Vec::with_capacity(wavelengths.len());
280
281 if bw_order == -2.0 {
282 let spd = lorentzian2_spd(&[peakwl], &[fwhm], Some(grid))?;
283 values.extend_from_slice(spd.spectra()[0].as_slice());
284 } else {
285 let g_spd = gaussian_spd(&[peakwl], &[fwhm], Some(grid))?;
286 let g = g_spd.spectra()[0].as_slice();
287
288 let mut ohno = Vec::with_capacity(wavelengths.len());
289 for &g_val in g {
290 let val = (g_val + strength_shoulder * g_val.powi(5)) / (1.0 + strength_shoulder);
291 ohno.push(val);
292 }
293
294 if bw_order == -1.0 || bw_order == 0.0 {
295 values.extend_from_slice(&ohno);
296 } else if bw_order > 0.0 {
297 let bw_spd = butterworth_spd(&[peakwl], &[fwhm], &[bw_order], Some(grid))?;
298 values.extend_from_slice(bw_spd.spectra()[0].as_slice());
299 } else {
300 let bw_spd = butterworth_spd(&[peakwl], &[fwhm], &[bw_order], Some(grid))?;
302 let bw = bw_spd.spectra()[0].as_slice();
303
304 let lz_spd = lorentzian2_spd(&[peakwl], &[fwhm], Some(grid))?;
305 let lz = lz_spd.spectra()[0].as_slice();
306
307 for j in 0..wavelengths.len() {
308 let mut val = 0.0;
309 if bw_order >= -1.0 && bw_order <= 0.0 {
310 val += ohno[j];
311 }
312 if bw_order > 0.0 {
313 val += bw[j];
314 }
315 if bw_order >= -2.0 && bw_order < -1.0 {
316 val += lz[j];
317 }
318 values.push(val);
319 }
320 }
321 }
322
323 spectra.push(values);
324 }
325
326 Spectrum::new(wavelengths, spectra)
327}
328
329pub fn phosphor_led_spd(
331 params: &[PhosphorLedParams],
332 grid: Option<WavelengthGrid>,
333) -> LuxResult<Spectrum> {
334 let res = phosphor_led_spd_with_components(params, grid)?;
335 Ok(res.spd)
336}
337
338pub fn phosphor_led_spd_with_components(
340 params: &[PhosphorLedParams],
341 grid: Option<WavelengthGrid>,
342) -> LuxResult<PhosphorLedComponents> {
343 if params.is_empty() {
344 return Err(LuxError::EmptyInput);
345 }
346
347 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
348 let wavelengths = getwlr(grid)?;
349 let num_mixtures = params.len();
350
351 let mut combined_spectra = Vec::with_capacity(num_mixtures);
352
353 let has_phosphors = params.iter().any(|p| p.strength_ph.unwrap_or(0.0) > 0.0);
355 let num_components = if has_phosphors { 3 } else { 1 };
356
357 let mut component_rows = vec![vec![0.0; wavelengths.len()]; num_mixtures * num_components];
362
363 for (i, param) in params.iter().enumerate() {
364 let mono_params = MonoLedParams {
366 peakwl: param.peakwl,
367 fwhm: param.fwhm,
368 bw_order: param.bw_order,
369 strength_shoulder: param.strength_shoulder,
370 };
371 let mono_spd = mono_led_spd(&[mono_params], Some(grid))?;
372 let mono_led = mono_spd.spectra()[0].clone();
373
374 let mut spd = mono_led.clone();
375
376 if let Some(s_ph) = param.strength_ph {
377 if s_ph > 0.0 && has_phosphors {
378 let ph1_params = MonoLedParams {
380 peakwl: param.peakwl_ph1,
381 fwhm: param.fwhm_ph1,
382 bw_order: -1.0,
383 strength_shoulder: 1.0,
384 };
385 let ph1_spd = mono_led_spd(&[ph1_params], Some(grid))?;
386 let ph1 = ph1_spd.spectra()[0].clone();
387
388 let ph2_params = MonoLedParams {
390 peakwl: param.peakwl_ph2,
391 fwhm: param.fwhm_ph2,
392 bw_order: -1.0,
393 strength_shoulder: 1.0,
394 };
395 let ph2_spd = mono_led_spd(&[ph2_params], Some(grid))?;
396 let ph2 = ph2_spd.spectra()[0].clone();
397
398 let mut phosphors = Vec::with_capacity(wavelengths.len());
400 let s_ph1 = param.strength_ph1;
401
402 if let Some(s_ph2) = param.strength_ph2 {
403 let sum = s_ph1 + s_ph2;
404 let denom = if sum == 0.0 { 1e-300 } else { sum };
405 for j in 0..wavelengths.len() {
406 let val = (s_ph1 * ph1[j] + s_ph2 * ph2[j]) / denom + 1e-300;
407 phosphors.push(val);
408 }
409 } else {
410 for j in 0..wavelengths.len() {
411 let val = s_ph1 * ph1[j] + (1.0 - s_ph1) * ph2[j] + 1e-300;
412 phosphors.push(val);
413 }
414 }
415
416 let max_ph = phosphors
418 .iter()
419 .copied()
420 .fold(f64::NEG_INFINITY, f64::max);
421 let max_ph_val = if max_ph <= 0.0 { 1.0 } else { max_ph };
422 for val in &mut phosphors {
423 *val /= max_ph_val;
424 }
425
426 for j in 0..wavelengths.len() {
428 spd[j] = mono_led[j] + s_ph * phosphors[j];
429 }
430
431 component_rows[i] = mono_led;
433 component_rows[num_mixtures + i] = ph1;
434 component_rows[2 * num_mixtures + i] = ph2;
435 } else {
436 component_rows[i] = mono_led;
437 }
438 } else {
439 component_rows[i] = mono_led;
440 }
441
442 if param.use_piecewise_fcn {
444 for j in 0..wavelengths.len() {
446 let wl = wavelengths[j];
447 let factor = if wl < param.peakwl {
448 let mono_val = component_rows[i][j]; mono_val
450 } else {
451 1.0
452 };
453 spd[j] *= factor;
454
455 component_rows[i][j] *= factor;
457 if has_phosphors && param.strength_ph.unwrap_or(0.0) > 0.0 {
458 component_rows[num_mixtures + i][j] *= factor;
459 component_rows[2 * num_mixtures + i][j] *= factor;
460 }
461 }
462 }
463
464 let max_val = spd
466 .iter()
467 .copied()
468 .fold(f64::NEG_INFINITY, f64::max);
469 let max_scale = if max_val <= 0.0 { 1.0 } else { max_val };
470 for val in &mut spd {
471 *val /= max_scale;
472 }
473
474 combined_spectra.push(spd);
475 }
476
477 for row in &mut component_rows {
479 let max_val = row
480 .iter()
481 .copied()
482 .fold(f64::NEG_INFINITY, f64::max);
483 let max_scale = if max_val <= 0.0 { 1.0 } else { max_val };
484 for val in &mut *row {
485 *val /= max_scale;
486 }
487 }
488
489 let spd = Spectrum::new(wavelengths.clone(), combined_spectra)?;
490 let components = Spectrum::new(wavelengths, component_rows)?;
491
492 Ok(PhosphorLedComponents { spd, components })
493}
494
495fn safe_div(val: f64) -> f64 {
496 if val.abs() < 1e-300 {
497 if val >= 0.0 {
498 1e-300
499 } else {
500 -1e-300
501 }
502 } else {
503 val
504 }
505}
506
507pub fn color3mixer(
509 yxy_target: [f64; 3],
510 yxy1: [f64; 3],
511 yxy2: [f64; 3],
512 yxy3: [f64; 3],
513) -> [f64; 3] {
514 let y1 = yxy1[0];
515 let x1 = yxy1[1];
516 let y1_coord = yxy1[2];
517
518 let y2 = yxy2[0];
519 let x2 = yxy2[1];
520 let y2_coord = yxy2[2];
521
522 let y3 = yxy3[0];
523 let x3 = yxy3[1];
524 let y3_coord = yxy3[2];
525
526 let yt = yxy_target[0];
527 let xt = yxy_target[1];
528 let yt_coord = yxy_target[2];
529
530 let denom = (x3 - x2) * y1_coord + (x2 - x1) * y3_coord + (x1 - x3) * y2_coord;
531 let m1 = y1_coord * ((xt - x3) * y2_coord - (yt_coord - y3_coord) * x2 + x3 * yt_coord - xt * y3_coord)
532 / safe_div(yt_coord * denom);
533
534 let m2 = -y2_coord * ((xt - x3) * y1_coord - (yt_coord - y3_coord) * x1 + x3 * yt_coord - xt * y3_coord)
535 / safe_div(yt_coord * denom);
536
537 let denom3 = (x2 - x1) * y3_coord - (y2_coord - y1_coord) * x3 + x1 * y2_coord - x2 * y1_coord;
538 let m3 = y3_coord * ((x2 - x1) * yt_coord - (y2_coord - y1_coord) * xt + x1 * y2_coord - x2 * y1_coord)
539 / safe_div(yt_coord * denom3);
540
541 [yt * m1 / safe_div(y1), yt * m2 / safe_div(y2), yt * m3 / safe_div(y3)]
542}
543
544fn solve_pseudo_inverse_3xn(a: &[[f64; 3]], b: [f64; 3]) -> Vec<f64> {
546 let n = a.len();
547 let mut aat = [[0.0; 3]; 3];
548 for i in 0..3 {
549 for k in 0..3 {
550 let mut sum = 0.0;
551 for j in 0..n {
552 sum += a[j][i] * a[j][k];
553 }
554 aat[i][k] = sum;
555 }
556 }
557
558 let aat_inv = crate::color::invert_matrix3(aat);
560
561 let mut y = [0.0; 3];
563 for i in 0..3 {
564 let mut sum = 0.0;
565 for k in 0..3 {
566 sum += aat_inv[i][k] * b[k];
567 }
568 y[i] = sum;
569 }
570
571 let mut x = vec![0.0; n];
573 for j in 0..n {
574 let mut sum = 0.0;
575 for i in 0..3 {
576 sum += a[j][i] * y[i];
577 }
578 x[j] = sum;
579 }
580
581 x
582}
583
584pub fn colormixer_pinv(
586 yxy_target: [f64; 3],
587 yxy_primaries: &[[f64; 3]],
588 input_fmt: &str,
589) -> Vec<f64> {
590 let n = yxy_primaries.len();
591 if input_fmt.to_lowercase() == "xyz" {
592 solve_pseudo_inverse_3xn(yxy_primaries, yxy_target)
593 } else {
594 let yt = yxy_target[0];
595 let xt = yxy_target[1];
596 let yt_coord = yxy_target[2];
597
598 let mut a_cols = vec![[0.0; 3]; n];
599 for j in 0..n {
600 let y_i = yxy_primaries[j][0];
601 let x_i = yxy_primaries[j][1];
602 let y_coord_i = yxy_primaries[j][2];
603
604 let ratio = y_i / y_coord_i.max(1e-300);
605 a_cols[j][0] = ratio * (x_i - xt);
606 a_cols[j][1] = ratio * (y_coord_i - yt_coord);
607 a_cols[j][2] = y_i / yt.max(1e-300);
608 }
609
610 solve_pseudo_inverse_3xn(&a_cols, [0.0, 0.0, 1.0])
611 }
612}
613
614pub fn colormixer(
616 yxy_target: [f64; 3],
617 yxy_primaries: &[[f64; 3]],
618 pair_strengths: &[f64],
619) -> Vec<f64> {
620 let n = yxy_primaries.len();
621 if n <= 3 {
622 let p1 = if n > 0 { yxy_primaries[0] } else { [100.0, 1.0/3.0, 1.0/3.0] };
624 let p2 = if n > 1 { yxy_primaries[1] } else { [100.0, 1.0/3.0, 1.0/3.0] };
625 let p3 = if n > 2 { yxy_primaries[2] } else { [100.0, 1.0/3.0, 1.0/3.0] };
626 let m = color3mixer(yxy_target, p1, p2, p3);
627 return m.to_vec();
628 }
629
630 #[derive(Debug, Clone, Copy)]
636 struct LRow {
637 _id: usize,
638 yxy: [f64; 3],
639 parent_a: usize,
640 parent_b: Option<usize>,
641 weight_a: f64,
642 weight_b: Option<f64>,
643 }
644
645 let mut mlut = Vec::new();
646 for i in 0..n {
647 mlut.push(LRow {
648 _id: i,
649 yxy: yxy_primaries[i],
650 parent_a: i,
651 parent_b: None,
652 weight_a: 1.0,
653 weight_b: None,
654 });
655 }
656
657 let mut so: Vec<usize> = (0..n).collect();
658 let mut ps = pair_strengths.to_vec();
659 if ps.len() < n - 3 {
660 ps.resize(n - 3, 0.5);
662 }
663
664 let mut k = 0;
665 let mut kk = 0;
666 let mut su_k = Vec::new();
667 let mut sn_k = Vec::new();
668
669 while so.len() > 3 {
670 let pair_strength_ab = ps[kk];
671 let p_a = so[2 * k];
672 let p_b = so[2 * k + 1];
673
674 let yxy_a = mlut[p_a].yxy;
676 let yxy_b = mlut[p_b].yxy;
677
678 let y_a = yxy_a[0];
679 let x_a = yxy_a[1];
680 let y_coord_a = yxy_a[2];
681
682 let y_b = yxy_b[0];
683 let x_b = yxy_b[1];
684 let y_coord_b = yxy_b[2];
685
686 let x_val_a = x_a * y_a / y_coord_a.max(1e-300);
687 let x_val_b = x_b * y_b / y_coord_b.max(1e-300);
688 let z_val_a = (1.0 - x_a - y_coord_a) * y_a / y_coord_a.max(1e-300);
689 let z_val_b = (1.0 - x_b - y_coord_b) * y_b / y_coord_b.max(1e-300);
690
691 let xm = pair_strength_ab * x_val_a + (1.0 - pair_strength_ab) * x_val_b;
692 let ym = pair_strength_ab * y_a + (1.0 - pair_strength_ab) * y_b;
693 let zm = pair_strength_ab * z_val_a + (1.0 - pair_strength_ab) * z_val_b;
694
695 let sum = xm + ym + zm;
696 let denom = if sum == 0.0 { 1e-300 } else { sum };
697 let xm_coord = xm / denom;
698 let ym_coord = ym / denom;
699
700 let new_id = mlut.len();
701 mlut.push(LRow {
702 _id: new_id,
703 yxy: [ym, xm_coord, ym_coord],
704 parent_a: p_a,
705 parent_b: Some(p_b),
706 weight_a: pair_strength_ab,
707 weight_b: Some(1.0 - pair_strength_ab),
708 });
709
710 su_k.push(p_a);
711 su_k.push(p_b);
712 sn_k.push(new_id);
713
714 let mut rem_so = Vec::new();
715 for &item in &so {
716 if !su_k.contains(&item) {
717 rem_so.push(item);
718 }
719 }
720 rem_so.extend(&sn_k);
721
722 if rem_so.len() <= 3 {
723 so = rem_so;
724 break;
725 }
726
727 let nn = so.len() / 2;
728 if k == nn - 1 {
729 so = rem_so;
730 su_k.clear();
731 sn_k.clear();
732 k = 0;
733 } else {
734 k += 1;
735 }
736 kk += 1;
737 }
738
739 let m3 = color3mixer(yxy_target, mlut[so[0]].yxy, mlut[so[1]].yxy, mlut[so[2]].yxy);
741 if m3.iter().any(|&val| val < 0.0 || val.is_nan()) {
742 return vec![f64::NAN; n];
743 }
744
745 let mut flux_acc = vec![0.0; mlut.len()];
747 flux_acc[so[0]] = m3[0];
748 flux_acc[so[1]] = m3[1];
749 flux_acc[so[2]] = m3[2];
750
751 for i in (n..mlut.len()).rev() {
752 let m_i = flux_acc[i];
753 let p_a = mlut[i].parent_a;
754 let w_a = mlut[i].weight_a;
755 flux_acc[p_a] += w_a * m_i;
756
757 if let Some(p_b) = mlut[i].parent_b {
758 let w_b = mlut[i].weight_b.unwrap_or(0.0);
759 flux_acc[p_b] += w_b * m_i;
760 }
761 }
762
763 flux_acc[0..n].to_vec()
764}
765
766fn target_to_yxy(target: &[f64], tar_type: &str, observer: Observer) -> LuxResult<[f64; 3]> {
768 match tar_type.to_lowercase().as_str() {
769 "cct" => {
770 let cct = target[0];
771 let xyz = cct_to_xyz(cct, observer)?;
772 Ok(xyz_to_yxy(xyz))
773 }
774 "yxy" => {
775 if target.len() < 3 {
776 return Err(LuxError::EmptyInput);
777 }
778 Ok([target[0], target[1], target[2]])
779 }
780 "xyz" => {
781 if target.len() < 3 {
782 return Err(LuxError::EmptyInput);
783 }
784 Ok(xyz_to_yxy([target[0], target[1], target[2]]))
785 }
786 _ => Err(LuxError::InvalidGridSpec),
787 }
788}
789
790pub fn spd_builder(
792 flux: Option<&[f64]>,
793 component_spds: Option<&Spectrum>,
794 params: &PhosphorLedParams,
795 pair_strengths: Option<&[f64]>,
796 target: Option<&[f64]>,
797 tar_type: &str,
798 observer: Observer,
799 grid: Option<WavelengthGrid>,
800) -> LuxResult<Spectrum> {
801 let grid = grid.unwrap_or(DEFAULT_WL_GRID);
802 let wavelengths = getwlr(grid)?;
803
804 let components = match component_spds {
806 Some(s) => s.clone(),
807 None => {
808 let res = phosphor_led_spd_with_components(&[params.clone()], Some(grid))?;
809 res.components
810 }
811 };
812
813 let n_components = components.spectrum_count();
814
815 if let Some(tar) = target {
817 if n_components < 3 {
818 return Err(LuxError::EmptyInput);
819 }
820
821 let xyz_components = components.spd_to_xyz(&observer.standard()?, false)?;
823 let mut yxy_components = Vec::with_capacity(n_components);
824 for &xyz in &xyz_components {
825 yxy_components.push(xyz_to_yxy(xyz));
826 }
827
828 let yxy_target = target_to_yxy(tar, tar_type, observer)?;
830
831 let m = if n_components == 3 {
833 color3mixer(
834 yxy_target,
835 yxy_components[0],
836 yxy_components[1],
837 yxy_components[2],
838 )
839 .to_vec()
840 } else {
841 let p_strengths = pair_strengths.unwrap_or(&[]);
842 colormixer(yxy_target, &yxy_components, p_strengths)
843 };
844
845 if m.iter().any(|&val| val.is_nan() || val < 0.0) {
846 let spd_values = vec![f64::NAN; wavelengths.len()];
848 return Spectrum::new(wavelengths, vec![spd_values]);
849 }
850
851 let mut spd_values = vec![0.0; wavelengths.len()];
853 for j in 0..n_components {
854 let factor = m[j];
855 let comp_values = &components.spectra()[j];
856 for k in 0..wavelengths.len() {
857 spd_values[k] += factor * comp_values[k];
858 }
859 }
860
861 let max_val = spd_values
863 .iter()
864 .copied()
865 .fold(f64::NEG_INFINITY, f64::max);
866 let max_scale = if max_val <= 0.0 { 1.0 } else { max_val };
867 for val in &mut spd_values {
868 *val /= max_scale;
869 }
870
871 Spectrum::new(wavelengths, vec![spd_values])
872 } else {
873 let flux_vals = flux.unwrap_or(&[]);
875 if flux_vals.is_empty() {
876 Ok(components)
878 } else {
879 let mut spd_values = vec![0.0; wavelengths.len()];
881 let num_to_mix = n_components.min(flux_vals.len());
882 for j in 0..num_to_mix {
883 let factor = flux_vals[j];
884 let comp_values = &components.spectra()[j];
885 for k in 0..wavelengths.len() {
886 spd_values[k] += factor * comp_values[k];
887 }
888 }
889
890 let max_val = spd_values
892 .iter()
893 .copied()
894 .fold(f64::NEG_INFINITY, f64::max);
895 let max_scale = if max_val <= 0.0 { 1.0 } else { max_val };
896 for val in &mut spd_values {
897 *val /= max_scale;
898 }
899
900 Spectrum::new(wavelengths, vec![spd_values])
901 }
902 }
903}
904
905pub fn fit_gaussian_spd_params(
907 target_xy: [f64; 2],
908 init_peak: f64,
909 init_fwhm: f64,
910) -> LuxResult<(f64, f64)> {
911 let obj_func = |params: [f64; 2]| -> f64 {
912 let peak = params[0];
913 let fwhm = params[1];
914 if fwhm <= 0.0 || peak < 360.0 || peak > 830.0 {
915 return 1e10;
916 }
917 let grid = DEFAULT_WL_GRID;
918 let g = match gaussian_spd(&[peak], &[fwhm], Some(grid)) {
919 Ok(s) => s,
920 Err(_) => return 1e10,
921 };
922 let observer = Observer::Cie1931_2;
923 let xyz = match g.spd_to_xyz(&observer.standard().unwrap(), false) {
924 Ok(v) => v[0],
925 Err(_) => return 1e10,
926 };
927 let yxy = xyz_to_yxy(xyz);
928 let x = yxy[1];
929 let y = yxy[2];
930 (x - target_xy[0]).powi(2) + (y - target_xy[1]).powi(2)
931 };
932
933 let opt = nelder_mead_2d(obj_func, [init_peak, init_fwhm], [2.0, 5.0], 1e-12, 1000);
934 Ok((opt[0], opt[1]))
935}
936
937fn nelder_mead_2d<F>(
938 mut obj_func: F,
939 init: [f64; 2],
940 step: [f64; 2],
941 tol: f64,
942 max_iter: usize,
943) -> [f64; 2]
944where
945 F: FnMut([f64; 2]) -> f64,
946{
947 let p0 = init;
948 let p1 = [init[0] + step[0], init[1]];
949 let p2 = [init[0], init[1] + step[1]];
950
951 let mut points = [
952 (p0, obj_func(p0)),
953 (p1, obj_func(p1)),
954 (p2, obj_func(p2)),
955 ];
956
957 for _ in 0..max_iter {
958 points.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
959
960 let diff = (points[2].1 - points[0].1).abs();
961 if diff < tol {
962 break;
963 }
964
965 let centroid = [
966 0.5 * (points[0].0[0] + points[1].0[0]),
967 0.5 * (points[0].0[1] + points[1].0[1]),
968 ];
969
970 let reflected = [
971 centroid[0] + 1.0 * (centroid[0] - points[2].0[0]),
972 centroid[1] + 1.0 * (centroid[1] - points[2].0[1]),
973 ];
974 let r_val = obj_func(reflected);
975
976 if r_val < points[1].1 && r_val >= points[0].1 {
977 points[2] = (reflected, r_val);
978 continue;
979 }
980
981 if r_val < points[0].1 {
982 let expanded = [
983 centroid[0] + 2.0 * (reflected[0] - centroid[0]),
984 centroid[1] + 2.0 * (reflected[1] - centroid[1]),
985 ];
986 let e_val = obj_func(expanded);
987 if e_val < r_val {
988 points[2] = (expanded, e_val);
989 } else {
990 points[2] = (reflected, r_val);
991 }
992 continue;
993 }
994
995 let contracted = [
996 centroid[0] + 0.5 * (points[2].0[0] - centroid[0]),
997 centroid[1] + 0.5 * (points[2].0[1] - centroid[1]),
998 ];
999 let c_val = obj_func(contracted);
1000 if c_val < points[2].1 {
1001 points[2] = (contracted, c_val);
1002 continue;
1003 }
1004
1005 for i in 1..3 {
1006 points[i].0[0] = points[0].0[0] + 0.5 * (points[i].0[0] - points[0].0[0]);
1007 points[i].0[1] = points[0].0[1] + 0.5 * (points[i].0[1] - points[0].0[1]);
1008 points[i].1 = obj_func(points[i].0);
1009 }
1010 }
1011
1012 points[0].0
1013}
1014