1#![allow(clippy::needless_range_loop)]
74
75use super::drude::DrudeLorentzOscillator;
76use super::mermin::check_oscillator;
77use super::sum_rules::SumRuleReport;
78use crate::electron::data::{ElectronDataError, OpticalElf};
79use std::f64::consts::FRAC_PI_2;
80use std::fmt;
81
82type Result<T> = std::result::Result<T, ElectronDataError>;
83
84pub const MAX_OSCILLATORS: usize = 16;
86
87#[derive(Debug, Clone, Copy, PartialEq)]
89pub enum FitWeighting {
90 Uniform,
92 Relative {
96 floor: f64,
98 },
99}
100
101#[derive(Debug, Clone, PartialEq)]
103pub struct MerminFitOptions {
104 pub n_oscillators: usize,
107 pub start: Option<Vec<DrudeLorentzOscillator>>,
110 pub weighting: FitWeighting,
112 pub max_iterations: usize,
114}
115
116impl Default for MerminFitOptions {
117 fn default() -> Self {
118 Self {
119 n_oscillators: 3,
120 start: None,
121 weighting: FitWeighting::Relative { floor: 1e-2 },
122 max_iterations: 500,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq)]
130pub struct MerminFit {
131 pub oscillators: Vec<DrudeLorentzOscillator>,
134 pub energy_ev: Vec<f64>,
136 pub data: Vec<f64>,
138 pub model: Vec<f64>,
140 pub residuals: Vec<f64>,
142 pub weighting: FitWeighting,
144 pub weighted_rms: f64,
146 pub rms_residual: f64,
148 pub max_abs_residual: f64,
150 pub iterations: usize,
152 pub converged: bool,
154 pub f_sum_ev2: f64,
156 pub p_eff: f64,
158 pub data_f_sum_ev2: f64,
160 pub data_p_eff: f64,
162}
163
164fn drude_basis(w: f64, e: f64, g: f64) -> f64 {
165 let d = w * w - e * e;
166 g * w / (d * d + g * g * w * w)
167}
168
169impl MerminFit {
170 pub fn model_elf(&self, energy_ev: f64) -> f64 {
173 if energy_ev <= 0.0 {
174 return 0.0;
175 }
176 self.oscillators
177 .iter()
178 .map(|o| o.strength_ev2 * drude_basis(energy_ev, o.energy_ev, o.width_ev))
179 .sum()
180 }
181
182 pub fn plasma_energy_ev(&self) -> f64 {
184 self.oscillators
185 .iter()
186 .map(|o| o.strength_ev2)
187 .sum::<f64>()
188 .sqrt()
189 }
190
191 pub fn summary(&self) -> String {
194 let osc: Vec<String> = self
195 .oscillators
196 .iter()
197 .map(|o| {
198 format!(
199 "(A={:.6e} eV^2, E={:.6e} eV, gamma={:.6e} eV)",
200 o.strength_ev2, o.energy_ev, o.width_ev
201 )
202 })
203 .collect();
204 format!("{} oscillators {}", self.oscillators.len(), osc.join(" "))
205 }
206}
207
208impl fmt::Display for MerminFit {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 writeln!(
211 f,
212 "Mermin-ELF fit: {} oscillators, {} iterations, converged: {}",
213 self.oscillators.len(),
214 self.iterations,
215 self.converged
216 )?;
217 for (j, o) in self.oscillators.iter().enumerate() {
218 writeln!(
219 f,
220 " {j}: A = {:.6e} eV^2, E = {:.6e} eV, gamma = {:.6e} eV",
221 o.strength_ev2, o.energy_ev, o.width_ev
222 )?;
223 }
224 writeln!(
225 f,
226 " residual rms {:.3e} (weighted {:.3e}), max |r| {:.3e}",
227 self.rms_residual, self.weighted_rms, self.max_abs_residual
228 )?;
229 writeln!(
230 f,
231 " f-sum: fit {:.6e} eV^2, data {:.6e} eV^2 (ratio {:.6})",
232 self.f_sum_ev2,
233 self.data_f_sum_ev2,
234 self.f_sum_ev2 / self.data_f_sum_ev2
235 )?;
236 write!(
237 f,
238 " P_eff: fit {:.6}, data {:.6}",
239 self.p_eff, self.data_p_eff
240 )
241 }
242}
243
244fn lstsq(cols: &[&[f64]], y: &[f64]) -> Option<Vec<f64>> {
250 let n = cols.len();
251 let m = y.len();
252 if n == 0 {
253 return Some(Vec::new());
254 }
255 if n > m {
256 return None;
257 }
258 let mut a: Vec<Vec<f64>> = cols.iter().map(|c| c.to_vec()).collect();
259 let mut b = y.to_vec();
260 let mut diag = vec![0.0; n];
261 for k in 0..n {
262 let norm = a[k][k..].iter().map(|x| x * x).sum::<f64>().sqrt();
263 if norm < 1e-13 {
264 return None;
265 }
266 let alpha = if a[k][k] > 0.0 { -norm } else { norm };
267 let mut v: Vec<f64> = a[k][k..].to_vec();
268 v[0] -= alpha;
269 let vnorm2: f64 = v.iter().map(|x| x * x).sum();
270 diag[k] = alpha;
271 if vnorm2 > 0.0 {
272 for j in (k + 1)..n {
273 let dot: f64 = v.iter().zip(&a[j][k..]).map(|(p, q)| p * q).sum();
274 let s = 2.0 * dot / vnorm2;
275 for (i, vi) in v.iter().enumerate() {
276 a[j][k + i] -= s * vi;
277 }
278 }
279 let dot: f64 = v.iter().zip(&b[k..]).map(|(p, q)| p * q).sum();
280 let s = 2.0 * dot / vnorm2;
281 for (i, vi) in v.iter().enumerate() {
282 b[k + i] -= s * vi;
283 }
284 }
285 }
286 let mut x = vec![0.0; n];
288 for k in (0..n).rev() {
289 let mut s = b[k];
290 for j in (k + 1)..n {
291 s -= a[j][k] * x[j];
292 }
293 x[k] = s / diag[k];
294 }
295 x.iter().all(|v| v.is_finite()).then_some(x)
296}
297
298fn nnls(cols: &[Vec<f64>], y: &[f64]) -> Vec<f64> {
302 let n = cols.len();
303 let mut x = vec![0.0; n];
304 let mut passive = vec![false; n];
305 let ynorm = y.iter().map(|v| v * v).sum::<f64>().sqrt();
306 let tol = 1e-13 * ynorm.max(f64::MIN_POSITIVE);
307 let gradient = |x: &[f64]| -> Vec<f64> {
308 let mut r = y.to_vec();
309 for (c, xc) in cols.iter().zip(x) {
310 if *xc != 0.0 {
311 for (ri, ci) in r.iter_mut().zip(c) {
312 *ri -= xc * ci;
313 }
314 }
315 }
316 cols.iter()
317 .map(|c| c.iter().zip(&r).map(|(p, q)| p * q).sum::<f64>())
318 .collect()
319 };
320 for _ in 0..(3 * n + 3) {
321 let w = gradient(&x);
322 let Some((t, wt)) = (0..n)
323 .filter(|&j| !passive[j])
324 .map(|j| (j, w[j]))
325 .max_by(|a, b| a.1.total_cmp(&b.1))
326 else {
327 break;
328 };
329 if wt <= tol {
330 break;
331 }
332 passive[t] = true;
333 for _ in 0..(3 * n + 3) {
334 let idx: Vec<usize> = (0..n).filter(|&j| passive[j]).collect();
335 let sub: Vec<&[f64]> = idx.iter().map(|&j| cols[j].as_slice()).collect();
336 let Some(s) = lstsq(&sub, y) else {
337 passive[t] = false;
339 return x;
340 };
341 if s.iter().all(|&v| v > 0.0) {
342 x = vec![0.0; n];
343 for (&j, &v) in idx.iter().zip(&s) {
344 x[j] = v;
345 }
346 break;
347 }
348 let mut alpha = f64::INFINITY;
349 for (&j, &sj) in idx.iter().zip(&s) {
350 if sj <= 0.0 {
351 let a = x[j] / (x[j] - sj);
352 if a < alpha {
353 alpha = a;
354 }
355 }
356 }
357 for (&j, &sj) in idx.iter().zip(&s) {
358 x[j] += alpha * (sj - x[j]);
359 }
360 for &j in &idx {
361 if x[j] <= 1e-14 {
362 x[j] = 0.0;
363 passive[j] = false;
364 }
365 }
366 }
367 }
368 x
369}
370
371fn solve(mut m: Vec<Vec<f64>>, mut r: Vec<f64>) -> Option<Vec<f64>> {
374 let n = r.len();
375 for k in 0..n {
376 let p = (k..n).max_by(|&a, &b| m[a][k].abs().total_cmp(&m[b][k].abs()))?;
377 if m[p][k].abs() < 1e-300 {
378 return None;
379 }
380 m.swap(k, p);
381 r.swap(k, p);
382 for i in (k + 1)..n {
383 let f = m[i][k] / m[k][k];
384 for j in k..n {
385 m[i][j] -= f * m[k][j];
386 }
387 r[i] -= f * r[k];
388 }
389 }
390 let mut x = vec![0.0; n];
391 for k in (0..n).rev() {
392 let mut s = r[k];
393 for j in (k + 1)..n {
394 s -= m[k][j] * x[j];
395 }
396 x[k] = s / m[k][k];
397 }
398 x.iter().all(|v| v.is_finite()).then_some(x)
399}
400
401fn local_knot_spacing(w: &[f64], e: f64) -> f64 {
408 let n = w.len();
409 if n < 2 {
410 return 0.0;
411 }
412 let k = match w.binary_search_by(|x| x.total_cmp(&e)) {
413 Ok(k) => k,
414 Err(0) => 0,
415 Err(i) if i >= n => n - 1,
416 Err(i) => {
417 if e - w[i - 1] <= w[i] - e {
418 i - 1
419 } else {
420 i
421 }
422 }
423 };
424 let left = if k > 0 { w[k] - w[k - 1] } else { 0.0 };
425 let right = if k + 1 < n { w[k + 1] - w[k] } else { 0.0 };
426 left.max(right)
427}
428
429struct Problem<'a> {
430 w: &'a [f64],
431 y: Vec<f64>,
432 row_weight: Vec<f64>,
433}
434
435impl Problem<'_> {
436 fn energy_width(&self, ln_e: f64, ln_g: f64) -> (f64, f64) {
441 let e = ln_e.exp();
442 (e, ln_g.exp().max(local_knot_spacing(self.w, e)))
443 }
444
445 fn raise_widths_to_floor(&self, theta: &mut [f64]) -> bool {
453 let mut changed = false;
454 for j in 0..theta.len() / 2 {
455 let ln_floor = local_knot_spacing(self.w, theta[2 * j].exp()).ln();
456 if theta[2 * j + 1] < ln_floor {
457 theta[2 * j + 1] = ln_floor;
458 changed = true;
459 }
460 }
461 changed
462 }
463
464 fn project(&self, theta: &[f64]) -> (Vec<f64>, Vec<f64>) {
467 let n = theta.len() / 2;
468 let yw: Vec<f64> = self
469 .y
470 .iter()
471 .zip(&self.row_weight)
472 .map(|(y, s)| y * s)
473 .collect();
474 let mut cols = Vec::with_capacity(n);
475 let mut norms = Vec::with_capacity(n);
476 for j in 0..n {
477 let (e, g) = self.energy_width(theta[2 * j], theta[2 * j + 1]);
478 let mut c: Vec<f64> = self
479 .w
480 .iter()
481 .zip(&self.row_weight)
482 .map(|(&w, &s)| s * drude_basis(w, e, g))
483 .collect();
484 let norm = c.iter().map(|v| v * v).sum::<f64>().sqrt();
485 let norm = if norm > 0.0 && norm.is_finite() {
486 norm
487 } else {
488 1.0
489 };
490 for v in &mut c {
491 *v /= norm;
492 }
493 cols.push(c);
494 norms.push(norm);
495 }
496 let coef = nnls(&cols, &yw);
497 let mut r: Vec<f64> = yw.iter().map(|v| -v).collect();
498 for (c, k) in cols.iter().zip(&coef) {
499 for (ri, ci) in r.iter_mut().zip(c) {
500 *ri += k * ci;
501 }
502 }
503 let amps = coef.iter().zip(&norms).map(|(k, n)| k / n).collect();
504 (amps, r)
505 }
506}
507
508fn default_start(elf: &OpticalElf, n: usize) -> Vec<DrudeLorentzOscillator> {
509 let w = elf.energy_ev();
510 let e = elf.elf_values();
511 let mut peaks: Vec<(f64, f64)> = Vec::new();
512 for i in 0..w.len() {
513 let left = i == 0 || e[i] > e[i - 1];
514 let right = i + 1 == w.len() || e[i] >= e[i + 1];
515 if left && right && e[i] > 0.0 {
516 peaks.push((e[i], w[i]));
517 }
518 }
519 peaks.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.total_cmp(&b.1)));
520 let mut energies: Vec<f64> = peaks.iter().take(n).map(|p| p.1).collect();
521 let (lo, hi) = (w[0].ln(), w[w.len() - 1].ln());
523 let mut k = 1usize;
524 while energies.len() < n {
525 let cand = (lo + (hi - lo) * k as f64 / (n as f64 + 1.0)).exp();
526 if !energies.iter().any(|&x| (x / cand).ln().abs() < 0.05) {
527 energies.push(cand);
528 }
529 k += 1;
530 if k > 10 * n + 10 {
531 energies.push(cand * (1.0 + 0.1 * k as f64));
532 }
533 }
534 energies.sort_by(f64::total_cmp);
535 energies
536 .into_iter()
537 .map(|e| DrudeLorentzOscillator {
538 strength_ev2: 0.0,
539 energy_ev: e,
540 width_ev: 0.25 * e,
541 })
542 .collect()
543}
544
545pub fn fit_mermin_oscillators(elf: &OpticalElf, options: &MerminFitOptions) -> Result<MerminFit> {
597 let w = elf.energy_ev();
598 let y = elf.elf_values();
599 let start = match &options.start {
600 Some(s) => s.clone(),
601 None => {
602 if !(1..=MAX_OSCILLATORS).contains(&options.n_oscillators) {
603 return Err(ElectronDataError::Invalid {
604 what: "Mermin fit",
605 reason: format!(
606 "number of oscillators must be in 1..={MAX_OSCILLATORS}, got {}",
607 options.n_oscillators
608 ),
609 });
610 }
611 default_start(elf, options.n_oscillators)
612 }
613 };
614 let n = start.len();
615 if !(1..=MAX_OSCILLATORS).contains(&n) {
616 return Err(ElectronDataError::Invalid {
617 what: "Mermin fit",
618 reason: format!("number of oscillators must be in 1..={MAX_OSCILLATORS}, got {n}"),
619 });
620 }
621 for o in &start {
622 let mut o = *o;
623 o.strength_ev2 = 0.0; check_oscillator(&o)?;
625 }
626 if w.len() < 2 * n + 1 {
627 return Err(ElectronDataError::Invalid {
628 what: "Mermin fit",
629 reason: format!(
630 "{} tabulated points cannot determine {n} oscillators (need at least {})",
631 w.len(),
632 2 * n + 1
633 ),
634 });
635 }
636 let ymax = y.iter().cloned().fold(0.0_f64, f64::max);
637 if ymax.is_nan() || ymax <= 0.0 {
638 return Err(ElectronDataError::Invalid {
639 what: "Mermin fit",
640 reason: "the ELF is zero everywhere".into(),
641 });
642 }
643 let row_weight: Vec<f64> = match options.weighting {
644 FitWeighting::Uniform => vec![1.0; w.len()],
645 FitWeighting::Relative { floor } => {
646 if !(floor > 0.0 && floor <= 1.0) {
647 return Err(ElectronDataError::Invalid {
648 what: "Mermin fit weighting",
649 reason: format!("relative floor must be in (0, 1], got {floor}"),
650 });
651 }
652 y.iter().map(|v| 1.0 / (v + floor * ymax)).collect()
653 }
654 };
655 let problem = Problem {
656 w,
657 y: y.to_vec(),
658 row_weight,
659 };
660
661 let mut theta: Vec<f64> = start
662 .iter()
663 .flat_map(|o| [o.energy_ev.ln(), o.width_ev.ln()])
664 .collect();
665 problem.raise_widths_to_floor(&mut theta);
668 let np = theta.len();
669 let (mut amps, mut r) = problem.project(&theta);
670 let mut cost = 0.5 * r.iter().map(|v| v * v).sum::<f64>();
671 let scale = 0.5
672 * problem
673 .y
674 .iter()
675 .zip(&problem.row_weight)
676 .map(|(y, s)| (y * s).powi(2))
677 .sum::<f64>();
678 let mut lambda = 1e-3;
679 let mut iterations = 0;
680 let mut converged = false;
681 let h = 1e-6;
682
683 while iterations < options.max_iterations {
684 if cost <= 1e-28 * scale {
685 converged = true;
686 break;
687 }
688 iterations += 1;
689 let mut jac: Vec<Vec<f64>> = Vec::with_capacity(np); for p in 0..np {
692 let mut tp = theta.clone();
693 tp[p] += h;
694 let (_, rp) = problem.project(&tp);
695 tp[p] -= 2.0 * h;
696 let (_, rm) = problem.project(&tp);
697 jac.push(
698 rp.iter()
699 .zip(&rm)
700 .map(|(a, b)| (a - b) / (2.0 * h))
701 .collect(),
702 );
703 }
704 let jtj: Vec<Vec<f64>> = (0..np)
705 .map(|a| {
706 (0..np)
707 .map(|b| jac[a].iter().zip(&jac[b]).map(|(p, q)| p * q).sum())
708 .collect()
709 })
710 .collect();
711 let g: Vec<f64> = (0..np)
712 .map(|a| jac[a].iter().zip(&r).map(|(p, q)| p * q).sum())
713 .collect();
714 let gmax = g.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
715 if gmax <= 1e-14 * scale.sqrt().max(f64::MIN_POSITIVE) {
716 converged = true;
717 break;
718 }
719 let mut accepted = false;
721 for _ in 0..40 {
722 let mut m = jtj.clone();
723 for (a, row) in m.iter_mut().enumerate() {
724 row[a] += lambda * jtj[a][a].max(1e-12 * gmax.max(1e-300));
725 }
726 let rhs: Vec<f64> = g.iter().map(|v| -v).collect();
727 if let Some(mut delta) = solve(m, rhs) {
728 for d in &mut delta {
729 *d = d.clamp(-1.0, 1.0);
730 }
731 let trial: Vec<f64> = theta.iter().zip(&delta).map(|(t, d)| t + d).collect();
732 let (a2, r2) = problem.project(&trial);
733 let c2 = 0.5 * r2.iter().map(|v| v * v).sum::<f64>();
734 if c2.is_finite() && c2 < cost {
735 let step = delta.iter().fold(0.0_f64, |m, d| m.max(d.abs()));
736 let drop = (cost - c2) / cost;
737 theta = trial;
738 amps = a2;
739 r = r2;
740 cost = c2;
741 if problem.raise_widths_to_floor(&mut theta) {
742 (amps, r) = problem.project(&theta);
746 cost = 0.5 * r.iter().map(|v| v * v).sum::<f64>();
747 }
748 lambda = (lambda * 0.1).max(1e-15);
749 accepted = true;
750 if step < 1e-13 || drop < 1e-15 {
751 converged = true;
752 }
753 break;
754 }
755 }
756 lambda *= 10.0;
757 }
758 if !accepted {
759 converged = true;
761 break;
762 }
763 if converged {
764 break;
765 }
766 }
767
768 let mut oscillators: Vec<DrudeLorentzOscillator> = (0..n)
769 .map(|j| {
770 let (energy_ev, width_ev) = problem.energy_width(theta[2 * j], theta[2 * j + 1]);
771 DrudeLorentzOscillator {
772 strength_ev2: amps[j],
773 energy_ev,
774 width_ev,
775 }
776 })
777 .collect();
778 oscillators.sort_by(|a, b| {
779 a.energy_ev
780 .total_cmp(&b.energy_ev)
781 .then(a.width_ev.total_cmp(&b.width_ev))
782 });
783
784 let mut fit = MerminFit {
785 oscillators,
786 energy_ev: w.to_vec(),
787 data: y.to_vec(),
788 model: Vec::new(),
789 residuals: Vec::new(),
790 weighting: options.weighting,
791 weighted_rms: (r.iter().map(|v| v * v).sum::<f64>() / r.len() as f64).sqrt(),
792 rms_residual: 0.0,
793 max_abs_residual: 0.0,
794 iterations,
795 converged,
796 f_sum_ev2: 0.0,
797 p_eff: 0.0,
798 data_f_sum_ev2: 0.0,
799 data_p_eff: 0.0,
800 };
801 fit.model = w.iter().map(|&e| fit.model_elf(e)).collect();
802 fit.residuals = fit.model.iter().zip(y).map(|(m, d)| m - d).collect();
803 fit.rms_residual = (fit.residuals.iter().map(|v| v * v).sum::<f64>() / w.len() as f64).sqrt();
804 fit.max_abs_residual = fit.residuals.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
805 fit.f_sum_ev2 = FRAC_PI_2 * fit.oscillators.iter().map(|o| o.strength_ev2).sum::<f64>();
806 fit.p_eff = fit
807 .oscillators
808 .iter()
809 .map(|o| o.strength_ev2 / (o.energy_ev * o.energy_ev))
810 .sum();
811 let rules = SumRuleReport::new(elf);
812 fit.data_f_sum_ev2 = rules.f_sum_ev2;
813 fit.data_p_eff = rules.p_eff;
814 Ok(fit)
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820 use crate::electron::inelastic::drude::DrudeLorentz;
821
822 fn osc(a: f64, e: f64, g: f64) -> DrudeLorentzOscillator {
823 DrudeLorentzOscillator {
824 strength_ev2: a,
825 energy_ev: e,
826 width_ev: g,
827 }
828 }
829
830 #[test]
831 fn lstsq_solves_an_overdetermined_system() {
832 let c0 = [1.0, 1.0, 1.0, 1.0];
833 let c1 = [0.0, 1.0, 2.0, 3.0];
834 let y = [1.0, 3.0, 5.0, 7.0]; let x = lstsq(&[&c0, &c1], &y).unwrap();
836 assert!((x[0] - 1.0).abs() < 1e-12 && (x[1] - 2.0).abs() < 1e-12);
837 }
838
839 #[test]
840 fn nnls_clips_negative_coefficients() {
841 let c0 = vec![1.0, 0.0, 0.0];
842 let c1 = vec![0.0, 1.0, 0.0];
843 let x = nnls(&[c0, c1], &[2.0, -3.0, 0.0]);
845 assert!((x[0] - 2.0).abs() < 1e-12);
846 assert_eq!(x[1], 0.0);
847 }
848
849 #[test]
850 fn nnls_matches_least_squares_when_the_solution_is_positive() {
851 let c0 = vec![1.0, 1.0, 0.5, 0.0];
852 let c1 = vec![0.0, 1.0, 1.0, 1.0];
853 let y: Vec<f64> = c0.iter().zip(&c1).map(|(a, b)| 1.5 * a + 0.7 * b).collect();
854 let x = nnls(&[c0, c1], &y);
855 assert!((x[0] - 1.5).abs() < 1e-12 && (x[1] - 0.7).abs() < 1e-12);
856 }
857
858 fn synthetic() -> (Vec<DrudeLorentzOscillator>, OpticalElf) {
859 let truth = vec![
860 osc(120.0, 9.0, 3.0),
861 osc(400.0, 22.0, 7.0),
862 osc(900.0, 55.0, 25.0),
863 ];
864 let elf = DrudeLorentz::new(truth.clone())
865 .unwrap()
866 .to_optical_elf("synthetic", 0.5, 2000.0, 400)
867 .unwrap();
868 (truth, elf)
869 }
870
871 #[test]
872 fn recovers_three_known_oscillators_from_a_perturbed_start() {
873 let (truth, elf) = synthetic();
874 let start = vec![
875 osc(1.0, 9.0 * 1.15, 3.0 * 0.8),
876 osc(1.0, 22.0 * 0.88, 7.0 * 1.2),
877 osc(1.0, 55.0 * 1.1, 25.0 * 0.85),
878 ];
879 let fit = fit_mermin_oscillators(
880 &elf,
881 &MerminFitOptions {
882 start: Some(start),
883 ..Default::default()
884 },
885 )
886 .unwrap();
887 assert!(fit.converged, "{fit}");
888 assert_eq!(fit.oscillators.len(), 3);
889 for (got, want) in fit.oscillators.iter().zip(&truth) {
890 for (name, g, w) in [
891 ("A", got.strength_ev2, want.strength_ev2),
892 ("E", got.energy_ev, want.energy_ev),
893 ("gamma", got.width_ev, want.width_ev),
894 ] {
895 assert!(((g - w) / w).abs() < 1e-3, "{name}: {g} vs {w}\n{fit}");
896 }
897 assert!(got.strength_ev2 >= 0.0);
898 }
899 let want = DrudeLorentz::new(truth).unwrap();
901 assert!(((fit.f_sum_ev2 - want.f_sum_ev2()) / want.f_sum_ev2()).abs() < 1e-3);
902 assert!(((fit.p_eff - want.p_eff()) / want.p_eff()).abs() < 1e-3);
903 assert!(fit.max_abs_residual < 1e-6, "{fit}");
904 assert_eq!(fit.residuals.len(), elf.energy_ev().len());
905 }
906
907 #[test]
908 fn default_start_also_recovers_the_synthetic_elf() {
909 let (truth, elf) = synthetic();
910 let fit = fit_mermin_oscillators(&elf, &MerminFitOptions::default()).unwrap();
911 assert!(fit.rms_residual < 1e-4, "{fit}");
912 let want = DrudeLorentz::new(truth).unwrap();
913 assert!(((fit.f_sum_ev2 - want.f_sum_ev2()) / want.f_sum_ev2()).abs() < 1e-2);
914 }
915
916 #[test]
917 fn amplitudes_stay_non_negative_for_a_surplus_of_oscillators() {
918 let (_, elf) = synthetic();
919 let fit = fit_mermin_oscillators(
920 &elf,
921 &MerminFitOptions {
922 n_oscillators: 5,
923 ..Default::default()
924 },
925 )
926 .unwrap();
927 assert!(fit.oscillators.iter().all(|o| o.strength_ev2 >= 0.0));
928 assert!(fit.rms_residual < 1e-3, "{fit}");
929 }
930
931 #[test]
932 fn fit_is_bitwise_deterministic_across_threads() {
933 let (_, elf) = synthetic();
934 let opts = MerminFitOptions::default();
935 let base = fit_mermin_oscillators(&elf, &opts).unwrap();
936 let handles: Vec<_> = (0..4)
937 .map(|_| {
938 let (elf, opts) = (elf.clone(), opts.clone());
939 std::thread::spawn(move || fit_mermin_oscillators(&elf, &opts).unwrap())
940 })
941 .collect();
942 for h in handles {
943 let other = h.join().unwrap();
944 for (a, b) in base.oscillators.iter().zip(&other.oscillators) {
945 assert_eq!(a.strength_ev2.to_bits(), b.strength_ev2.to_bits());
946 assert_eq!(a.energy_ev.to_bits(), b.energy_ev.to_bits());
947 assert_eq!(a.width_ev.to_bits(), b.width_ev.to_bits());
948 }
949 assert_eq!(base.iterations, other.iterations);
950 }
951 }
952
953 #[test]
954 fn invalid_options_are_rejected() {
955 let (_, elf) = synthetic();
956 for n in [0, MAX_OSCILLATORS + 1] {
957 assert!(fit_mermin_oscillators(
958 &elf,
959 &MerminFitOptions {
960 n_oscillators: n,
961 ..Default::default()
962 }
963 )
964 .is_err());
965 }
966 assert!(fit_mermin_oscillators(
967 &elf,
968 &MerminFitOptions {
969 weighting: FitWeighting::Relative { floor: 0.0 },
970 ..Default::default()
971 }
972 )
973 .is_err());
974 assert!(fit_mermin_oscillators(
975 &elf,
976 &MerminFitOptions {
977 start: Some(vec![osc(1.0, -1.0, 1.0)]),
978 ..Default::default()
979 }
980 )
981 .is_err());
982 }
983
984 #[test]
985 fn local_knot_spacing_is_the_larger_gap_next_to_the_nearest_knot() {
986 let w = [1.0, 2.0, 4.0, 8.0, 16.0];
988 assert_eq!(local_knot_spacing(&w, 4.0), 4.0);
990 assert_eq!(local_knot_spacing(&w, 3.5), 4.0);
991 assert_eq!(local_knot_spacing(&w, 5.9), 4.0);
992 assert_eq!(local_knot_spacing(&w, 6.1), 8.0);
994 assert_eq!(local_knot_spacing(&w, 3.0), 2.0);
996 assert_eq!(local_knot_spacing(&w, 1.0), 1.0);
998 assert_eq!(local_knot_spacing(&w, 0.1), 1.0);
999 assert_eq!(local_knot_spacing(&w, 16.0), 8.0);
1000 assert_eq!(local_knot_spacing(&w, 1e6), 8.0);
1001 }
1002
1003 #[test]
1004 fn widths_are_not_fitted_below_the_knot_spacing() {
1005 let (_, elf) = synthetic();
1008 let w = elf.energy_ev();
1009 let start = vec![osc(1.0, 9.0, 1e-4), osc(1.0, 55.0, 1e-4)];
1010 let fit = fit_mermin_oscillators(
1011 &elf,
1012 &MerminFitOptions {
1013 start: Some(start),
1014 max_iterations: 5,
1015 ..Default::default()
1016 },
1017 )
1018 .unwrap();
1019 for o in &fit.oscillators {
1020 assert!(o.width_ev >= local_knot_spacing(w, o.energy_ev), "{fit}");
1021 }
1022 }
1023
1024 #[test]
1025 fn data_sum_rules_are_reported() {
1026 let (truth, elf) = synthetic();
1027 let fit = fit_mermin_oscillators(&elf, &MerminFitOptions::default()).unwrap();
1028 let want = DrudeLorentz::new(truth).unwrap();
1029 assert!(fit.data_f_sum_ev2 > 0.9 * want.f_sum_ev2());
1031 assert!(fit.data_p_eff > 0.9 * want.p_eff());
1032 assert!(fit.summary().starts_with("3 oscillators"));
1033 }
1034}