1#[derive(Debug, Clone)]
13pub struct FitResult {
14 pub y_fit: Vec<f64>,
16 pub parameters: Vec<f64>,
18 pub param_names: Vec<String>,
20}
21
22pub trait FitFunction {
24 fn name(&self) -> &str;
26
27 fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult>;
29}
30
31pub struct LinearFit;
33
34impl FitFunction for LinearFit {
35 fn name(&self) -> &str {
36 "Linear"
37 }
38
39 fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult> {
40 if x.len() != y.len() || x.len() < 2 {
41 return None;
42 }
43 let n = x.len() as f64;
44 let sum_x: f64 = x.iter().sum();
45 let sum_y: f64 = y.iter().sum();
46 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
47 let sum_xx: f64 = x.iter().map(|&xi| xi * xi).sum();
48
49 let denominator = n * sum_xx - sum_x * sum_x;
50 if denominator.abs() < 1e-12 {
51 return None;
52 }
53
54 let m = (n * sum_xy - sum_x * sum_y) / denominator;
55 let c = (sum_y - m * sum_x) / n;
56
57 let y_fit = x.iter().map(|&xi| m * xi + c).collect();
58
59 Some(FitResult {
60 y_fit,
61 parameters: vec![m, c],
62 param_names: vec!["Slope (m)".to_string(), "Intercept (c)".to_string()],
63 })
64 }
65}
66
67pub struct GaussianEstimateFit;
70
71impl FitFunction for GaussianEstimateFit {
72 fn name(&self) -> &str {
73 "Gaussian (Estimate)"
74 }
75
76 fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult> {
77 if x.len() != y.len() || x.len() < 3 {
78 return None;
79 }
80
81 let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
82 let mut max_y = f64::NEG_INFINITY;
83 let mut max_idx = 0;
84 for (i, &yi) in y.iter().enumerate() {
85 if yi > max_y {
86 max_y = yi;
87 max_idx = i;
88 }
89 }
90
91 let a = max_y - bg;
92 let mu = x[max_idx];
93
94 let half_max = bg + a / 2.0;
96 let mut left_idx = max_idx;
97 while left_idx > 0 && y[left_idx] > half_max {
98 left_idx -= 1;
99 }
100 let mut right_idx = max_idx;
101 while right_idx < y.len() - 1 && y[right_idx] > half_max {
102 right_idx += 1;
103 }
104
105 let fwhm = x[right_idx] - x[left_idx];
106 let sigma = if fwhm > 0.0 {
107 fwhm / 2.355
108 } else {
109 (x.last().unwrap() - x.first().unwrap()) / 4.0
110 };
111
112 let y_fit = x
113 .iter()
114 .map(|&xi| {
115 let z = (xi - mu) / sigma;
116 a * (-0.5 * z * z).exp() + bg
117 })
118 .collect();
119
120 Some(FitResult {
121 y_fit,
122 parameters: vec![a, mu, sigma, bg],
123 param_names: vec![
124 "Amplitude (A)".to_string(),
125 "Center (mu)".to_string(),
126 "Sigma".to_string(),
127 "Background".to_string(),
128 ],
129 })
130 }
131}
132
133pub const LOG2: f64 = std::f64::consts::LN_2;
146
147pub fn fwhm_to_sigma_factor() -> f64 {
151 2.0 * (2.0 * LOG2).sqrt()
152}
153
154#[derive(Debug, Clone)]
160pub struct LeastSqResult {
161 pub parameters: Vec<f64>,
164 pub covariance: Vec<Vec<f64>>,
168 pub uncertainties: Vec<f64>,
174 pub chisq: f64,
177 pub reduced_chisq: Option<f64>,
181 pub niter: usize,
183 pub nfev: usize,
185}
186
187impl LeastSqResult {
188 pub fn std_errors(&self) -> Vec<f64> {
194 (0..self.parameters.len())
195 .map(|i| self.covariance[i][i].abs().sqrt())
196 .collect()
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum FitError {
203 LengthMismatch,
205 NoFreeParameters,
208 NotEnoughData,
210 NonFinite,
213 SingularMatrix,
216 InvalidConstraint,
220 BadConstraintReference,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq)]
232pub enum Constraint {
233 Free,
235 Positive,
238 Quoted {
243 min: f64,
245 max: f64,
247 },
248 Fixed,
251 Factor {
253 reference: usize,
255 factor: f64,
257 },
258 Delta {
260 reference: usize,
262 delta: f64,
264 },
265 Sum {
267 reference: usize,
269 sum: f64,
271 },
272 Ignored,
275}
276
277fn get_parameters(params: &[f64], constraints: &[Constraint]) -> Vec<f64> {
283 let mut out: Vec<f64> = params
284 .iter()
285 .zip(constraints)
286 .map(|(&p, c)| match c {
287 Constraint::Positive => p.abs(),
288 _ => p,
289 })
290 .collect();
291 for (i, c) in constraints.iter().enumerate() {
292 match *c {
293 Constraint::Factor { reference, factor } => out[i] = factor * out[reference],
294 Constraint::Delta { reference, delta } => out[i] = delta + out[reference],
295 Constraint::Sum { reference, sum } => out[i] = sum - out[reference],
296 Constraint::Ignored => out[i] = 0.0,
297 _ => {}
298 }
299 }
300 out
301}
302
303fn get_sigma_parameters(
307 parameters: &[f64],
308 sigma0: &[f64],
309 constraints: &[Constraint],
310) -> Vec<f64> {
311 let mut sigma_par = vec![0.0_f64; parameters.len()];
312 let mut n_free = 0usize;
313 for (i, c) in constraints.iter().enumerate() {
314 match *c {
315 Constraint::Free | Constraint::Positive => {
316 sigma_par[i] = sigma0[n_free];
317 n_free += 1;
318 }
319 Constraint::Quoted { min, max } => {
320 let pmax = min.max(max);
321 let pmin = min.min(max);
322 let b = 0.5 * (pmax - pmin);
323 if b > 0.0 && parameters[i] < pmax && parameters[i] > pmin {
324 sigma_par[i] = (b * parameters[i].cos() * sigma0[n_free]).abs();
325 n_free += 1;
326 } else {
327 sigma_par[i] = parameters[i];
328 }
329 }
330 Constraint::Fixed => sigma_par[i] = parameters[i],
331 _ => {}
332 }
333 }
334 for (i, c) in constraints.iter().enumerate() {
335 match *c {
336 Constraint::Factor { reference, .. }
337 | Constraint::Delta { reference, .. }
338 | Constraint::Sum { reference, .. } => sigma_par[i] = sigma_par[reference],
339 _ => {}
340 }
341 }
342 sigma_par
343}
344
345pub fn invert_matrix(m: &[Vec<f64>]) -> Option<Vec<Vec<f64>>> {
351 let n = m.len();
352 if n == 0 {
353 return Some(Vec::new());
354 }
355 let mut a: Vec<Vec<f64>> = Vec::with_capacity(n);
357 for (i, row) in m.iter().enumerate() {
358 if row.len() != n {
359 return None;
360 }
361 let mut aug = row.clone();
362 aug.extend((0..n).map(|j| if i == j { 1.0 } else { 0.0 }));
363 a.push(aug);
364 }
365 for col in 0..n {
366 let mut pivot = col;
368 let mut best = a[col][col].abs();
369 for (r, row) in a.iter().enumerate().skip(col + 1) {
370 let v = row[col].abs();
371 if v > best {
372 best = v;
373 pivot = r;
374 }
375 }
376 if best == 0.0 {
377 return None; }
379 a.swap(col, pivot);
380 let pivot_val = a[col][col];
381 for v in a[col].iter_mut() {
382 *v /= pivot_val;
383 }
384 let pivot_row = a[col].clone();
385 for (r, row) in a.iter_mut().enumerate() {
386 if r == col {
387 continue;
388 }
389 let factor = row[col];
390 if factor != 0.0 {
391 for (cell, &pv) in row.iter_mut().zip(pivot_row.iter()) {
392 *cell -= factor * pv;
393 }
394 }
395 }
396 }
397 let inv = a
399 .into_iter()
400 .map(|row| row[n..].to_vec())
401 .collect::<Vec<_>>();
402 Some(inv)
403}
404
405pub const DEFAULT_DELTACHI: f64 = 0.001;
409
410pub const DEFAULT_MAX_ITER: usize = 100;
412
413pub fn leastsq<F>(
428 model: F,
429 xdata: &[f64],
430 ydata: &[f64],
431 p0: &[f64],
432 sigma: Option<&[f64]>,
433 max_iter: usize,
434 deltachi: f64,
435) -> Result<LeastSqResult, FitError>
436where
437 F: Fn(&[f64], &[f64]) -> Vec<f64>,
438{
439 if xdata.len() != ydata.len() {
440 return Err(FitError::LengthMismatch);
441 }
442 let n_param = p0.len();
443 if n_param == 0 {
444 return Err(FitError::NoFreeParameters);
445 }
446 let m = ydata.len();
447 if m < n_param {
448 return Err(FitError::NotEnoughData);
449 }
450 if xdata.iter().chain(ydata.iter()).any(|v| !v.is_finite()) {
452 return Err(FitError::NonFinite);
453 }
454 let weight0: Vec<f64> = match sigma {
456 Some(s) => {
457 if s.len() != m {
458 return Err(FitError::LengthMismatch);
459 }
460 if s.iter().any(|v| !v.is_finite()) {
461 return Err(FitError::NonFinite);
462 }
463 s.iter()
464 .map(|&sv| {
465 let denom = if sv == 0.0 { 1.0 } else { sv };
466 let w = 1.0 / denom;
467 w * w
468 })
469 .collect()
470 }
471 None => vec![1.0; m],
472 };
473
474 let epsfcn = f64::EPSILON;
475 let sqrt_epsfcn = epsfcn.sqrt();
476
477 let mut fittedpar = p0.to_vec();
478 let mut flambda = 0.001_f64;
479 let mut iiter = max_iter as i64;
480 let mut last_evaluation: Option<Vec<f64>> = None;
481 let mut iteration_counter: usize = 0;
482 let mut nfev: usize = 0;
483
484 let mut chisq0: f64;
486 let mut alpha0: Vec<Vec<f64>> = vec![vec![0.0; n_param]; n_param];
487
488 loop {
489 if iiter <= 0 {
490 break;
491 }
492 iteration_counter += 1;
493
494 let yfit0 = match &last_evaluation {
497 Some(ev) => ev.clone(),
498 None => {
499 let ev = model(xdata, &fittedpar);
500 nfev += 1;
501 ev
502 }
503 };
504 let delta: Vec<f64> = fittedpar
506 .iter()
507 .map(|&p| (p + if p == 0.0 { 1.0 } else { 0.0 }) * sqrt_epsfcn)
508 .collect();
509 let mut deriv: Vec<Vec<f64>> = Vec::with_capacity(n_param);
511 for i in 0..n_param {
512 let mut pwork = fittedpar.clone();
513 pwork[i] = fittedpar[i] + delta[i];
514 let f1 = model(xdata, &pwork);
515 nfev += 1;
516 let di = delta[i];
517 let row: Vec<f64> = f1
518 .iter()
519 .zip(yfit0.iter())
520 .map(|(&a, &b)| (a - b) / di)
521 .collect();
522 deriv.push(row);
523 }
524 let deltay: Vec<f64> = ydata
526 .iter()
527 .zip(yfit0.iter())
528 .map(|(&y, &f)| y - f)
529 .collect();
530 let help0: Vec<f64> = weight0
531 .iter()
532 .zip(deltay.iter())
533 .map(|(&w, &d)| w * d)
534 .collect();
535 let mut beta = vec![0.0_f64; n_param];
537 for i in 0..n_param {
538 let mut s = 0.0;
539 for j in 0..m {
540 s += help0[j] * deriv[i][j];
541 }
542 beta[i] = s;
543 }
544 let mut alpha = vec![vec![0.0_f64; n_param]; n_param];
546 for i in 0..n_param {
547 for k in 0..n_param {
548 let mut s = 0.0;
549 for j in 0..m {
550 s += deriv[i][j] * weight0[j] * deriv[k][j];
551 }
552 alpha[i][k] = s;
553 }
554 }
555 chisq0 = help0.iter().zip(deltay.iter()).map(|(&h, &d)| h * d).sum();
557 alpha0 = alpha.clone();
558
559 loop {
561 let mut alpha_lm = alpha0.clone();
563 for (d, row) in alpha_lm.iter_mut().enumerate() {
564 row[d] *= 1.0 + flambda;
565 }
566 let inv_alpha = match invert_matrix(&alpha_lm) {
567 Some(inv) => inv,
568 None => {
569 flambda *= 10.0;
571 if flambda > 1000.0 {
572 iiter = 0;
573 break;
574 }
575 continue;
576 }
577 };
578 let mut deltapar = vec![0.0_f64; n_param];
581 for (k, dp) in deltapar.iter_mut().enumerate() {
582 let mut s = 0.0;
583 for (i, &b) in beta.iter().enumerate() {
584 s += b * inv_alpha[i][k];
585 }
586 *dp = s;
587 }
588 let newpar: Vec<f64> = fittedpar
589 .iter()
590 .zip(deltapar.iter())
591 .map(|(&p, &d)| p + d)
592 .collect();
593 let yfit = model(xdata, &newpar);
594 nfev += 1;
595 let chisq: f64 = weight0
596 .iter()
597 .zip(ydata.iter().zip(yfit.iter()))
598 .map(|(&w, (&y, &f))| {
599 let r = y - f;
600 w * r * r
601 })
602 .sum();
603 let absdeltachi = chisq0 - chisq;
604 if absdeltachi < 0.0 {
605 flambda *= 10.0;
607 if flambda > 1000.0 {
608 iiter = 0;
609 break;
610 }
611 } else {
612 fittedpar = newpar;
614 let lastdeltachi =
615 100.0 * (absdeltachi / (chisq + if chisq == 0.0 { 1.0 } else { 0.0 }));
616 if iteration_counter >= 2 && (lastdeltachi < deltachi || absdeltachi < sqrt_epsfcn)
622 {
623 iiter = 0;
624 }
625 flambda /= 10.0;
629 last_evaluation = Some(yfit);
630 break;
631 }
632 }
633 iiter -= 1;
634 }
635
636 let covariance = invert_matrix(&alpha0).ok_or(FitError::SingularMatrix)?;
638 let chisq_final = {
639 let yfit = model(xdata, &fittedpar);
641 nfev += 1;
642 weight0
643 .iter()
644 .zip(ydata.iter().zip(yfit.iter()))
645 .map(|(&w, (&y, &f))| {
646 let r = y - f;
647 w * r * r
648 })
649 .sum::<f64>()
650 };
651 let dof = m as i64 - n_param as i64;
652 let reduced_chisq = if dof > 0 {
653 Some(chisq_final / dof as f64)
654 } else {
655 None
656 };
657
658 let uncertainties: Vec<f64> = (0..n_param)
660 .map(|i| covariance[i][i].abs().sqrt())
661 .collect();
662
663 Ok(LeastSqResult {
664 parameters: fittedpar,
665 covariance,
666 uncertainties,
667 chisq: chisq_final,
668 reduced_chisq,
669 niter: iteration_counter,
670 nfev,
671 })
672}
673
674fn take(v: &[f64], indices: &[usize]) -> Vec<f64> {
676 indices.iter().map(|&i| v[i]).collect()
677}
678
679struct CabOut {
681 chisq: f64,
683 alpha: Vec<Vec<f64>>,
685 beta: Vec<f64>,
687 n_free: usize,
689 free_index: Vec<usize>,
691 noigno: Vec<usize>,
693 fitparam: Vec<f64>,
695}
696
697#[allow(clippy::too_many_arguments)]
702fn chisq_alpha_beta_constrained<F>(
703 model: &F,
704 parameters: &[f64],
705 xdata: &[f64],
706 ydata: &[f64],
707 weight0: &[f64],
708 constraints: &[Constraint],
709 sqrt_epsfcn: f64,
710 last_evaluation: Option<&[f64]>,
711 nfev: &mut usize,
712) -> CabOut
713where
714 F: Fn(&[f64], &[f64]) -> Vec<f64>,
715{
716 let m = ydata.len();
717
718 let mut fitparam: Vec<f64> = Vec::new();
721 let mut free_index: Vec<usize> = Vec::new();
722 let mut noigno: Vec<usize> = Vec::new();
723 let mut derivfactor: Vec<f64> = Vec::new();
724 for (i, c) in constraints.iter().enumerate() {
725 if !matches!(c, Constraint::Ignored) {
726 noigno.push(i);
727 }
728 match *c {
729 Constraint::Free => {
730 fitparam.push(parameters[i]);
731 derivfactor.push(1.0);
732 free_index.push(i);
733 }
734 Constraint::Positive => {
735 fitparam.push(parameters[i].abs());
736 derivfactor.push(1.0);
737 free_index.push(i);
738 }
739 Constraint::Quoted { min, max } => {
740 let pmax = min.max(max);
741 let pmin = min.min(max);
742 if (pmax - pmin) > 0.0 && parameters[i] <= pmax && parameters[i] >= pmin {
743 let a = 0.5 * (pmax + pmin);
744 let b = 0.5 * (pmax - pmin);
745 fitparam.push(parameters[i]);
746 derivfactor.push(b * ((parameters[i] - a) / b).asin().cos());
747 free_index.push(i);
748 }
749 }
751 _ => {}
752 }
753 }
754 let n_free = fitparam.len();
755
756 let delta: Vec<f64> = fitparam
758 .iter()
759 .map(|&p| (p + if p == 0.0 { 1.0 } else { 0.0 }) * sqrt_epsfcn)
760 .collect();
761
762 let mut pwork = parameters.to_vec();
764 for (i, &fi) in free_index.iter().enumerate() {
765 pwork[fi] = fitparam[i];
766 }
767
768 let yfit: Vec<f64> = match last_evaluation {
773 Some(ev) => ev.to_vec(),
774 None => {
775 let base_in = take(&get_parameters(&pwork, constraints), &noigno);
776 let ev = model(xdata, &base_in);
777 *nfev += 1;
778 ev
779 }
780 };
781
782 let mut deriv: Vec<Vec<f64>> = Vec::with_capacity(n_free);
785 for i in 0..n_free {
786 let fi = free_index[i];
787 pwork[fi] = fitparam[i] + delta[i];
788 let newpar = take(&get_parameters(&pwork, constraints), &noigno);
789 let f1 = model(xdata, &newpar);
790 *nfev += 1;
791 let di = delta[i];
792 let df = derivfactor[i];
793 let row: Vec<f64> = f1
794 .iter()
795 .zip(yfit.iter())
796 .map(|(&a, &b)| (a - b) / di * df)
797 .collect();
798 deriv.push(row);
799 pwork[fi] = fitparam[i]; }
801
802 let deltay: Vec<f64> = ydata
803 .iter()
804 .zip(yfit.iter())
805 .map(|(&y, &f)| y - f)
806 .collect();
807 let help0: Vec<f64> = weight0
808 .iter()
809 .zip(deltay.iter())
810 .map(|(&w, &d)| w * d)
811 .collect();
812 let mut beta = vec![0.0_f64; n_free];
813 for (i, b) in beta.iter_mut().enumerate() {
814 let mut s = 0.0;
815 for j in 0..m {
816 s += help0[j] * deriv[i][j];
817 }
818 *b = s;
819 }
820 let mut alpha = vec![vec![0.0_f64; n_free]; n_free];
821 for i in 0..n_free {
822 for k in 0..n_free {
823 let mut s = 0.0;
824 for j in 0..m {
825 s += deriv[i][j] * weight0[j] * deriv[k][j];
826 }
827 alpha[i][k] = s;
828 }
829 }
830 let chisq = help0.iter().zip(deltay.iter()).map(|(&h, &d)| h * d).sum();
831
832 CabOut {
833 chisq,
834 alpha,
835 beta,
836 n_free,
837 free_index,
838 noigno,
839 fitparam,
840 }
841}
842
843#[allow(clippy::too_many_arguments)]
858pub fn leastsq_constrained<F>(
859 model: F,
860 xdata: &[f64],
861 ydata: &[f64],
862 p0: &[f64],
863 constraints: &[Constraint],
864 sigma: Option<&[f64]>,
865 max_iter: usize,
866 deltachi: f64,
867) -> Result<LeastSqResult, FitError>
868where
869 F: Fn(&[f64], &[f64]) -> Vec<f64>,
870{
871 if xdata.len() != ydata.len() {
872 return Err(FitError::LengthMismatch);
873 }
874 let n_param = p0.len();
875 if n_param == 0 {
876 return Err(FitError::NoFreeParameters);
877 }
878 if constraints.len() != n_param {
879 return Err(FitError::BadConstraintReference);
880 }
881 let m = ydata.len();
882 if m < 1 {
883 return Err(FitError::NotEnoughData);
884 }
885 if xdata.iter().chain(ydata.iter()).any(|v| !v.is_finite()) {
886 return Err(FitError::NonFinite);
887 }
888 for c in constraints {
890 match *c {
891 Constraint::Factor { reference, .. }
892 | Constraint::Delta { reference, .. }
893 | Constraint::Sum { reference, .. } => {
894 if reference >= n_param {
895 return Err(FitError::BadConstraintReference);
896 }
897 }
898 Constraint::Quoted { min, max } => {
899 if (min.max(max) - min.min(max)) == 0.0 {
900 return Err(FitError::InvalidConstraint);
901 }
902 }
903 _ => {}
904 }
905 }
906
907 let weight0: Vec<f64> = match sigma {
908 Some(s) => {
909 if s.len() != m {
910 return Err(FitError::LengthMismatch);
911 }
912 if s.iter().any(|v| !v.is_finite()) {
913 return Err(FitError::NonFinite);
914 }
915 s.iter()
916 .map(|&sv| {
917 let denom = if sv == 0.0 { 1.0 } else { sv };
918 let w = 1.0 / denom;
919 w * w
920 })
921 .collect()
922 }
923 None => vec![1.0; m],
924 };
925
926 let epsfcn = f64::EPSILON;
927 let sqrt_epsfcn = epsfcn.sqrt();
928
929 let n_free_initial = constraints
932 .iter()
933 .enumerate()
934 .filter(|(i, c)| match **c {
935 Constraint::Free | Constraint::Positive => true,
936 Constraint::Quoted { min, max } => {
937 let (pmax, pmin) = (min.max(max), min.min(max));
938 (pmax - pmin) > 0.0 && p0[*i] <= pmax && p0[*i] >= pmin
939 }
940 _ => false,
941 })
942 .count();
943 if n_free_initial == 0 {
944 return Err(FitError::NoFreeParameters);
945 }
946
947 let mut fittedpar = p0.to_vec();
948 let mut flambda = 0.001_f64;
949 let mut iiter = max_iter as i64;
950 let mut last_evaluation: Option<Vec<f64>> = None;
951 let mut iteration_counter: usize = 0;
952 let mut nfev: usize = 0;
953
954 let mut alpha0: Vec<Vec<f64>> = vec![vec![0.0; n_free_initial]; n_free_initial];
955 let mut n_free_final = n_free_initial;
956
957 loop {
958 if iiter <= 0 {
959 break;
960 }
961 iteration_counter += 1;
962
963 let cab = chisq_alpha_beta_constrained(
964 &model,
965 &fittedpar,
966 xdata,
967 ydata,
968 &weight0,
969 constraints,
970 sqrt_epsfcn,
971 last_evaluation.as_deref(),
972 &mut nfev,
973 );
974 let chisq0 = cab.chisq;
975 alpha0 = cab.alpha.clone();
976 n_free_final = cab.n_free;
977 let beta = &cab.beta;
978 let free_index = &cab.free_index;
979 let noigno = &cab.noigno;
980 let fitparam = &cab.fitparam;
981 if cab.n_free == 0 {
982 return Err(FitError::NoFreeParameters);
983 }
984
985 loop {
986 let mut alpha_lm = alpha0.clone();
987 for (d, row) in alpha_lm.iter_mut().enumerate() {
988 row[d] *= 1.0 + flambda;
989 }
990 let inv_alpha = match invert_matrix(&alpha_lm) {
991 Some(inv) => inv,
992 None => {
993 flambda *= 10.0;
994 if flambda > 1000.0 {
995 iiter = 0;
996 break;
997 }
998 continue;
999 }
1000 };
1001 let mut deltapar = vec![0.0_f64; cab.n_free];
1003 for (k, dp) in deltapar.iter_mut().enumerate() {
1004 let mut s = 0.0;
1005 for (i, &b) in beta.iter().enumerate() {
1006 s += b * inv_alpha[i][k];
1007 }
1008 *dp = s;
1009 }
1010 let mut newpar = p0.to_vec();
1015 for (i, &fi) in free_index.iter().enumerate() {
1016 let pv = match constraints[fi] {
1017 Constraint::Quoted { min, max } => {
1018 let pmax = min.max(max);
1019 let pmin = min.min(max);
1020 let a = 0.5 * (pmax + pmin);
1021 let b = 0.5 * (pmax - pmin);
1022 a + b * (((fitparam[i] - a) / b).asin() + deltapar[i]).sin()
1023 }
1024 _ => fitparam[i] + deltapar[i],
1027 };
1028 newpar[fi] = pv;
1029 }
1030 let newpar = get_parameters(&newpar, constraints);
1031 let workpar = take(&newpar, noigno);
1032 let yfit = model(xdata, &workpar);
1033 nfev += 1;
1034 let chisq: f64 = weight0
1035 .iter()
1036 .zip(ydata.iter().zip(yfit.iter()))
1037 .map(|(&w, (&y, &f))| {
1038 let r = y - f;
1039 w * r * r
1040 })
1041 .sum();
1042 let absdeltachi = chisq0 - chisq;
1043 if absdeltachi < 0.0 {
1044 flambda *= 10.0;
1045 if flambda > 1000.0 {
1046 iiter = 0;
1047 break;
1048 }
1049 } else {
1050 fittedpar = newpar;
1051 let lastdeltachi =
1052 100.0 * (absdeltachi / (chisq + if chisq == 0.0 { 1.0 } else { 0.0 }));
1053 if iteration_counter >= 2 && (lastdeltachi < deltachi || absdeltachi < sqrt_epsfcn)
1054 {
1055 iiter = 0;
1056 }
1057 flambda /= 10.0;
1058 last_evaluation = Some(yfit);
1059 break;
1060 }
1061 }
1062 iiter -= 1;
1063 }
1064
1065 let cov0 = invert_matrix(&alpha0).ok_or(FitError::SingularMatrix)?;
1067
1068 let new_constraints: Vec<Constraint> = constraints
1072 .iter()
1073 .map(|c| match c {
1074 Constraint::Fixed | Constraint::Ignored => *c,
1075 _ => Constraint::Free,
1076 })
1077 .collect();
1078 let cab2 = chisq_alpha_beta_constrained(
1079 &model,
1080 &fittedpar,
1081 xdata,
1082 ydata,
1083 &weight0,
1084 &new_constraints,
1085 sqrt_epsfcn,
1086 last_evaluation.as_deref(),
1087 &mut nfev,
1088 );
1089 let mut covariance = vec![vec![0.0_f64; n_param]; n_param];
1090 if let Some(cov_free) = invert_matrix(&cab2.alpha) {
1091 for (r, &pr) in cab2.free_index.iter().enumerate() {
1092 for (cc, &pc) in cab2.free_index.iter().enumerate() {
1093 covariance[pr][pc] = cov_free[r][cc];
1094 }
1095 }
1096 }
1097 for (idx, c) in constraints.iter().enumerate() {
1098 if matches!(c, Constraint::Fixed | Constraint::Ignored) {
1099 covariance[idx][idx] = fittedpar[idx] * fittedpar[idx];
1100 }
1101 }
1102
1103 let sigma0: Vec<f64> = (0..n_free_final).map(|i| cov0[i][i].abs().sqrt()).collect();
1106 let uncertainties = get_sigma_parameters(&fittedpar, &sigma0, constraints);
1107
1108 let workpar = take(&get_parameters(&fittedpar, constraints), &cab2.noigno);
1110 let yfit_final = model(xdata, &workpar);
1111 nfev += 1;
1112 let chisq_final: f64 = weight0
1113 .iter()
1114 .zip(ydata.iter().zip(yfit_final.iter()))
1115 .map(|(&w, (&y, &f))| {
1116 let r = y - f;
1117 w * r * r
1118 })
1119 .sum();
1120 let dof = m as i64 - n_free_final as i64;
1121 let reduced_chisq = if dof > 0 {
1122 Some(chisq_final / dof as f64)
1123 } else {
1124 None
1125 };
1126
1127 Ok(LeastSqResult {
1128 parameters: fittedpar,
1129 covariance,
1130 uncertainties,
1131 chisq: chisq_final,
1132 reduced_chisq,
1133 niter: iteration_counter,
1134 nfev,
1135 })
1136}
1137
1138pub fn gaussian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1154 let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
1155 let sigma = fwhm / fwhm_to_sigma_factor();
1156 x.iter()
1157 .map(|&xi| {
1158 let mut y = bg;
1159 if sigma != 0.0 {
1160 let dhelp = (xi - centroid) / sigma;
1161 if dhelp <= 20.0 {
1162 y += height * (-0.5 * dhelp * dhelp).exp();
1163 }
1164 }
1165 y
1166 })
1167 .collect()
1168}
1169
1170pub fn gaussian_area_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1176 let (area, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
1177 let sigma = fwhm / fwhm_to_sigma_factor();
1178 let sqrt2pi = (2.0 * std::f64::consts::PI).sqrt();
1179 x.iter()
1180 .map(|&xi| {
1181 let mut y = bg;
1182 if sigma != 0.0 {
1183 let height = area / (sigma * sqrt2pi);
1184 let dhelp = (xi - centroid) / sigma;
1185 if dhelp <= 35.0 {
1186 y += height * (-0.5 * dhelp * dhelp).exp();
1187 }
1188 }
1189 y
1190 })
1191 .collect()
1192}
1193
1194pub fn lorentzian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1199 let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
1200 x.iter()
1201 .map(|&xi| {
1202 let mut y = bg;
1203 if fwhm != 0.0 {
1204 let dhelp = (xi - centroid) / (0.5 * fwhm);
1205 y += height / (1.0 + dhelp * dhelp);
1206 }
1207 y
1208 })
1209 .collect()
1210}
1211
1212pub fn pseudo_voigt_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1219 let (height, centroid, fwhm, eta, bg) = (params[0], params[1], params[2], params[3], params[4]);
1220 let sigma = fwhm / fwhm_to_sigma_factor();
1221 x.iter()
1222 .map(|&xi| {
1223 let mut y = bg;
1224 if fwhm != 0.0 {
1225 let dl = (xi - centroid) / (0.5 * fwhm);
1227 y += eta * height / (1.0 + dl * dl);
1228 }
1229 if sigma != 0.0 {
1230 let dg = (xi - centroid) / sigma;
1232 if dg <= 35.0 {
1233 y += (1.0 - eta) * height * (-0.5 * dg * dg).exp();
1234 }
1235 }
1236 y
1237 })
1238 .collect()
1239}
1240
1241fn erf(x: f64) -> f64 {
1261 if x == 0.0 {
1262 return 0.0;
1263 }
1264 let sign = if x < 0.0 { -1.0 } else { 1.0 };
1265 let x = x.abs();
1266 const A1: f64 = 0.254829592;
1268 const A2: f64 = -0.284496736;
1269 const A3: f64 = 1.421413741;
1270 const A4: f64 = -1.453152027;
1271 const A5: f64 = 1.061405429;
1272 const P: f64 = 0.3275911;
1273 let t = 1.0 / (1.0 + P * x);
1274 let poly = ((((A5 * t + A4) * t + A3) * t + A2) * t + A1) * t;
1275 sign * (1.0 - poly * (-x * x).exp())
1276}
1277
1278fn erfc(x: f64) -> f64 {
1280 1.0 - erf(x)
1281}
1282
1283fn step_denom(fwhm: f64) -> f64 {
1286 fwhm * std::f64::consts::SQRT_2 / fwhm_to_sigma_factor()
1287}
1288
1289pub fn stepdown_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1296 let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
1297 let denom = step_denom(fwhm);
1298 x.iter()
1299 .map(|&xi| {
1300 let mut y = bg;
1301 if denom != 0.0 {
1302 y += height * 0.5 * erfc((xi - centroid) / denom);
1303 }
1304 y
1305 })
1306 .collect()
1307}
1308
1309pub fn stepup_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1314 let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
1315 let denom = step_denom(fwhm);
1316 x.iter()
1317 .map(|&xi| {
1318 let mut y = bg;
1319 if denom != 0.0 {
1320 y += height * 0.5 * (1.0 + erf((xi - centroid) / denom));
1321 }
1322 y
1323 })
1324 .collect()
1325}
1326
1327pub fn slit_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1335 let (height, position, fwhm, beamfwhm, bg) =
1336 (params[0], params[1], params[2], params[3], params[4]);
1337 let denom = step_denom(beamfwhm);
1338 let c1 = position - 0.5 * fwhm;
1339 let c2 = position + 0.5 * fwhm;
1340 x.iter()
1341 .map(|&xi| {
1342 let mut y = bg;
1343 if denom != 0.0 {
1344 y += height * 0.25 * (1.0 + erf((xi - c1) / denom)) * erfc((xi - c2) / denom);
1345 }
1346 y
1347 })
1348 .collect()
1349}
1350
1351pub fn atan_stepup_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1357 let (height, position, width, bg) = (params[0], params[1], params[2], params[3]);
1358 x.iter()
1359 .map(|&xi| {
1360 let mut y = bg;
1361 if width != 0.0 {
1362 y += height * (0.5 + ((xi - position) / width).atan() / std::f64::consts::PI);
1363 }
1364 y
1365 })
1366 .collect()
1367}
1368
1369pub fn estimate_height_position_fwhm(x: &[f64], y: &[f64]) -> Option<(f64, f64, f64, f64)> {
1388 if x.len() != y.len() || x.len() < 3 {
1389 return None;
1390 }
1391 let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
1392 let mut max_y = f64::NEG_INFINITY;
1393 let mut max_idx = 0;
1394 for (i, &yi) in y.iter().enumerate() {
1395 if yi > max_y {
1396 max_y = yi;
1397 max_idx = i;
1398 }
1399 }
1400 let height = max_y - bg;
1401 let centroid = x[max_idx];
1402 let half_max = bg + height / 2.0;
1403 let mut left = max_idx;
1404 while left > 0 && y[left] > half_max {
1405 left -= 1;
1406 }
1407 let mut right = max_idx;
1408 while right < y.len() - 1 && y[right] > half_max {
1409 right += 1;
1410 }
1411 let fwhm = if right > left {
1412 x[right] - x[left]
1413 } else {
1414 (x[x.len() - 1] - x[0]).abs() / 4.0
1415 };
1416 let fwhm = if fwhm > 0.0 {
1417 fwhm
1418 } else {
1419 (x[x.len() - 1] - x[0]).abs() / 4.0
1420 };
1421 Some((height, centroid, fwhm, bg))
1422}
1423
1424pub fn estimate_gaussian(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1426 let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
1427 Some(vec![h, c, f, bg])
1428}
1429
1430pub fn estimate_gaussian_area(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1435 let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
1436 let area = (2.0 * std::f64::consts::PI).sqrt() * h * f / fwhm_to_sigma_factor();
1437 Some(vec![area, c, f, bg])
1438}
1439
1440pub fn estimate_lorentzian(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1445 let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
1446 Some(vec![h, c, f, bg])
1447}
1448
1449pub fn estimate_pseudo_voigt(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1453 let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
1454 Some(vec![h, c, f, 0.5, bg])
1455}
1456
1457fn convolve_valid(y: &[f64], kernel: &[f64]) -> Vec<f64> {
1461 let (n, m) = (y.len(), kernel.len());
1462 if n < m || m == 0 {
1463 return Vec::new();
1464 }
1465 (0..=n - m)
1466 .map(|k| (0..m).map(|j| y[k + j] * kernel[m - 1 - j]).sum())
1467 .collect()
1468}
1469
1470fn estimate_step(x: &[f64], y: &[f64], kernel: &[f64]) -> Option<Vec<f64>> {
1480 if x.len() != y.len() || x.len() < 3 {
1481 return None;
1482 }
1483 let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
1484 let max_y = y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1485 let data_amplitude = max_y - bg;
1486
1487 let cutoff = kernel.len() / 2;
1488 let y_deriv = convolve_valid(y, kernel);
1489 let (center, fwhm) = if y_deriv.len() >= 3 && x.len() > 2 * cutoff {
1490 let x_slice = &x[cutoff..x.len() - cutoff];
1491 match estimate_height_position_fwhm(x_slice, &y_deriv) {
1492 Some((_h, c, f, _b)) => (c, f),
1493 None => step_fallback(x),
1494 }
1495 } else {
1496 step_fallback(x)
1497 };
1498 Some(vec![data_amplitude, center, fwhm, bg])
1499}
1500
1501fn step_fallback(x: &[f64]) -> (f64, f64) {
1504 let center = x[x.len() / 2];
1505 let dx = if x.len() > 1 { x[1] - x[0] } else { 1.0 };
1506 (center, 8.0 * dx)
1507}
1508
1509pub fn estimate_stepup(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1513 estimate_step(x, y, &[0.25, 0.75, 0.0, -0.75, -0.25])
1514}
1515
1516pub fn estimate_stepdown(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1520 estimate_step(x, y, &[-0.25, -0.75, 0.0, 0.75, 0.25])
1521}
1522
1523pub fn estimate_atan_stepup(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1528 estimate_stepup(x, y)
1529}
1530
1531pub fn estimate_slit(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1542 let up = estimate_stepup(x, y)?; let down = estimate_stepdown(x, y)?; let (center_up, fwhm_up) = (up[1], up[2]);
1545 let (center_down, fwhm_down) = (down[1], down[2]);
1546 let edge_distance = (center_down - center_up).abs();
1547
1548 let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
1549 let y_minus_bg: Vec<f64> = y.iter().map(|&yi| yi - bg).collect();
1550 let height = y_minus_bg.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1551
1552 let threshold = 0.5 * height;
1554 let first = y_minus_bg.iter().position(|&v| v >= threshold)?;
1555 let last = y_minus_bg.iter().rposition(|&v| v >= threshold)?;
1556 let position = (x[first] + x[last]) / 2.0;
1557 let fwhm = x[last] - x[first];
1558
1559 let mut beamfwhm = 0.5 * (fwhm_up + fwhm_down);
1561 beamfwhm = beamfwhm.min(edge_distance / 10.0);
1562 let xmin = x.iter().copied().fold(f64::INFINITY, f64::min);
1563 let xmax = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1564 beamfwhm = beamfwhm.max((xmax - xmin) * 3.0 / x.len() as f64);
1565
1566 Some(vec![height, position, fwhm, beamfwhm, bg])
1567}
1568
1569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1575pub enum PeakModel {
1576 Gaussian,
1578 GaussianArea,
1580 Lorentzian,
1582 PseudoVoigt,
1584 StepDown,
1586 StepUp,
1588 Slit,
1590 AtanStepUp,
1592}
1593
1594impl PeakModel {
1595 pub fn name(self) -> &'static str {
1597 match self {
1598 PeakModel::Gaussian => "Gaussian",
1599 PeakModel::GaussianArea => "Gaussian (Area)",
1600 PeakModel::Lorentzian => "Lorentzian",
1601 PeakModel::PseudoVoigt => "Pseudo-Voigt",
1602 PeakModel::StepDown => "Step Down",
1603 PeakModel::StepUp => "Step Up",
1604 PeakModel::Slit => "Slit",
1605 PeakModel::AtanStepUp => "Arctan Step Up",
1606 }
1607 }
1608
1609 pub fn param_names(self) -> Vec<String> {
1611 let owned = |s: &str| s.to_string();
1612 match self {
1613 PeakModel::Gaussian => vec![
1614 owned("Height"),
1615 owned("Center"),
1616 owned("FWHM"),
1617 owned("Background"),
1618 ],
1619 PeakModel::GaussianArea => vec![
1620 owned("Area"),
1621 owned("Center"),
1622 owned("FWHM"),
1623 owned("Background"),
1624 ],
1625 PeakModel::Lorentzian => vec![
1626 owned("Height"),
1627 owned("Center"),
1628 owned("FWHM"),
1629 owned("Background"),
1630 ],
1631 PeakModel::PseudoVoigt => vec![
1632 owned("Height"),
1633 owned("Center"),
1634 owned("FWHM"),
1635 owned("Eta"),
1636 owned("Background"),
1637 ],
1638 PeakModel::StepDown | PeakModel::StepUp => vec![
1639 owned("Height"),
1640 owned("Center"),
1641 owned("FWHM"),
1642 owned("Background"),
1643 ],
1644 PeakModel::Slit => vec![
1645 owned("Height"),
1646 owned("Center"),
1647 owned("FWHM"),
1648 owned("BeamFWHM"),
1649 owned("Background"),
1650 ],
1651 PeakModel::AtanStepUp => vec![
1652 owned("Height"),
1653 owned("Center"),
1654 owned("Width"),
1655 owned("Background"),
1656 ],
1657 }
1658 }
1659
1660 pub fn eval(self, x: &[f64], params: &[f64]) -> Vec<f64> {
1662 match self {
1663 PeakModel::Gaussian => gaussian_model(x, params),
1664 PeakModel::GaussianArea => gaussian_area_model(x, params),
1665 PeakModel::Lorentzian => lorentzian_model(x, params),
1666 PeakModel::PseudoVoigt => pseudo_voigt_model(x, params),
1667 PeakModel::StepDown => stepdown_model(x, params),
1668 PeakModel::StepUp => stepup_model(x, params),
1669 PeakModel::Slit => slit_model(x, params),
1670 PeakModel::AtanStepUp => atan_stepup_model(x, params),
1671 }
1672 }
1673
1674 pub fn estimate(self, x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
1676 match self {
1677 PeakModel::Gaussian => estimate_gaussian(x, y),
1678 PeakModel::GaussianArea => estimate_gaussian_area(x, y),
1679 PeakModel::Lorentzian => estimate_lorentzian(x, y),
1680 PeakModel::PseudoVoigt => estimate_pseudo_voigt(x, y),
1681 PeakModel::StepDown => estimate_stepdown(x, y),
1682 PeakModel::StepUp => estimate_stepup(x, y),
1683 PeakModel::Slit => estimate_slit(x, y),
1684 PeakModel::AtanStepUp => estimate_atan_stepup(x, y),
1685 }
1686 }
1687}
1688
1689#[derive(Debug, Clone)]
1692pub struct IterativeFitResult {
1693 pub fit: FitResult,
1695 pub solver: LeastSqResult,
1697}
1698
1699impl IterativeFitResult {
1700 pub fn std_errors(&self) -> Vec<f64> {
1702 self.solver.std_errors()
1703 }
1704
1705 pub fn reduced_chisq(&self) -> Option<f64> {
1707 self.solver.reduced_chisq
1708 }
1709}
1710
1711pub struct IterativeFit {
1717 pub model: PeakModel,
1719 pub max_iter: usize,
1721 pub deltachi: f64,
1723}
1724
1725impl IterativeFit {
1726 pub fn new(model: PeakModel) -> Self {
1729 Self {
1730 model,
1731 max_iter: DEFAULT_MAX_ITER,
1732 deltachi: DEFAULT_DELTACHI,
1733 }
1734 }
1735
1736 pub fn fit_full(&self, x: &[f64], y: &[f64]) -> Option<IterativeFitResult> {
1738 let p0 = self.model.estimate(x, y)?;
1739 let model = self.model;
1740 let solver = leastsq(
1741 |xx, pp| model.eval(xx, pp),
1742 x,
1743 y,
1744 &p0,
1745 None,
1746 self.max_iter,
1747 self.deltachi,
1748 )
1749 .ok()?;
1750 let y_fit = self.model.eval(x, &solver.parameters);
1751 let fit = FitResult {
1752 y_fit,
1753 parameters: solver.parameters.clone(),
1754 param_names: self.model.param_names(),
1755 };
1756 Some(IterativeFitResult { fit, solver })
1757 }
1758}
1759
1760impl FitFunction for IterativeFit {
1761 fn name(&self) -> &str {
1762 self.model.name()
1763 }
1764
1765 fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult> {
1766 self.fit_full(x, y).map(|r| r.fit)
1767 }
1768}
1769
1770pub fn fit_in_range(
1777 xs: &[f64],
1778 ys: &[f64],
1779 xmin: f64,
1780 xmax: f64,
1781 model: &IterativeFit,
1782) -> Option<IterativeFitResult> {
1783 if xs.len() != ys.len() {
1784 return None;
1785 }
1786 let (lo, hi) = if xmin <= xmax {
1787 (xmin, xmax)
1788 } else {
1789 (xmax, xmin)
1790 };
1791 let mut xr = Vec::new();
1792 let mut yr = Vec::new();
1793 for (&xi, &yi) in xs.iter().zip(ys.iter()) {
1794 if xi >= lo && xi <= hi {
1795 xr.push(xi);
1796 yr.push(yi);
1797 }
1798 }
1799 if xr.len() < 3 {
1800 return None;
1801 }
1802 model.fit_full(&xr, &yr)
1803}
1804
1805pub fn multi_gaussian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
1824 let inv = 1.0 / fwhm_to_sigma_factor();
1825 let mut y = vec![0.0_f64; x.len()];
1826 for triple in params.chunks_exact(3) {
1827 let (height, centroid, fwhm) = (triple[0], triple[1], triple[2]);
1828 let sigma = fwhm * inv;
1829 if sigma == 0.0 {
1830 continue;
1831 }
1832 for (yi, &xi) in y.iter_mut().zip(x.iter()) {
1833 let dhelp = (xi - centroid) / sigma;
1834 if dhelp <= 20.0 {
1835 *yi += height * (-0.5 * dhelp * dhelp).exp();
1836 }
1837 }
1838 }
1839 y
1840}
1841
1842pub fn estimate_multi_gaussian(
1857 x: &[f64],
1858 y: &[f64],
1859 search_fwhm: f64,
1860 sensitivity: f64,
1861) -> Option<(Vec<f64>, Vec<Constraint>)> {
1862 let npoints = y.len();
1863 if npoints == 0 || x.len() != npoints {
1864 return None;
1865 }
1866 let search_fwhm = search_fwhm.max(3.0);
1867 let search_sens = sensitivity.max(1.0);
1868
1869 let found = crate::core::peaks::peak_search(y, search_fwhm, search_sens);
1870 let peaks: Vec<usize> = if found.is_empty() {
1871 let maxv = y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1873 match y.iter().position(|&v| v == maxv) {
1874 Some(p) => vec![p],
1875 None => return None,
1876 }
1877 } else {
1878 found.iter().map(|p| p.index).collect()
1879 };
1880 if peaks.is_empty() {
1881 return None;
1882 }
1883
1884 let sig = 5.0 * (x[npoints - 1] - x[0]).abs() / npoints as f64;
1886 let mut param: Vec<f64> = Vec::with_capacity(peaks.len() * 3);
1887 let mut index_largest = 0usize;
1888 let mut height_largest = f64::NEG_INFINITY;
1889 for (k, &pi) in peaks.iter().enumerate() {
1890 let height = y[pi];
1891 let pos = if k == 0 && x[pi].abs() < 1.0e-16 {
1893 0.0
1894 } else {
1895 x[pi]
1896 };
1897 param.push(height);
1898 param.push(pos);
1899 param.push(sig);
1900 if height > height_largest {
1901 height_largest = height;
1902 index_largest = k;
1903 }
1904 }
1905 let _ = index_largest; let sf = search_fwhm as usize;
1910 let (fwhmx, use_fwhmx) = if x.len() > sf {
1911 ((x[sf] - x[0]).abs(), true)
1912 } else {
1913 (0.0, false)
1914 };
1915 let xmin = x.iter().copied().fold(f64::INFINITY, f64::min);
1916 let xmax = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1917 let mut prelim: Vec<Constraint> = Vec::with_capacity(param.len());
1918 for k in 0..peaks.len() {
1919 let pos = param[3 * k + 1];
1920 prelim.push(Constraint::Positive);
1921 if use_fwhmx && fwhmx > 0.0 {
1922 prelim.push(Constraint::Quoted {
1923 min: pos - 0.5 * fwhmx,
1924 max: pos + 0.5 * fwhmx,
1925 });
1926 } else if xmax > xmin {
1927 prelim.push(Constraint::Quoted {
1928 min: xmin,
1929 max: xmax,
1930 });
1931 } else {
1932 prelim.push(Constraint::Free);
1933 }
1934 prelim.push(Constraint::Positive);
1935 }
1936
1937 let fittedpar = leastsq_constrained(
1939 multi_gaussian_model,
1940 x,
1941 y,
1942 ¶m,
1943 &prelim,
1944 None,
1945 4,
1946 DEFAULT_DELTACHI,
1947 )
1948 .map(|r| r.parameters)
1949 .unwrap_or(param);
1950
1951 let mut cons: Vec<Constraint> = Vec::with_capacity(fittedpar.len());
1953 for _ in 0..peaks.len() {
1954 cons.push(Constraint::Positive);
1955 cons.push(Constraint::Free);
1956 cons.push(Constraint::Positive);
1957 }
1958
1959 Some((fittedpar, cons))
1960}
1961
1962pub fn fit_multi_gaussian(
1972 x: &[f64],
1973 y: &[f64],
1974 search_fwhm: f64,
1975 sensitivity: f64,
1976 max_iter: usize,
1977 deltachi: f64,
1978) -> Option<LeastSqResult> {
1979 let (seeds, cons) = estimate_multi_gaussian(x, y, search_fwhm, sensitivity)?;
1980 leastsq_constrained(
1981 multi_gaussian_model,
1982 x,
1983 y,
1984 &seeds,
1985 &cons,
1986 None,
1987 max_iter,
1988 deltachi,
1989 )
1990 .ok()
1991}
1992
1993pub fn fit_multi_gaussian_full(
2002 x: &[f64],
2003 y: &[f64],
2004 search_fwhm: f64,
2005 sensitivity: f64,
2006 max_iter: usize,
2007 deltachi: f64,
2008) -> Option<IterativeFitResult> {
2009 let solver = fit_multi_gaussian(x, y, search_fwhm, sensitivity, max_iter, deltachi)?;
2010 let y_fit = multi_gaussian_model(x, &solver.parameters);
2011 let mut param_names = Vec::with_capacity(solver.parameters.len());
2012 for peak in 0..solver.parameters.len() / 3 {
2013 let i = peak + 1;
2014 param_names.push(format!("Height {i}"));
2015 param_names.push(format!("Center {i}"));
2016 param_names.push(format!("FWHM {i}"));
2017 }
2018 let fit = FitResult {
2019 y_fit,
2020 parameters: solver.parameters.clone(),
2021 param_names,
2022 };
2023 Some(IterativeFitResult { fit, solver })
2024}
2025
2026pub fn fit_peak_from(
2036 model: PeakModel,
2037 x: &[f64],
2038 y: &[f64],
2039 p0: &[f64],
2040 constraints: &[Constraint],
2041 max_iter: usize,
2042 deltachi: f64,
2043) -> Option<IterativeFitResult> {
2044 if constraints.len() != p0.len() {
2045 return None;
2046 }
2047 let solver = leastsq_constrained(
2048 |xx, pp| model.eval(xx, pp),
2049 x,
2050 y,
2051 p0,
2052 constraints,
2053 None,
2054 max_iter,
2055 deltachi,
2056 )
2057 .ok()?;
2058 let y_fit = model.eval(x, &solver.parameters);
2059 let fit = FitResult {
2060 y_fit,
2061 parameters: solver.parameters.clone(),
2062 param_names: model.param_names(),
2063 };
2064 Some(IterativeFitResult { fit, solver })
2065}
2066
2067pub fn fit_peak_constrained(
2074 model: PeakModel,
2075 x: &[f64],
2076 y: &[f64],
2077 constraints: &[Constraint],
2078 max_iter: usize,
2079 deltachi: f64,
2080) -> Option<IterativeFitResult> {
2081 let p0 = model.estimate(x, y)?;
2082 fit_peak_from(model, x, y, &p0, constraints, max_iter, deltachi)
2083}
2084
2085#[derive(Debug, Clone)]
2089pub struct BackgroundPeakFit {
2090 pub peak: IterativeFitResult,
2094 pub background: Vec<f64>,
2097 pub total: Vec<f64>,
2099}
2100
2101pub fn fit_peak_with_background(
2126 model: PeakModel,
2127 background: crate::core::background::Background,
2128 x: &[f64],
2129 y: &[f64],
2130 max_iter: usize,
2131 deltachi: f64,
2132) -> Option<BackgroundPeakFit> {
2133 if x.is_empty() || x.len() != y.len() {
2134 return None;
2135 }
2136 let bg = background.compute(x, y);
2137 let residual: Vec<f64> = y.iter().zip(&bg).map(|(&yi, &bi)| yi - bi).collect();
2138 let fitter = IterativeFit {
2139 model,
2140 max_iter,
2141 deltachi,
2142 };
2143 let peak = fitter.fit_full(x, &residual)?;
2144 let total: Vec<f64> = bg
2145 .iter()
2146 .zip(&peak.fit.y_fit)
2147 .map(|(&bi, &fi)| bi + fi)
2148 .collect();
2149 Some(BackgroundPeakFit {
2150 peak,
2151 background: bg,
2152 total,
2153 })
2154}
2155
2156#[cfg(test)]
2157mod tests {
2158 use super::*;
2159 use crate::core::peaks::DEFAULT_PEAK_SENSITIVITY;
2160
2161 fn synth_gaussian(xs: &[f64], height: f64, center: f64, fwhm: f64, bg: f64) -> Vec<f64> {
2163 gaussian_model(xs, &[height, center, fwhm, bg])
2164 }
2165
2166 fn linspace(a: f64, b: f64, n: usize) -> Vec<f64> {
2167 (0..n)
2168 .map(|i| a + (b - a) * (i as f64) / ((n - 1) as f64))
2169 .collect()
2170 }
2171
2172 #[test]
2173 fn invert_identity() {
2174 let id = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
2175 let inv = invert_matrix(&id).unwrap();
2176 assert_eq!(inv, id);
2177 }
2178
2179 #[test]
2180 fn invert_known_2x2() {
2181 let m = vec![vec![4.0, 7.0], vec![2.0, 6.0]];
2183 let inv = invert_matrix(&m).unwrap();
2184 let expected = [[0.6, -0.7], [-0.2, 0.4]];
2185 for i in 0..2 {
2186 for j in 0..2 {
2187 assert!((inv[i][j] - expected[i][j]).abs() < 1e-12);
2188 }
2189 }
2190 }
2191
2192 #[test]
2193 fn invert_singular_returns_none() {
2194 let m = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
2195 assert!(invert_matrix(&m).is_none());
2196 }
2197
2198 #[test]
2199 fn leastsq_recovers_noiseless_line_exactly() {
2200 let xs = linspace(-5.0, 5.0, 21);
2202 let (a_true, b_true) = (2.5, -1.0);
2203 let ys: Vec<f64> = xs.iter().map(|&x| a_true * x + b_true).collect();
2204 let model = |x: &[f64], p: &[f64]| x.iter().map(|&xi| p[0] * xi + p[1]).collect::<Vec<_>>();
2205 let res = leastsq(
2206 model,
2207 &xs,
2208 &ys,
2209 &[0.0, 0.0],
2210 None,
2211 DEFAULT_MAX_ITER,
2212 DEFAULT_DELTACHI,
2213 )
2214 .unwrap();
2215 assert!(
2216 (res.parameters[0] - a_true).abs() < 1e-6,
2217 "slope {} vs {}",
2218 res.parameters[0],
2219 a_true
2220 );
2221 assert!(
2222 (res.parameters[1] - b_true).abs() < 1e-6,
2223 "intercept {} vs {}",
2224 res.parameters[1],
2225 b_true
2226 );
2227 assert!(res.chisq < 1e-12, "chisq {}", res.chisq);
2229 }
2230
2231 #[test]
2232 fn leastsq_converges_on_noisy_gaussian() {
2233 let xs = linspace(-3.0, 7.0, 101);
2236 let clean = synth_gaussian(&xs, 10.0, 2.0, 1.5, 1.0);
2237 let ys: Vec<f64> = clean
2239 .iter()
2240 .enumerate()
2241 .map(|(i, &c)| c + 0.05 * ((i as f64) * 0.7).sin())
2242 .collect();
2243 let fit = IterativeFit::new(PeakModel::Gaussian)
2244 .fit_full(&xs, &ys)
2245 .expect("fit should succeed");
2246 let p = &fit.fit.parameters;
2247 assert!((p[0] - 10.0).abs() < 0.2, "height {}", p[0]);
2248 assert!((p[1] - 2.0).abs() < 0.05, "center {}", p[1]);
2249 assert!((p[2] - 1.5).abs() < 0.1, "fwhm {}", p[2]);
2250 assert!((p[3] - 1.0).abs() < 0.1, "bg {}", p[3]);
2251 let rc = fit.reduced_chisq().unwrap();
2254 assert!(rc < 0.01, "reduced chisq {}", rc);
2255 }
2256
2257 #[test]
2258 fn gaussian_model_recovers_own_peak() {
2259 let xs = linspace(0.0, 20.0, 201);
2260 let ys = synth_gaussian(&xs, 5.0, 8.0, 2.0, 0.5);
2261 let fit = IterativeFit::new(PeakModel::Gaussian)
2262 .fit_full(&xs, &ys)
2263 .unwrap();
2264 let p = &fit.fit.parameters;
2265 assert!((p[0] - 5.0).abs() < 1e-3, "height {}", p[0]);
2266 assert!((p[1] - 8.0).abs() < 1e-3, "center {}", p[1]);
2267 assert!((p[2] - 2.0).abs() < 1e-3, "fwhm {}", p[2]);
2268 assert!((p[3] - 0.5).abs() < 1e-3, "bg {}", p[3]);
2269 assert!(fit.reduced_chisq().unwrap() < 1e-6);
2271 }
2272
2273 #[test]
2274 fn gaussian_area_model_recovers_own_peak() {
2275 let xs = linspace(0.0, 20.0, 201);
2277 let area = 12.0;
2278 let ys = gaussian_area_model(&xs, &[area, 9.0, 2.5, 0.2]);
2279 let fit = IterativeFit::new(PeakModel::GaussianArea)
2280 .fit_full(&xs, &ys)
2281 .unwrap();
2282 let p = &fit.fit.parameters;
2283 assert!((p[0] - area).abs() < 1e-2, "area {}", p[0]);
2284 assert!((p[1] - 9.0).abs() < 1e-3, "center {}", p[1]);
2285 assert!((p[2] - 2.5).abs() < 1e-3, "fwhm {}", p[2]);
2286 assert!((p[3] - 0.2).abs() < 1e-3, "bg {}", p[3]);
2287 assert!(fit.reduced_chisq().unwrap() < 1e-6);
2288 }
2289
2290 #[test]
2291 fn lorentzian_model_recovers_own_peak() {
2292 let xs = linspace(0.0, 20.0, 201);
2293 let ys = lorentzian_model(&xs, &[7.0, 11.0, 3.0, 1.0]);
2294 let fit = IterativeFit::new(PeakModel::Lorentzian)
2295 .fit_full(&xs, &ys)
2296 .unwrap();
2297 let p = &fit.fit.parameters;
2298 assert!((p[0] - 7.0).abs() < 1e-2, "height {}", p[0]);
2299 assert!((p[1] - 11.0).abs() < 1e-3, "center {}", p[1]);
2300 assert!((p[2] - 3.0).abs() < 1e-2, "fwhm {}", p[2]);
2301 assert!((p[3] - 1.0).abs() < 1e-2, "bg {}", p[3]);
2302 assert!(fit.reduced_chisq().unwrap() < 1e-6);
2303 }
2304
2305 #[test]
2306 fn pseudo_voigt_model_recovers_own_peak() {
2307 let xs = linspace(0.0, 20.0, 301);
2308 let ys = pseudo_voigt_model(&xs, &[6.0, 10.0, 2.0, 0.4, 0.5]);
2309 let fit = IterativeFit::new(PeakModel::PseudoVoigt)
2310 .fit_full(&xs, &ys)
2311 .unwrap();
2312 let p = &fit.fit.parameters;
2313 assert!((p[0] - 6.0).abs() < 5e-2, "height {}", p[0]);
2314 assert!((p[1] - 10.0).abs() < 1e-2, "center {}", p[1]);
2315 assert!((p[2] - 2.0).abs() < 5e-2, "fwhm {}", p[2]);
2316 assert!((p[3] - 0.4).abs() < 5e-2, "eta {}", p[3]);
2317 assert!((p[4] - 0.5).abs() < 5e-2, "bg {}", p[4]);
2318 assert!(fit.reduced_chisq().unwrap() < 1e-4);
2319 }
2320
2321 #[test]
2322 fn pseudo_voigt_eta_limits_match_gauss_and_lorentz() {
2323 let xs = linspace(0.0, 10.0, 51);
2325 let g = gaussian_model(&xs, &[3.0, 5.0, 2.0, 0.0]);
2326 let pv_g = pseudo_voigt_model(&xs, &[3.0, 5.0, 2.0, 0.0, 0.0]);
2327 for (a, b) in g.iter().zip(pv_g.iter()) {
2328 assert!((a - b).abs() < 1e-12);
2329 }
2330 let l = lorentzian_model(&xs, &[3.0, 5.0, 2.0, 0.0]);
2331 let pv_l = pseudo_voigt_model(&xs, &[3.0, 5.0, 2.0, 1.0, 0.0]);
2332 for (a, b) in l.iter().zip(pv_l.iter()) {
2333 assert!((a - b).abs() < 1e-12);
2334 }
2335 }
2336
2337 #[test]
2338 fn fit_in_range_ignores_outside_points() {
2339 let xs = linspace(0.0, 20.0, 201);
2343 let in_range: Vec<f64> = xs
2344 .iter()
2345 .map(|&x| {
2346 if (4.0..=12.0).contains(&x) {
2347 let sigma = 2.0 / fwhm_to_sigma_factor();
2349 let d = (x - 8.0) / sigma;
2350 5.0 * (-0.5 * d * d).exp() + 0.5
2351 } else {
2352 100.0 + 50.0 * x
2354 }
2355 })
2356 .collect();
2357 let fitter = IterativeFit::new(PeakModel::Gaussian);
2358 let res = fit_in_range(&xs, &in_range, 4.0, 12.0, &fitter).unwrap();
2359 let p = &res.fit.parameters;
2360 assert!((p[1] - 8.0).abs() < 0.05, "center pulled to {}", p[1]);
2361 assert!((p[2] - 2.0).abs() < 0.1, "fwhm {}", p[2]);
2362 assert!((p[0] - 5.0).abs() < 0.2, "height {}", p[0]);
2363 }
2364
2365 #[test]
2366 fn fit_in_range_reversed_bounds_equivalent() {
2367 let xs = linspace(0.0, 20.0, 201);
2368 let ys = synth_gaussian(&xs, 4.0, 10.0, 2.0, 0.3);
2369 let fitter = IterativeFit::new(PeakModel::Gaussian);
2370 let a = fit_in_range(&xs, &ys, 6.0, 14.0, &fitter).unwrap();
2371 let b = fit_in_range(&xs, &ys, 14.0, 6.0, &fitter).unwrap();
2372 for (pa, pb) in a.fit.parameters.iter().zip(b.fit.parameters.iter()) {
2373 assert!((pa - pb).abs() < 1e-12);
2374 }
2375 }
2376
2377 #[test]
2378 fn std_errors_from_covariance_diagonal() {
2379 let res = LeastSqResult {
2382 parameters: vec![1.0, 2.0, 3.0],
2383 covariance: vec![
2384 vec![4.0, 0.1, 0.0],
2385 vec![0.1, 9.0, 0.2],
2386 vec![0.0, 0.2, 16.0],
2387 ],
2388 uncertainties: vec![2.0, 3.0, 4.0],
2389 chisq: 0.0,
2390 reduced_chisq: Some(0.0),
2391 niter: 1,
2392 nfev: 1,
2393 };
2394 let errs = res.std_errors();
2395 assert!((errs[0] - 2.0).abs() < 1e-12);
2396 assert!((errs[1] - 3.0).abs() < 1e-12);
2397 assert!((errs[2] - 4.0).abs() < 1e-12);
2398 }
2399
2400 #[test]
2401 fn std_errors_guard_negative_diagonal() {
2402 let res = LeastSqResult {
2404 parameters: vec![1.0],
2405 covariance: vec![vec![-1e-15]],
2406 uncertainties: vec![0.0],
2407 chisq: 0.0,
2408 reduced_chisq: None,
2409 niter: 0,
2410 nfev: 0,
2411 };
2412 let e = res.std_errors();
2413 assert!(e[0].is_finite() && e[0] >= 0.0);
2414 }
2415
2416 #[test]
2417 fn leastsq_length_mismatch_errors() {
2418 let r = leastsq(
2419 |x: &[f64], _p: &[f64]| x.to_vec(),
2420 &[1.0, 2.0, 3.0],
2421 &[1.0, 2.0],
2422 &[0.0],
2423 None,
2424 10,
2425 DEFAULT_DELTACHI,
2426 );
2427 assert_eq!(r.unwrap_err(), FitError::LengthMismatch);
2428 }
2429
2430 #[test]
2431 fn leastsq_rejects_nonfinite() {
2432 let r = leastsq(
2433 |x: &[f64], p: &[f64]| x.iter().map(|&xi| p[0] * xi).collect::<Vec<_>>(),
2434 &[1.0, f64::NAN, 3.0],
2435 &[1.0, 2.0, 3.0],
2436 &[1.0],
2437 None,
2438 10,
2439 DEFAULT_DELTACHI,
2440 );
2441 assert_eq!(r.unwrap_err(), FitError::NonFinite);
2442 }
2443
2444 #[test]
2445 fn estimate_seeds_are_close() {
2446 let xs = linspace(0.0, 20.0, 201);
2447 let ys = synth_gaussian(&xs, 5.0, 8.0, 2.0, 0.5);
2448 let (h, c, f, bg) = estimate_height_position_fwhm(&xs, &ys).unwrap();
2449 assert!((h - 5.0).abs() < 0.5, "height seed {}", h);
2450 assert!((c - 8.0).abs() < 0.2, "center seed {}", c);
2451 assert!((f - 2.0).abs() < 0.5, "fwhm seed {}", f);
2452 assert!((bg - 0.5).abs() < 0.1, "bg seed {}", bg);
2453 }
2454
2455 fn line(x: &[f64], p: &[f64]) -> Vec<f64> {
2459 x.iter().map(|&xi| p[0] * xi + p[1]).collect()
2460 }
2461 fn constant(x: &[f64], p: &[f64]) -> Vec<f64> {
2463 vec![p[0]; x.len()]
2464 }
2465
2466 #[test]
2467 fn constrained_all_free_matches_unconstrained() {
2468 let xs = linspace(-5.0, 5.0, 21);
2469 let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
2470 let free = leastsq_constrained(
2471 line,
2472 &xs,
2473 &ys,
2474 &[0.0, 0.0],
2475 &[Constraint::Free, Constraint::Free],
2476 None,
2477 DEFAULT_MAX_ITER,
2478 DEFAULT_DELTACHI,
2479 )
2480 .unwrap();
2481 let plain = leastsq(
2482 line,
2483 &xs,
2484 &ys,
2485 &[0.0, 0.0],
2486 None,
2487 DEFAULT_MAX_ITER,
2488 DEFAULT_DELTACHI,
2489 )
2490 .unwrap();
2491 assert!((free.parameters[0] - plain.parameters[0]).abs() < 1e-6);
2492 assert!((free.parameters[1] - plain.parameters[1]).abs() < 1e-6);
2493 assert!((free.parameters[0] - 2.5).abs() < 1e-6);
2494 assert!((free.parameters[1] + 1.0).abs() < 1e-6);
2495 }
2496
2497 #[test]
2498 fn constrained_fixed_holds_parameter() {
2499 let xs = linspace(-5.0, 5.0, 21);
2500 let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
2501 let res = leastsq_constrained(
2503 line,
2504 &xs,
2505 &ys,
2506 &[0.0, -1.0],
2507 &[Constraint::Free, Constraint::Fixed],
2508 None,
2509 DEFAULT_MAX_ITER,
2510 DEFAULT_DELTACHI,
2511 )
2512 .unwrap();
2513 assert_eq!(res.parameters[1], -1.0, "fixed b must not move");
2514 assert!(
2515 (res.parameters[0] - 2.5).abs() < 1e-6,
2516 "a {}",
2517 res.parameters[0]
2518 );
2519 }
2520
2521 #[test]
2522 fn constrained_fixed_gets_full_uncertainty() {
2523 let xs = linspace(-5.0, 5.0, 21);
2524 let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
2525 let res = leastsq_constrained(
2526 line,
2527 &xs,
2528 &ys,
2529 &[0.0, -1.0],
2530 &[Constraint::Free, Constraint::Fixed],
2531 None,
2532 DEFAULT_MAX_ITER,
2533 DEFAULT_DELTACHI,
2534 )
2535 .unwrap();
2536 assert_eq!(res.uncertainties[1], res.parameters[1]);
2538 assert!((res.covariance[1][1] - res.parameters[1] * res.parameters[1]).abs() < 1e-12);
2539 }
2540
2541 #[test]
2542 fn constrained_positive_enforces_and_recovers() {
2543 let xs = linspace(0.0, 10.0, 21);
2544 let neg: Vec<f64> = vec![-3.0; xs.len()];
2547 let r_neg = leastsq_constrained(
2548 constant,
2549 &xs,
2550 &neg,
2551 &[1.0],
2552 &[Constraint::Positive],
2553 None,
2554 DEFAULT_MAX_ITER,
2555 DEFAULT_DELTACHI,
2556 )
2557 .unwrap();
2558 assert!(
2559 r_neg.parameters[0] >= 0.0,
2560 "positive violated: {}",
2561 r_neg.parameters[0]
2562 );
2563 assert!(
2564 r_neg.chisq > 1.0,
2565 "constraint should prevent fitting the negative target, chisq {}",
2566 r_neg.chisq
2567 );
2568 let pos: Vec<f64> = vec![4.0; xs.len()];
2570 let r_pos = leastsq_constrained(
2571 constant,
2572 &xs,
2573 &pos,
2574 &[1.0],
2575 &[Constraint::Positive],
2576 None,
2577 DEFAULT_MAX_ITER,
2578 DEFAULT_DELTACHI,
2579 )
2580 .unwrap();
2581 assert!(
2582 (r_pos.parameters[0] - 4.0).abs() < 0.1,
2583 "recover {}",
2584 r_pos.parameters[0]
2585 );
2586 }
2587
2588 #[test]
2589 fn constrained_quoted_clamps_to_bounds() {
2590 let xs = linspace(0.0, 10.0, 21);
2591 let high: Vec<f64> = vec![10.0; xs.len()];
2593 let r_hi = leastsq_constrained(
2594 constant,
2595 &xs,
2596 &high,
2597 &[2.5],
2598 &[Constraint::Quoted { min: 0.0, max: 5.0 }],
2599 None,
2600 DEFAULT_MAX_ITER,
2601 DEFAULT_DELTACHI,
2602 )
2603 .unwrap();
2604 assert!(
2605 (0.0..=5.0).contains(&r_hi.parameters[0]),
2606 "out of bounds: {}",
2607 r_hi.parameters[0]
2608 );
2609 assert!(
2610 r_hi.parameters[0] > 4.5,
2611 "did not saturate near 5: {}",
2612 r_hi.parameters[0]
2613 );
2614 let mid: Vec<f64> = vec![3.0; xs.len()];
2616 let r_mid = leastsq_constrained(
2617 constant,
2618 &xs,
2619 &mid,
2620 &[2.5],
2621 &[Constraint::Quoted { min: 0.0, max: 5.0 }],
2622 None,
2623 DEFAULT_MAX_ITER,
2624 DEFAULT_DELTACHI,
2625 )
2626 .unwrap();
2627 assert!(
2628 (r_mid.parameters[0] - 3.0).abs() < 0.05,
2629 "recover {}",
2630 r_mid.parameters[0]
2631 );
2632 }
2633
2634 #[test]
2635 fn constrained_factor_ties_parameters() {
2636 let xs = linspace(-5.0, 5.0, 21);
2637 let ys: Vec<f64> = xs.iter().map(|&x| 3.0 * x + 6.0).collect();
2639 let res = leastsq_constrained(
2640 line,
2641 &xs,
2642 &ys,
2643 &[1.0, 0.0],
2644 &[
2645 Constraint::Free,
2646 Constraint::Factor {
2647 reference: 0,
2648 factor: 2.0,
2649 },
2650 ],
2651 None,
2652 DEFAULT_MAX_ITER,
2653 DEFAULT_DELTACHI,
2654 )
2655 .unwrap();
2656 assert!(
2657 (res.parameters[0] - 3.0).abs() < 1e-4,
2658 "a {}",
2659 res.parameters[0]
2660 );
2661 assert!(
2662 (res.parameters[1] - 6.0).abs() < 1e-4,
2663 "b {}",
2664 res.parameters[1]
2665 );
2666 assert!(
2667 (res.parameters[1] - 2.0 * res.parameters[0]).abs() < 1e-9,
2668 "tie broken"
2669 );
2670 }
2671
2672 #[test]
2673 fn constrained_delta_ties_parameters() {
2674 let xs = linspace(-5.0, 5.0, 21);
2675 let ys: Vec<f64> = xs.iter().map(|&x| 2.0 * x + 7.0).collect();
2677 let res = leastsq_constrained(
2678 line,
2679 &xs,
2680 &ys,
2681 &[0.0, 0.0],
2682 &[
2683 Constraint::Free,
2684 Constraint::Delta {
2685 reference: 0,
2686 delta: 5.0,
2687 },
2688 ],
2689 None,
2690 DEFAULT_MAX_ITER,
2691 DEFAULT_DELTACHI,
2692 )
2693 .unwrap();
2694 assert!(
2695 (res.parameters[0] - 2.0).abs() < 1e-4,
2696 "a {}",
2697 res.parameters[0]
2698 );
2699 assert!(
2700 (res.parameters[1] - res.parameters[0] - 5.0).abs() < 1e-9,
2701 "tie broken"
2702 );
2703 }
2704
2705 #[test]
2706 fn constrained_sum_ties_parameters() {
2707 let xs = linspace(-5.0, 5.0, 21);
2708 let ys: Vec<f64> = xs.iter().map(|&x| 4.0 * x + 6.0).collect();
2710 let res = leastsq_constrained(
2711 line,
2712 &xs,
2713 &ys,
2714 &[0.0, 0.0],
2715 &[
2716 Constraint::Free,
2717 Constraint::Sum {
2718 reference: 0,
2719 sum: 10.0,
2720 },
2721 ],
2722 None,
2723 DEFAULT_MAX_ITER,
2724 DEFAULT_DELTACHI,
2725 )
2726 .unwrap();
2727 assert!(
2728 (res.parameters[0] - 4.0).abs() < 1e-4,
2729 "a {}",
2730 res.parameters[0]
2731 );
2732 assert!(
2733 (res.parameters[0] + res.parameters[1] - 10.0).abs() < 1e-9,
2734 "tie broken"
2735 );
2736 }
2737
2738 #[test]
2739 fn constrained_rejects_bad_spec() {
2740 let xs = linspace(0.0, 4.0, 5);
2741 let ys = vec![1.0; 5];
2742 assert_eq!(
2744 leastsq_constrained(constant, &xs, &ys, &[1.0], &[], None, 10, DEFAULT_DELTACHI)
2745 .unwrap_err(),
2746 FitError::BadConstraintReference
2747 );
2748 assert_eq!(
2750 leastsq_constrained(
2751 constant,
2752 &xs,
2753 &ys,
2754 &[1.0],
2755 &[Constraint::Quoted { min: 5.0, max: 5.0 }],
2756 None,
2757 10,
2758 DEFAULT_DELTACHI,
2759 )
2760 .unwrap_err(),
2761 FitError::InvalidConstraint
2762 );
2763 assert_eq!(
2765 leastsq_constrained(
2766 line,
2767 &xs,
2768 &ys,
2769 &[1.0, 0.0],
2770 &[
2771 Constraint::Free,
2772 Constraint::Factor {
2773 reference: 9,
2774 factor: 2.0
2775 }
2776 ],
2777 None,
2778 10,
2779 DEFAULT_DELTACHI,
2780 )
2781 .unwrap_err(),
2782 FitError::BadConstraintReference
2783 );
2784 assert_eq!(
2786 leastsq_constrained(
2787 line,
2788 &xs,
2789 &ys,
2790 &[1.0, 2.0],
2791 &[Constraint::Fixed, Constraint::Fixed],
2792 None,
2793 10,
2794 DEFAULT_DELTACHI,
2795 )
2796 .unwrap_err(),
2797 FitError::NoFreeParameters
2798 );
2799 }
2800
2801 fn grid(n: usize) -> Vec<f64> {
2804 (0..n).map(|i| i as f64).collect()
2805 }
2806
2807 fn nearest_peak(params: &[f64], target: f64) -> [f64; 3] {
2809 params
2810 .chunks_exact(3)
2811 .min_by(|a, b| {
2812 (a[1] - target)
2813 .abs()
2814 .partial_cmp(&(b[1] - target).abs())
2815 .unwrap()
2816 })
2817 .map(|t| [t[0], t[1], t[2]])
2818 .unwrap()
2819 }
2820
2821 #[test]
2822 fn multi_gaussian_model_is_sum_of_single_gaussians() {
2823 let xs = grid(100);
2824 let a = gaussian_model(&xs, &[100.0, 30.0, 8.0, 0.0]);
2826 let b = gaussian_model(&xs, &[60.0, 70.0, 5.0, 0.0]);
2827 let sum = multi_gaussian_model(&xs, &[100.0, 30.0, 8.0, 60.0, 70.0, 5.0]);
2828 for i in 0..xs.len() {
2829 assert!((sum[i] - (a[i] + b[i])).abs() < 1e-9, "mismatch at {i}");
2830 }
2831 }
2832
2833 #[test]
2834 fn fit_multi_gaussian_recovers_two_peaks() {
2835 let xs = grid(100);
2836 let mut ys = gaussian_model(&xs, &[100.0, 30.0, 8.0, 0.0]);
2837 for (yi, g) in ys
2838 .iter_mut()
2839 .zip(gaussian_model(&xs, &[80.0, 70.0, 6.0, 0.0]))
2840 {
2841 *yi += g;
2842 }
2843 let res = fit_multi_gaussian(
2844 &xs,
2845 &ys,
2846 8.0,
2847 DEFAULT_PEAK_SENSITIVITY,
2848 DEFAULT_MAX_ITER,
2849 DEFAULT_DELTACHI,
2850 )
2851 .expect("multi-peak fit should succeed");
2852 assert!(res.parameters.len() >= 6, "expected >=2 peaks");
2853 let p1 = nearest_peak(&res.parameters, 30.0);
2854 let p2 = nearest_peak(&res.parameters, 70.0);
2855 assert!((p1[1] - 30.0).abs() < 1.0, "centre1 {}", p1[1]);
2856 assert!((p1[0] - 100.0).abs() < 5.0, "height1 {}", p1[0]);
2857 assert!((p1[2] - 8.0).abs() < 1.0, "fwhm1 {}", p1[2]);
2858 assert!((p2[1] - 70.0).abs() < 1.0, "centre2 {}", p2[1]);
2859 assert!((p2[0] - 80.0).abs() < 5.0, "height2 {}", p2[0]);
2860 assert!((p2[2] - 6.0).abs() < 1.0, "fwhm2 {}", p2[2]);
2861 }
2862
2863 #[test]
2864 fn fit_multi_gaussian_full_packages_names_errors_and_curve() {
2865 let xs = grid(100);
2866 let mut ys = gaussian_model(&xs, &[100.0, 30.0, 8.0, 0.0]);
2867 for (yi, g) in ys
2868 .iter_mut()
2869 .zip(gaussian_model(&xs, &[80.0, 70.0, 6.0, 0.0]))
2870 {
2871 *yi += g;
2872 }
2873 let ir = fit_multi_gaussian_full(
2874 &xs,
2875 &ys,
2876 8.0,
2877 DEFAULT_PEAK_SENSITIVITY,
2878 DEFAULT_MAX_ITER,
2879 DEFAULT_DELTACHI,
2880 )
2881 .expect("multi-peak fit should succeed");
2882 let n = ir.fit.parameters.len();
2883 assert!(n >= 6 && n.is_multiple_of(3), "param count {n}");
2884 assert_eq!(ir.fit.param_names.len(), n);
2886 assert_eq!(ir.std_errors().len(), n);
2887 assert_eq!(ir.fit.y_fit.len(), xs.len());
2888 assert_eq!(ir.fit.param_names[0], "Height 1");
2890 assert_eq!(ir.fit.param_names[1], "Center 1");
2891 assert_eq!(ir.fit.param_names[2], "FWHM 1");
2892 assert_eq!(ir.fit.y_fit, multi_gaussian_model(&xs, &ir.fit.parameters));
2894 assert!((nearest_peak(&ir.fit.parameters, 30.0)[1] - 30.0).abs() < 1.0);
2896 assert!((nearest_peak(&ir.fit.parameters, 70.0)[1] - 70.0).abs() < 1.0);
2897 }
2898
2899 #[test]
2900 fn fit_peak_constrained_all_free_recovers_peak() {
2901 let xs = grid(100);
2902 let ys = gaussian_model(&xs, &[60.0, 45.0, 7.0, 5.0]);
2903 let cons = vec![Constraint::Free; 4];
2904 let ir = fit_peak_constrained(
2905 PeakModel::Gaussian,
2906 &xs,
2907 &ys,
2908 &cons,
2909 DEFAULT_MAX_ITER,
2910 DEFAULT_DELTACHI,
2911 )
2912 .unwrap();
2913 let p = &ir.fit.parameters;
2914 assert!((p[1] - 45.0).abs() < 1.0, "centre {}", p[1]);
2915 assert!((p[2] - 7.0).abs() < 1.0, "fwhm {}", p[2]);
2916 assert_eq!(ir.fit.param_names, PeakModel::Gaussian.param_names());
2917 }
2918
2919 #[test]
2920 fn fit_peak_constrained_fixed_holds_param_at_estimate() {
2921 let xs = grid(100);
2922 let ys = gaussian_model(&xs, &[60.0, 45.0, 7.0, 5.0]);
2923 let p0 = PeakModel::Gaussian.estimate(&xs, &ys).unwrap();
2925 let mut cons = vec![Constraint::Free; 4];
2926 cons[1] = Constraint::Fixed; let ir = fit_peak_constrained(
2928 PeakModel::Gaussian,
2929 &xs,
2930 &ys,
2931 &cons,
2932 DEFAULT_MAX_ITER,
2933 DEFAULT_DELTACHI,
2934 )
2935 .unwrap();
2936 assert_eq!(ir.fit.parameters[1], p0[1]);
2937 }
2938
2939 #[test]
2940 fn fit_peak_from_uses_explicit_p0_and_recovers() {
2941 let xs = grid(100);
2942 let ys = gaussian_model(&xs, &[60.0, 45.0, 7.0, 5.0]);
2943 let p0 = [20.0, 40.0, 12.0, 0.0];
2945 let cons = vec![Constraint::Free; 4];
2946 let ir = fit_peak_from(
2947 PeakModel::Gaussian,
2948 &xs,
2949 &ys,
2950 &p0,
2951 &cons,
2952 DEFAULT_MAX_ITER,
2953 DEFAULT_DELTACHI,
2954 )
2955 .unwrap();
2956 let p = &ir.fit.parameters;
2957 assert!((p[1] - 45.0).abs() < 1.0, "centre {}", p[1]);
2958 assert!((p[2] - 7.0).abs() < 1.0, "fwhm {}", p[2]);
2959 let est = PeakModel::Gaussian.estimate(&xs, &ys).unwrap();
2962 let via_estimate = fit_peak_from(
2963 PeakModel::Gaussian,
2964 &xs,
2965 &ys,
2966 &est,
2967 &cons,
2968 DEFAULT_MAX_ITER,
2969 DEFAULT_DELTACHI,
2970 )
2971 .unwrap();
2972 let direct = fit_peak_constrained(
2973 PeakModel::Gaussian,
2974 &xs,
2975 &ys,
2976 &cons,
2977 DEFAULT_MAX_ITER,
2978 DEFAULT_DELTACHI,
2979 )
2980 .unwrap();
2981 assert_eq!(via_estimate.fit.parameters, direct.fit.parameters);
2982 }
2983
2984 #[test]
2985 fn fit_peak_constrained_rejects_count_mismatch() {
2986 let xs = grid(50);
2987 let ys = gaussian_model(&xs, &[60.0, 25.0, 7.0, 0.0]);
2988 assert!(
2990 fit_peak_constrained(
2991 PeakModel::Gaussian,
2992 &xs,
2993 &ys,
2994 &[Constraint::Free; 3],
2995 DEFAULT_MAX_ITER,
2996 DEFAULT_DELTACHI,
2997 )
2998 .is_none()
2999 );
3000 }
3001
3002 #[test]
3003 fn fit_multi_gaussian_single_peak() {
3004 let xs = grid(100);
3005 let ys = gaussian_model(&xs, &[50.0, 45.0, 7.0, 0.0]);
3006 let res = fit_multi_gaussian(
3007 &xs,
3008 &ys,
3009 7.0,
3010 DEFAULT_PEAK_SENSITIVITY,
3011 DEFAULT_MAX_ITER,
3012 DEFAULT_DELTACHI,
3013 )
3014 .unwrap();
3015 let p = nearest_peak(&res.parameters, 45.0);
3016 assert!((p[0] - 50.0).abs() < 2.0, "height {}", p[0]);
3017 assert!((p[1] - 45.0).abs() < 0.5, "centre {}", p[1]);
3018 assert!((p[2] - 7.0).abs() < 0.5, "fwhm {}", p[2]);
3019 }
3020
3021 #[test]
3022 fn estimate_multi_gaussian_seeds_height_and_position() {
3023 let xs = grid(100);
3024 let ys = gaussian_model(&xs, &[50.0, 45.0, 7.0, 0.0]);
3025 let (seeds, cons) =
3026 estimate_multi_gaussian(&xs, &ys, 7.0, DEFAULT_PEAK_SENSITIVITY).unwrap();
3027 assert_eq!(seeds.len() % 3, 0);
3028 assert_eq!(seeds.len(), cons.len());
3029 assert_eq!(cons[0], Constraint::Positive);
3031 assert_eq!(cons[1], Constraint::Free);
3032 assert_eq!(cons[2], Constraint::Positive);
3033 }
3034
3035 #[test]
3036 fn estimate_multi_gaussian_rejects_empty_and_mismatch() {
3037 assert!(estimate_multi_gaussian(&[], &[], 5.0, 2.5).is_none());
3038 assert!(estimate_multi_gaussian(&[0.0, 1.0], &[1.0], 5.0, 2.5).is_none());
3039 }
3040
3041 #[test]
3044 fn erf_matches_known_values() {
3045 assert_eq!(erf(0.0), 0.0);
3047 assert!((erf(0.5) - 0.520_499_877_813_046_5).abs() < 1e-6);
3048 assert!((erf(1.0) - 0.842_700_792_949_714_9).abs() < 1e-6);
3049 assert!((erf(2.0) - 0.995_322_265_018_952_7).abs() < 1e-6);
3050 assert!((erf(-1.0) + erf(1.0)).abs() < 1e-12);
3052 }
3053
3054 #[test]
3055 fn erfc_is_one_minus_erf() {
3056 for &x in &[-2.0, -0.3, 0.0, 0.7, 1.5] {
3057 assert!((erfc(x) - (1.0 - erf(x))).abs() < 1e-15);
3058 }
3059 }
3060
3061 #[test]
3062 fn convolve_valid_matches_numpy_reversed_kernel() {
3063 assert_eq!(
3066 convolve_valid(&[1.0, 2.0, 3.0, 4.0], &[1.0, 0.0, 0.0]),
3067 vec![3.0, 4.0]
3068 );
3069 let out = convolve_valid(&[1.0, 2.0, 3.0, 4.0, 5.0], &[0.25, 0.75, 0.0, -0.75, -0.25]);
3071 assert_eq!(out.len(), 1);
3072 assert!((out[0] - 2.5).abs() < 1e-12);
3073 assert!(convolve_valid(&[1.0], &[1.0, 2.0]).is_empty());
3075 }
3076
3077 #[test]
3078 fn stepup_model_half_height_at_center_and_asymptotes() {
3079 let p = [10.0, 0.0, 4.0, 2.0]; assert_eq!(stepup_model(&[0.0], &p)[0], 7.0);
3082 assert!((stepup_model(&[-1000.0], &p)[0] - 2.0).abs() < 1e-9);
3084 assert!((stepup_model(&[1000.0], &p)[0] - 12.0).abs() < 1e-9);
3085 let xs = linspace(-20.0, 20.0, 81);
3087 let ys = stepup_model(&xs, &p);
3088 assert!(ys.windows(2).all(|w| w[1] >= w[0]));
3089 }
3090
3091 #[test]
3092 fn stepdown_model_half_height_at_center_and_asymptotes() {
3093 let p = [10.0, 0.0, 4.0, 2.0];
3094 assert_eq!(stepdown_model(&[0.0], &p)[0], 7.0); assert!((stepdown_model(&[-1000.0], &p)[0] - 12.0).abs() < 1e-9);
3096 assert!((stepdown_model(&[1000.0], &p)[0] - 2.0).abs() < 1e-9);
3097 let xs = linspace(-20.0, 20.0, 81);
3098 let ys = stepdown_model(&xs, &p);
3099 assert!(ys.windows(2).all(|w| w[1] <= w[0]));
3100 }
3101
3102 #[test]
3103 fn atan_stepup_model_half_height_at_center_and_monotonic() {
3104 let p = [10.0, 0.0, 4.0, 2.0]; assert_eq!(atan_stepup_model(&[0.0], &p)[0], 7.0); assert!((atan_stepup_model(&[-1.0e8], &p)[0] - 2.0).abs() < 1e-6);
3107 assert!((atan_stepup_model(&[1.0e8], &p)[0] - 12.0).abs() < 1e-6);
3108 let xs = linspace(-50.0, 50.0, 101);
3109 let ys = atan_stepup_model(&xs, &p);
3110 assert!(ys.windows(2).all(|w| w[1] >= w[0]));
3111 }
3112
3113 #[test]
3114 fn slit_model_is_symmetric_and_localised() {
3115 let p = [10.0, 0.0, 10.0, 2.0, 1.0]; for &d in &[1.0, 3.0, 7.0, 12.0] {
3118 let a = slit_model(&[d], &p)[0];
3119 let b = slit_model(&[-d], &p)[0];
3120 assert!((a - b).abs() < 1e-12, "slit asymmetric at {d}: {a} vs {b}");
3121 }
3122 assert!(slit_model(&[0.0], &p)[0] > 5.0);
3124 assert!((slit_model(&[1000.0], &p)[0] - 1.0).abs() < 1e-9);
3125 assert!((slit_model(&[-1000.0], &p)[0] - 1.0).abs() < 1e-9);
3126 }
3127
3128 #[test]
3129 fn estimate_stepup_recovers_center_height_bg() {
3130 let xs = linspace(0.0, 100.0, 101);
3131 let ys = stepup_model(&xs, &[8.0, 40.0, 6.0, 3.0]);
3132 let s = estimate_stepup(&xs, &ys).unwrap();
3133 assert!((s[0] - 8.0).abs() < 0.5, "height seed {}", s[0]);
3134 assert!((s[1] - 40.0).abs() < 3.0, "center seed {}", s[1]);
3135 assert!((s[3] - 3.0).abs() < 0.5, "bg seed {}", s[3]);
3136 }
3137
3138 #[test]
3139 fn estimate_stepdown_recovers_center_height_bg() {
3140 let xs = linspace(0.0, 100.0, 101);
3141 let ys = stepdown_model(&xs, &[8.0, 40.0, 6.0, 3.0]);
3142 let s = estimate_stepdown(&xs, &ys).unwrap();
3143 assert!((s[0] - 8.0).abs() < 0.5, "height seed {}", s[0]);
3144 assert!((s[1] - 40.0).abs() < 3.0, "center seed {}", s[1]);
3145 assert!((s[3] - 3.0).abs() < 0.5, "bg seed {}", s[3]);
3146 }
3147
3148 #[test]
3149 fn estimate_slit_centers_on_the_slit() {
3150 let xs = linspace(0.0, 100.0, 201);
3151 let ys = slit_model(&xs, &[10.0, 50.0, 20.0, 3.0, 2.0]);
3152 let s = estimate_slit(&xs, &ys).unwrap();
3153 assert_eq!(s.len(), 5);
3154 assert!((s[1] - 50.0).abs() < 2.0, "position seed {}", s[1]);
3155 assert!(s[3] > 0.0, "beamfwhm seed must be positive: {}", s[3]);
3156 }
3157
3158 #[test]
3159 fn peak_model_step_variants_delegate() {
3160 assert_eq!(PeakModel::StepUp.name(), "Step Up");
3161 assert_eq!(PeakModel::StepDown.name(), "Step Down");
3162 assert_eq!(PeakModel::Slit.name(), "Slit");
3163 assert_eq!(PeakModel::AtanStepUp.name(), "Arctan Step Up");
3164 assert_eq!(
3165 PeakModel::StepUp.param_names(),
3166 vec!["Height", "Center", "FWHM", "Background"]
3167 );
3168 assert_eq!(PeakModel::Slit.param_names().len(), 5);
3169 assert_eq!(
3170 PeakModel::AtanStepUp.param_names(),
3171 vec!["Height", "Center", "Width", "Background"]
3172 );
3173 let xs = linspace(-5.0, 5.0, 11);
3175 let p = [3.0, 0.0, 2.0, 1.0];
3176 assert_eq!(PeakModel::StepUp.eval(&xs, &p), stepup_model(&xs, &p));
3177 assert_eq!(PeakModel::StepDown.eval(&xs, &p), stepdown_model(&xs, &p));
3178 assert_eq!(
3179 PeakModel::AtanStepUp.eval(&xs, &p),
3180 atan_stepup_model(&xs, &p)
3181 );
3182 }
3183
3184 #[test]
3185 fn iterative_fit_recovers_stepup() {
3186 let xs = linspace(0.0, 100.0, 101);
3187 let truth = [8.0, 40.0, 6.0, 3.0];
3188 let ys = stepup_model(&xs, &truth);
3189 let fit = IterativeFit::new(PeakModel::StepUp)
3190 .fit_full(&xs, &ys)
3191 .unwrap();
3192 let p = &fit.fit.parameters;
3193 assert!((p[0] - truth[0]).abs() < 1.0, "height {}", p[0]);
3194 assert!((p[1] - truth[1]).abs() < 1.0, "center {}", p[1]);
3195 assert!((p[3] - truth[3]).abs() < 1.0, "bg {}", p[3]);
3196 }
3197
3198 #[test]
3201 fn fit_peak_with_background_none_is_byte_identical_to_plain_fit() {
3202 use crate::core::background::Background;
3203 let xs = grid(100);
3204 let ys = gaussian_model(&xs, &[100.0, 50.0, 8.0, 0.0]);
3205 let bgfit = fit_peak_with_background(
3206 PeakModel::Gaussian,
3207 Background::None,
3208 &xs,
3209 &ys,
3210 DEFAULT_MAX_ITER,
3211 DEFAULT_DELTACHI,
3212 )
3213 .unwrap();
3214 let plain = IterativeFit::new(PeakModel::Gaussian)
3215 .fit_full(&xs, &ys)
3216 .unwrap();
3217 assert!(bgfit.background.iter().all(|&b| b == 0.0));
3220 assert_eq!(bgfit.peak.fit.parameters, plain.fit.parameters);
3221 assert_eq!(bgfit.total, bgfit.peak.fit.y_fit);
3222 assert_eq!(bgfit.total, plain.fit.y_fit);
3223 }
3224
3225 #[test]
3226 fn fit_peak_with_background_constant_offset_recovers_peak() {
3227 use crate::core::background::Background;
3228 let xs = grid(100);
3229 let ys = gaussian_model(&xs, &[100.0, 50.0, 8.0, 25.0]);
3232 let bgfit = fit_peak_with_background(
3233 PeakModel::Gaussian,
3234 Background::Constant,
3235 &xs,
3236 &ys,
3237 DEFAULT_MAX_ITER,
3238 DEFAULT_DELTACHI,
3239 )
3240 .unwrap();
3241 assert!(
3243 (bgfit.background[0] - 25.0).abs() < 1.0,
3244 "background {}",
3245 bgfit.background[0]
3246 );
3247 assert!(bgfit.background.iter().all(|&b| b == bgfit.background[0]));
3248 let p = &bgfit.peak.fit.parameters;
3250 assert!((p[1] - 50.0).abs() < 2.0, "centre {}", p[1]);
3251 let max_err = ys
3252 .iter()
3253 .zip(&bgfit.total)
3254 .map(|(&d, &t)| (d - t).abs())
3255 .fold(0.0_f64, f64::max);
3256 assert!(max_err < 5.0, "max |data - total| = {max_err}");
3257 }
3258
3259 #[test]
3260 fn fit_peak_with_background_linear_recovers_peak_on_slope() {
3261 use crate::core::background::Background;
3262 let xs = grid(120);
3263 let base = line(&xs, &[0.5, 10.0]);
3265 let peak = gaussian_model(&xs, &[80.0, 60.0, 8.0, 0.0]);
3266 let ys: Vec<f64> = base.iter().zip(&peak).map(|(&b, &p)| b + p).collect();
3267 let bgfit = fit_peak_with_background(
3268 PeakModel::Gaussian,
3269 Background::Linear,
3270 &xs,
3271 &ys,
3272 DEFAULT_MAX_ITER,
3273 DEFAULT_DELTACHI,
3274 )
3275 .unwrap();
3276 assert!(
3278 *bgfit.background.last().unwrap() > bgfit.background[0],
3279 "background not rising: {} -> {}",
3280 bgfit.background[0],
3281 bgfit.background.last().unwrap()
3282 );
3283 let p = &bgfit.peak.fit.parameters;
3285 assert!((p[1] - 60.0).abs() < 3.0, "centre {}", p[1]);
3286 assert!((p[2] - 8.0).abs() < 4.0, "fwhm {}", p[2]);
3287 let max_err = ys
3289 .iter()
3290 .zip(&bgfit.total)
3291 .map(|(&d, &t)| (d - t).abs())
3292 .fold(0.0_f64, f64::max);
3293 assert!(max_err < 12.0, "max |data - total| = {max_err}");
3294 }
3295
3296 #[test]
3297 fn fit_peak_with_background_rejects_bad_input() {
3298 use crate::core::background::Background;
3299 let xs = grid(10);
3300 let ys = constant(&xs, &[1.0]);
3301 assert!(
3303 fit_peak_with_background(
3304 PeakModel::Gaussian,
3305 Background::None,
3306 &xs,
3307 &ys[..5],
3308 DEFAULT_MAX_ITER,
3309 DEFAULT_DELTACHI,
3310 )
3311 .is_none()
3312 );
3313 assert!(
3315 fit_peak_with_background(
3316 PeakModel::Gaussian,
3317 Background::None,
3318 &[],
3319 &[],
3320 DEFAULT_MAX_ITER,
3321 DEFAULT_DELTACHI,
3322 )
3323 .is_none()
3324 );
3325 }
3326}