#[derive(Debug, Clone)]
pub struct FitResult {
pub y_fit: Vec<f64>,
pub parameters: Vec<f64>,
pub param_names: Vec<String>,
}
pub trait FitFunction {
fn name(&self) -> &str;
fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult>;
}
pub struct LinearFit;
impl FitFunction for LinearFit {
fn name(&self) -> &str {
"Linear"
}
fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult> {
if x.len() != y.len() || x.len() < 2 {
return None;
}
let n = x.len() as f64;
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
let sum_xx: f64 = x.iter().map(|&xi| xi * xi).sum();
let denominator = n * sum_xx - sum_x * sum_x;
if denominator.abs() < 1e-12 {
return None;
}
let m = (n * sum_xy - sum_x * sum_y) / denominator;
let c = (sum_y - m * sum_x) / n;
let y_fit = x.iter().map(|&xi| m * xi + c).collect();
Some(FitResult {
y_fit,
parameters: vec![m, c],
param_names: vec!["Slope (m)".to_string(), "Intercept (c)".to_string()],
})
}
}
pub struct GaussianEstimateFit;
impl FitFunction for GaussianEstimateFit {
fn name(&self) -> &str {
"Gaussian (Estimate)"
}
fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult> {
if x.len() != y.len() || x.len() < 3 {
return None;
}
let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
let mut max_y = f64::NEG_INFINITY;
let mut max_idx = 0;
for (i, &yi) in y.iter().enumerate() {
if yi > max_y {
max_y = yi;
max_idx = i;
}
}
let a = max_y - bg;
let mu = x[max_idx];
let half_max = bg + a / 2.0;
let mut left_idx = max_idx;
while left_idx > 0 && y[left_idx] > half_max {
left_idx -= 1;
}
let mut right_idx = max_idx;
while right_idx < y.len() - 1 && y[right_idx] > half_max {
right_idx += 1;
}
let fwhm = x[right_idx] - x[left_idx];
let sigma = if fwhm > 0.0 {
fwhm / 2.355
} else {
(x.last().unwrap() - x.first().unwrap()) / 4.0
};
let y_fit = x
.iter()
.map(|&xi| {
let z = (xi - mu) / sigma;
a * (-0.5 * z * z).exp() + bg
})
.collect();
Some(FitResult {
y_fit,
parameters: vec![a, mu, sigma, bg],
param_names: vec![
"Amplitude (A)".to_string(),
"Center (mu)".to_string(),
"Sigma".to_string(),
"Background".to_string(),
],
})
}
}
pub const LOG2: f64 = std::f64::consts::LN_2;
pub fn fwhm_to_sigma_factor() -> f64 {
2.0 * (2.0 * LOG2).sqrt()
}
#[derive(Debug, Clone)]
pub struct LeastSqResult {
pub parameters: Vec<f64>,
pub covariance: Vec<Vec<f64>>,
pub uncertainties: Vec<f64>,
pub chisq: f64,
pub reduced_chisq: Option<f64>,
pub niter: usize,
pub nfev: usize,
}
impl LeastSqResult {
pub fn std_errors(&self) -> Vec<f64> {
(0..self.parameters.len())
.map(|i| self.covariance[i][i].abs().sqrt())
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FitError {
LengthMismatch,
NoFreeParameters,
NotEnoughData,
NonFinite,
SingularMatrix,
InvalidConstraint,
BadConstraintReference,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Constraint {
Free,
Positive,
Quoted {
min: f64,
max: f64,
},
Fixed,
Factor {
reference: usize,
factor: f64,
},
Delta {
reference: usize,
delta: f64,
},
Sum {
reference: usize,
sum: f64,
},
Ignored,
}
fn get_parameters(params: &[f64], constraints: &[Constraint]) -> Vec<f64> {
let mut out: Vec<f64> = params
.iter()
.zip(constraints)
.map(|(&p, c)| match c {
Constraint::Positive => p.abs(),
_ => p,
})
.collect();
for (i, c) in constraints.iter().enumerate() {
match *c {
Constraint::Factor { reference, factor } => out[i] = factor * out[reference],
Constraint::Delta { reference, delta } => out[i] = delta + out[reference],
Constraint::Sum { reference, sum } => out[i] = sum - out[reference],
Constraint::Ignored => out[i] = 0.0,
_ => {}
}
}
out
}
fn get_sigma_parameters(
parameters: &[f64],
sigma0: &[f64],
constraints: &[Constraint],
) -> Vec<f64> {
let mut sigma_par = vec![0.0_f64; parameters.len()];
let mut n_free = 0usize;
for (i, c) in constraints.iter().enumerate() {
match *c {
Constraint::Free | Constraint::Positive => {
sigma_par[i] = sigma0[n_free];
n_free += 1;
}
Constraint::Quoted { min, max } => {
let pmax = min.max(max);
let pmin = min.min(max);
let b = 0.5 * (pmax - pmin);
if b > 0.0 && parameters[i] < pmax && parameters[i] > pmin {
sigma_par[i] = (b * parameters[i].cos() * sigma0[n_free]).abs();
n_free += 1;
} else {
sigma_par[i] = parameters[i];
}
}
Constraint::Fixed => sigma_par[i] = parameters[i],
_ => {}
}
}
for (i, c) in constraints.iter().enumerate() {
match *c {
Constraint::Factor { reference, factor } => {
sigma_par[i] = factor * sigma_par[reference]
}
Constraint::Delta { reference, .. } | Constraint::Sum { reference, .. } => {
sigma_par[i] = sigma_par[reference]
}
_ => {}
}
}
sigma_par
}
pub fn invert_matrix(m: &[Vec<f64>]) -> Option<Vec<Vec<f64>>> {
let n = m.len();
if n == 0 {
return Some(Vec::new());
}
let mut a: Vec<Vec<f64>> = Vec::with_capacity(n);
for (i, row) in m.iter().enumerate() {
if row.len() != n {
return None;
}
let mut aug = row.clone();
aug.extend((0..n).map(|j| if i == j { 1.0 } else { 0.0 }));
a.push(aug);
}
for col in 0..n {
let mut pivot = col;
let mut best = a[col][col].abs();
for (r, row) in a.iter().enumerate().skip(col + 1) {
let v = row[col].abs();
if v > best {
best = v;
pivot = r;
}
}
if best == 0.0 {
return None; }
a.swap(col, pivot);
let pivot_val = a[col][col];
for v in a[col].iter_mut() {
*v /= pivot_val;
}
let pivot_row = a[col].clone();
for (r, row) in a.iter_mut().enumerate() {
if r == col {
continue;
}
let factor = row[col];
if factor != 0.0 {
for (cell, &pv) in row.iter_mut().zip(pivot_row.iter()) {
*cell -= factor * pv;
}
}
}
}
let inv = a
.into_iter()
.map(|row| row[n..].to_vec())
.collect::<Vec<_>>();
Some(inv)
}
pub const DEFAULT_DELTACHI: f64 = 0.001;
pub const DEFAULT_MAX_ITER: usize = 100;
pub const DEFAULT_FIT_SENSITIVITY: f64 = 2.5;
#[allow(clippy::too_many_arguments)]
pub fn leastsq<F>(
model: F,
xdata: &[f64],
ydata: &[f64],
p0: &[f64],
sigma: Option<&[f64]>,
max_iter: usize,
deltachi: f64,
left_derivative: bool,
) -> Result<LeastSqResult, FitError>
where
F: Fn(&[f64], &[f64]) -> Vec<f64>,
{
if xdata.len() != ydata.len() {
return Err(FitError::LengthMismatch);
}
let n_param = p0.len();
if n_param == 0 {
return Err(FitError::NoFreeParameters);
}
let m = ydata.len();
if m < n_param {
return Err(FitError::NotEnoughData);
}
if xdata.iter().chain(ydata.iter()).any(|v| !v.is_finite()) {
return Err(FitError::NonFinite);
}
let weight0: Vec<f64> = match sigma {
Some(s) => {
if s.len() != m {
return Err(FitError::LengthMismatch);
}
if s.iter().any(|v| !v.is_finite()) {
return Err(FitError::NonFinite);
}
s.iter()
.map(|&sv| {
let denom = if sv == 0.0 { 1.0 } else { sv };
let w = 1.0 / denom;
w * w
})
.collect()
}
None => vec![1.0; m],
};
let epsfcn = f64::EPSILON;
let sqrt_epsfcn = epsfcn.sqrt();
let mut fittedpar = p0.to_vec();
let mut flambda = 0.001_f64;
let mut iiter = max_iter as i64;
let mut last_evaluation: Option<Vec<f64>> = None;
let mut iteration_counter: usize = 0;
let mut nfev: usize = 0;
let mut chisq0: f64;
let mut alpha0: Vec<Vec<f64>> = vec![vec![0.0; n_param]; n_param];
loop {
if iiter <= 0 {
break;
}
iteration_counter += 1;
let yfit0 = match &last_evaluation {
Some(ev) => ev.clone(),
None => {
let ev = model(xdata, &fittedpar);
nfev += 1;
ev
}
};
let delta: Vec<f64> = fittedpar
.iter()
.map(|&p| (p + if p == 0.0 { 1.0 } else { 0.0 }) * sqrt_epsfcn)
.collect();
let mut deriv: Vec<Vec<f64>> = Vec::with_capacity(n_param);
for i in 0..n_param {
let mut pwork = fittedpar.clone();
pwork[i] = fittedpar[i] + delta[i];
let f1 = model(xdata, &pwork);
nfev += 1;
let di = delta[i];
let row: Vec<f64> = if left_derivative {
pwork[i] = fittedpar[i] - delta[i];
let f2 = model(xdata, &pwork);
nfev += 1;
f1.iter()
.zip(f2.iter())
.map(|(&a, &b)| (a - b) / (2.0 * di))
.collect()
} else {
f1.iter()
.zip(yfit0.iter())
.map(|(&a, &b)| (a - b) / di)
.collect()
};
deriv.push(row);
}
let deltay: Vec<f64> = ydata
.iter()
.zip(yfit0.iter())
.map(|(&y, &f)| y - f)
.collect();
let help0: Vec<f64> = weight0
.iter()
.zip(deltay.iter())
.map(|(&w, &d)| w * d)
.collect();
let mut beta = vec![0.0_f64; n_param];
for i in 0..n_param {
let mut s = 0.0;
for j in 0..m {
s += help0[j] * deriv[i][j];
}
beta[i] = s;
}
let mut alpha = vec![vec![0.0_f64; n_param]; n_param];
for i in 0..n_param {
for k in 0..n_param {
let mut s = 0.0;
for j in 0..m {
s += deriv[i][j] * weight0[j] * deriv[k][j];
}
alpha[i][k] = s;
}
}
chisq0 = help0.iter().zip(deltay.iter()).map(|(&h, &d)| h * d).sum();
alpha0 = alpha.clone();
loop {
iiter -= 1;
let mut alpha_lm = alpha0.clone();
for (d, row) in alpha_lm.iter_mut().enumerate() {
row[d] *= 1.0 + flambda;
}
let inv_alpha = match invert_matrix(&alpha_lm) {
Some(inv) => inv,
None => {
flambda *= 10.0;
if flambda > 1000.0 {
iiter = 0;
break;
}
continue;
}
};
let mut deltapar = vec![0.0_f64; n_param];
for (k, dp) in deltapar.iter_mut().enumerate() {
let mut s = 0.0;
for (i, &b) in beta.iter().enumerate() {
s += b * inv_alpha[i][k];
}
*dp = s;
}
let newpar: Vec<f64> = fittedpar
.iter()
.zip(deltapar.iter())
.map(|(&p, &d)| p + d)
.collect();
let yfit = model(xdata, &newpar);
nfev += 1;
let chisq: f64 = weight0
.iter()
.zip(ydata.iter().zip(yfit.iter()))
.map(|(&w, (&y, &f))| {
let r = y - f;
w * r * r
})
.sum();
let absdeltachi = chisq0 - chisq;
if absdeltachi < 0.0 {
flambda *= 10.0;
if flambda > 1000.0 {
iiter = 0;
break;
}
} else {
fittedpar = newpar;
let lastdeltachi =
100.0 * (absdeltachi / (chisq + if chisq == 0.0 { 1.0 } else { 0.0 }));
if iteration_counter >= 2 && (lastdeltachi < deltachi || absdeltachi < sqrt_epsfcn)
{
iiter = 0;
}
flambda /= 10.0;
last_evaluation = Some(yfit);
break;
}
}
}
let covariance = invert_matrix(&alpha0).ok_or(FitError::SingularMatrix)?;
let chisq_final = {
let yfit = model(xdata, &fittedpar);
nfev += 1;
weight0
.iter()
.zip(ydata.iter().zip(yfit.iter()))
.map(|(&w, (&y, &f))| {
let r = y - f;
w * r * r
})
.sum::<f64>()
};
let dof = m as i64 - n_param as i64;
let reduced_chisq = if dof > 0 {
Some(chisq_final / dof as f64)
} else {
None
};
let uncertainties: Vec<f64> = (0..n_param)
.map(|i| covariance[i][i].abs().sqrt())
.collect();
Ok(LeastSqResult {
parameters: fittedpar,
covariance,
uncertainties,
chisq: chisq_final,
reduced_chisq,
niter: iteration_counter,
nfev,
})
}
fn take(v: &[f64], indices: &[usize]) -> Vec<f64> {
indices.iter().map(|&i| v[i]).collect()
}
struct CabOut {
chisq: f64,
alpha: Vec<Vec<f64>>,
beta: Vec<f64>,
n_free: usize,
free_index: Vec<usize>,
noigno: Vec<usize>,
fitparam: Vec<f64>,
}
#[allow(clippy::too_many_arguments)]
fn chisq_alpha_beta_constrained<F>(
model: &F,
parameters: &[f64],
xdata: &[f64],
ydata: &[f64],
weight0: &[f64],
constraints: &[Constraint],
sqrt_epsfcn: f64,
last_evaluation: Option<&[f64]>,
left_derivative: bool,
nfev: &mut usize,
) -> CabOut
where
F: Fn(&[f64], &[f64]) -> Vec<f64>,
{
let m = ydata.len();
let mut fitparam: Vec<f64> = Vec::new();
let mut free_index: Vec<usize> = Vec::new();
let mut noigno: Vec<usize> = Vec::new();
let mut derivfactor: Vec<f64> = Vec::new();
for (i, c) in constraints.iter().enumerate() {
if !matches!(c, Constraint::Ignored) {
noigno.push(i);
}
match *c {
Constraint::Free => {
fitparam.push(parameters[i]);
derivfactor.push(1.0);
free_index.push(i);
}
Constraint::Positive => {
fitparam.push(parameters[i].abs());
derivfactor.push(1.0);
free_index.push(i);
}
Constraint::Quoted { min, max } => {
let pmax = min.max(max);
let pmin = min.min(max);
if (pmax - pmin) > 0.0 && parameters[i] <= pmax && parameters[i] >= pmin {
let a = 0.5 * (pmax + pmin);
let b = 0.5 * (pmax - pmin);
fitparam.push(parameters[i]);
derivfactor.push(b * ((parameters[i] - a) / b).asin().cos());
free_index.push(i);
}
}
_ => {}
}
}
let n_free = fitparam.len();
let delta: Vec<f64> = fitparam
.iter()
.map(|&p| (p + if p == 0.0 { 1.0 } else { 0.0 }) * sqrt_epsfcn)
.collect();
let mut pwork = parameters.to_vec();
for (i, &fi) in free_index.iter().enumerate() {
pwork[fi] = fitparam[i];
}
let yfit: Vec<f64> = match last_evaluation {
Some(ev) => ev.to_vec(),
None => {
let base_in = take(&get_parameters(&pwork, constraints), &noigno);
let ev = model(xdata, &base_in);
*nfev += 1;
ev
}
};
let mut deriv: Vec<Vec<f64>> = Vec::with_capacity(n_free);
for i in 0..n_free {
let fi = free_index[i];
pwork[fi] = fitparam[i] + delta[i];
let newpar = take(&get_parameters(&pwork, constraints), &noigno);
let f1 = model(xdata, &newpar);
*nfev += 1;
let di = delta[i];
let df = derivfactor[i];
let row: Vec<f64> = if left_derivative {
pwork[fi] = fitparam[i] - delta[i];
let newpar = take(&get_parameters(&pwork, constraints), &noigno);
let f2 = model(xdata, &newpar);
*nfev += 1;
f1.iter()
.zip(f2.iter())
.map(|(&a, &b)| (a - b) / (2.0 * di) * df)
.collect()
} else {
f1.iter()
.zip(yfit.iter())
.map(|(&a, &b)| (a - b) / di * df)
.collect()
};
deriv.push(row);
pwork[fi] = fitparam[i]; }
let deltay: Vec<f64> = ydata
.iter()
.zip(yfit.iter())
.map(|(&y, &f)| y - f)
.collect();
let help0: Vec<f64> = weight0
.iter()
.zip(deltay.iter())
.map(|(&w, &d)| w * d)
.collect();
let mut beta = vec![0.0_f64; n_free];
for (i, b) in beta.iter_mut().enumerate() {
let mut s = 0.0;
for j in 0..m {
s += help0[j] * deriv[i][j];
}
*b = s;
}
let mut alpha = vec![vec![0.0_f64; n_free]; n_free];
for i in 0..n_free {
for k in 0..n_free {
let mut s = 0.0;
for j in 0..m {
s += deriv[i][j] * weight0[j] * deriv[k][j];
}
alpha[i][k] = s;
}
}
let chisq = help0.iter().zip(deltay.iter()).map(|(&h, &d)| h * d).sum();
CabOut {
chisq,
alpha,
beta,
n_free,
free_index,
noigno,
fitparam,
}
}
#[allow(clippy::too_many_arguments)]
pub fn leastsq_constrained<F>(
model: F,
xdata: &[f64],
ydata: &[f64],
p0: &[f64],
constraints: &[Constraint],
sigma: Option<&[f64]>,
max_iter: usize,
deltachi: f64,
left_derivative: bool,
) -> Result<LeastSqResult, FitError>
where
F: Fn(&[f64], &[f64]) -> Vec<f64>,
{
if xdata.len() != ydata.len() {
return Err(FitError::LengthMismatch);
}
let n_param = p0.len();
if n_param == 0 {
return Err(FitError::NoFreeParameters);
}
if constraints.len() != n_param {
return Err(FitError::BadConstraintReference);
}
let m = ydata.len();
if m < 1 {
return Err(FitError::NotEnoughData);
}
if xdata.iter().chain(ydata.iter()).any(|v| !v.is_finite()) {
return Err(FitError::NonFinite);
}
for c in constraints {
match *c {
Constraint::Factor { reference, .. }
| Constraint::Delta { reference, .. }
| Constraint::Sum { reference, .. } => {
if reference >= n_param {
return Err(FitError::BadConstraintReference);
}
}
Constraint::Quoted { min, max } => {
if (min.max(max) - min.min(max)) == 0.0 {
return Err(FitError::InvalidConstraint);
}
}
_ => {}
}
}
let weight0: Vec<f64> = match sigma {
Some(s) => {
if s.len() != m {
return Err(FitError::LengthMismatch);
}
if s.iter().any(|v| !v.is_finite()) {
return Err(FitError::NonFinite);
}
s.iter()
.map(|&sv| {
let denom = if sv == 0.0 { 1.0 } else { sv };
let w = 1.0 / denom;
w * w
})
.collect()
}
None => vec![1.0; m],
};
let epsfcn = f64::EPSILON;
let sqrt_epsfcn = epsfcn.sqrt();
let n_free_initial = constraints
.iter()
.enumerate()
.filter(|(i, c)| match **c {
Constraint::Free | Constraint::Positive => true,
Constraint::Quoted { min, max } => {
let (pmax, pmin) = (min.max(max), min.min(max));
(pmax - pmin) > 0.0 && p0[*i] <= pmax && p0[*i] >= pmin
}
_ => false,
})
.count();
if n_free_initial == 0 {
return Err(FitError::NoFreeParameters);
}
let mut fittedpar = p0.to_vec();
let mut flambda = 0.001_f64;
let mut iiter = max_iter as i64;
let mut last_evaluation: Option<Vec<f64>> = None;
let mut iteration_counter: usize = 0;
let mut nfev: usize = 0;
let mut alpha0: Vec<Vec<f64>> = vec![vec![0.0; n_free_initial]; n_free_initial];
let mut n_free_final = n_free_initial;
loop {
if iiter <= 0 {
break;
}
iteration_counter += 1;
let cab = chisq_alpha_beta_constrained(
&model,
&fittedpar,
xdata,
ydata,
&weight0,
constraints,
sqrt_epsfcn,
last_evaluation.as_deref(),
left_derivative,
&mut nfev,
);
let chisq0 = cab.chisq;
alpha0 = cab.alpha.clone();
n_free_final = cab.n_free;
let beta = &cab.beta;
let free_index = &cab.free_index;
let noigno = &cab.noigno;
let fitparam = &cab.fitparam;
if cab.n_free == 0 {
return Err(FitError::NoFreeParameters);
}
loop {
iiter -= 1;
let mut alpha_lm = alpha0.clone();
for (d, row) in alpha_lm.iter_mut().enumerate() {
row[d] *= 1.0 + flambda;
}
let inv_alpha = match invert_matrix(&alpha_lm) {
Some(inv) => inv,
None => {
flambda *= 10.0;
if flambda > 1000.0 {
iiter = 0;
break;
}
continue;
}
};
let mut deltapar = vec![0.0_f64; cab.n_free];
for (k, dp) in deltapar.iter_mut().enumerate() {
let mut s = 0.0;
for (i, &b) in beta.iter().enumerate() {
s += b * inv_alpha[i][k];
}
*dp = s;
}
let mut newpar = p0.to_vec();
for (i, &fi) in free_index.iter().enumerate() {
let pv = match constraints[fi] {
Constraint::Quoted { min, max } => {
let pmax = min.max(max);
let pmin = min.min(max);
let a = 0.5 * (pmax + pmin);
let b = 0.5 * (pmax - pmin);
a + b * (((fitparam[i] - a) / b).asin() + deltapar[i]).sin()
}
_ => fitparam[i] + deltapar[i],
};
newpar[fi] = pv;
}
let newpar = get_parameters(&newpar, constraints);
let workpar = take(&newpar, noigno);
let yfit = model(xdata, &workpar);
nfev += 1;
let chisq: f64 = weight0
.iter()
.zip(ydata.iter().zip(yfit.iter()))
.map(|(&w, (&y, &f))| {
let r = y - f;
w * r * r
})
.sum();
let absdeltachi = chisq0 - chisq;
if absdeltachi < 0.0 {
flambda *= 10.0;
if flambda > 1000.0 {
iiter = 0;
break;
}
} else {
fittedpar = newpar;
let lastdeltachi =
100.0 * (absdeltachi / (chisq + if chisq == 0.0 { 1.0 } else { 0.0 }));
if iteration_counter >= 2 && (lastdeltachi < deltachi || absdeltachi < sqrt_epsfcn)
{
iiter = 0;
}
flambda /= 10.0;
last_evaluation = Some(yfit);
break;
}
}
}
let cov0 = invert_matrix(&alpha0).ok_or(FitError::SingularMatrix)?;
let new_constraints: Vec<Constraint> = constraints
.iter()
.map(|c| match c {
Constraint::Fixed | Constraint::Ignored => *c,
_ => Constraint::Free,
})
.collect();
let cab2 = chisq_alpha_beta_constrained(
&model,
&fittedpar,
xdata,
ydata,
&weight0,
&new_constraints,
sqrt_epsfcn,
last_evaluation.as_deref(),
left_derivative,
&mut nfev,
);
let mut covariance = vec![vec![0.0_f64; n_param]; n_param];
if let Some(cov_free) = invert_matrix(&cab2.alpha) {
for (r, &pr) in cab2.free_index.iter().enumerate() {
for (cc, &pc) in cab2.free_index.iter().enumerate() {
covariance[pr][pc] = cov_free[r][cc];
}
}
}
for (idx, c) in constraints.iter().enumerate() {
if matches!(c, Constraint::Fixed | Constraint::Ignored) {
covariance[idx][idx] = fittedpar[idx] * fittedpar[idx];
}
}
let sigma0: Vec<f64> = (0..n_free_final).map(|i| cov0[i][i].abs().sqrt()).collect();
let uncertainties = get_sigma_parameters(&fittedpar, &sigma0, constraints);
let workpar = take(&get_parameters(&fittedpar, constraints), &cab2.noigno);
let yfit_final = model(xdata, &workpar);
nfev += 1;
let chisq_final: f64 = weight0
.iter()
.zip(ydata.iter().zip(yfit_final.iter()))
.map(|(&w, (&y, &f))| {
let r = y - f;
w * r * r
})
.sum();
let dof = m as i64 - n_free_final as i64;
let reduced_chisq = if dof > 0 {
Some(chisq_final / dof as f64)
} else {
None
};
Ok(LeastSqResult {
parameters: fittedpar,
covariance,
uncertainties,
chisq: chisq_final,
reduced_chisq,
niter: iteration_counter,
nfev,
})
}
pub fn gaussian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
let sigma = fwhm / fwhm_to_sigma_factor();
x.iter()
.map(|&xi| {
let mut y = bg;
if sigma != 0.0 {
let dhelp = (xi - centroid) / sigma;
if dhelp <= 20.0 {
y += height * (-0.5 * dhelp * dhelp).exp();
}
}
y
})
.collect()
}
pub fn gaussian_area_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (area, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
let sigma = fwhm / fwhm_to_sigma_factor();
let sqrt2pi = (2.0 * std::f64::consts::PI).sqrt();
x.iter()
.map(|&xi| {
let mut y = bg;
if sigma != 0.0 {
let height = area / (sigma * sqrt2pi);
let dhelp = (xi - centroid) / sigma;
if dhelp <= 35.0 {
y += height * (-0.5 * dhelp * dhelp).exp();
}
}
y
})
.collect()
}
pub fn lorentzian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
x.iter()
.map(|&xi| {
let mut y = bg;
if fwhm != 0.0 {
let dhelp = (xi - centroid) / (0.5 * fwhm);
y += height / (1.0 + dhelp * dhelp);
}
y
})
.collect()
}
pub fn pseudo_voigt_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm, eta, bg) = (params[0], params[1], params[2], params[3], params[4]);
let sigma = fwhm / fwhm_to_sigma_factor();
x.iter()
.map(|&xi| {
let mut y = bg;
if fwhm != 0.0 {
let dl = (xi - centroid) / (0.5 * fwhm);
y += eta * height / (1.0 + dl * dl);
}
if sigma != 0.0 {
let dg = (xi - centroid) / sigma;
if dg <= 35.0 {
y += (1.0 - eta) * height * (-0.5 * dg * dg).exp();
}
}
y
})
.collect()
}
pub fn lorentzian_area_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (area, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
x.iter()
.map(|&xi| {
let mut y = bg;
if fwhm != 0.0 {
let dhelp = (xi - centroid) / (0.5 * fwhm);
y += area / (0.5 * std::f64::consts::PI * fwhm * (1.0 + dhelp * dhelp));
}
y
})
.collect()
}
pub fn split_gaussian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm1, fwhm2, bg) =
(params[0], params[1], params[2], params[3], params[4]);
let sigma1 = fwhm1 / fwhm_to_sigma_factor();
let sigma2 = fwhm2 / fwhm_to_sigma_factor();
x.iter()
.map(|&xi| {
let mut y = bg;
let diff = xi - centroid;
let sigma = if diff > 0.0 { sigma2 } else { sigma1 };
if sigma != 0.0 {
let dhelp = diff / sigma;
if dhelp <= 20.0 {
y += height * (-0.5 * dhelp * dhelp).exp();
}
}
y
})
.collect()
}
pub fn split_lorentzian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm1, fwhm2, bg) =
(params[0], params[1], params[2], params[3], params[4]);
x.iter()
.map(|&xi| {
let mut y = bg;
let diff = xi - centroid;
let fwhm = if diff > 0.0 { fwhm2 } else { fwhm1 };
if fwhm != 0.0 {
let dhelp = diff / (0.5 * fwhm);
y += height / (1.0 + dhelp * dhelp);
}
y
})
.collect()
}
pub fn pseudo_voigt_area_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (area, centroid, fwhm, eta, bg) = (params[0], params[1], params[2], params[3], params[4]);
let sigma = fwhm / fwhm_to_sigma_factor();
let half_pi = 0.5 * std::f64::consts::PI;
let sqrt2pi = (2.0 * std::f64::consts::PI).sqrt();
x.iter()
.map(|&xi| {
let mut y = bg;
if fwhm != 0.0 {
let dl = (xi - centroid) / (0.5 * fwhm);
y += eta * (area / (half_pi * fwhm * (1.0 + dl * dl)));
}
if sigma != 0.0 {
let height = area / (sigma * sqrt2pi);
let dg = (xi - centroid) / sigma;
if dg <= 35.0 {
y += (1.0 - eta) * height * (-0.5 * dg * dg).exp();
}
}
y
})
.collect()
}
pub fn split_pseudo_voigt_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm1, fwhm2, eta, bg) = (
params[0], params[1], params[2], params[3], params[4], params[5],
);
let sigma1 = fwhm1 / fwhm_to_sigma_factor();
let sigma2 = fwhm2 / fwhm_to_sigma_factor();
x.iter()
.map(|&xi| {
let mut y = bg;
let diff = xi - centroid;
let (fwhm, sigma) = if diff > 0.0 {
(fwhm2, sigma2)
} else {
(fwhm1, sigma1)
};
if fwhm != 0.0 {
let dl = diff / (0.5 * fwhm);
y += eta * height / (1.0 + dl * dl);
}
if sigma != 0.0 {
let dg = diff / sigma;
if dg <= 35.0 {
y += (1.0 - eta) * height * (-0.5 * dg * dg).exp();
}
}
y
})
.collect()
}
pub fn split_pseudo_voigt2_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm1, fwhm2, eta1, eta2, bg) = (
params[0], params[1], params[2], params[3], params[4], params[5], params[6],
);
let sigma1 = fwhm1 / fwhm_to_sigma_factor();
let sigma2 = fwhm2 / fwhm_to_sigma_factor();
x.iter()
.map(|&xi| {
let mut y = bg;
let diff = xi - centroid;
let (fwhm, sigma, eta) = if diff > 0.0 {
(fwhm2, sigma2, eta2)
} else {
(fwhm1, sigma1, eta1)
};
if fwhm != 0.0 {
let dl = (2.0 * diff) / fwhm;
y += eta * height / (1.0 + dl * dl);
}
if sigma != 0.0 {
let dg = diff / sigma;
if dg <= 35.0 {
y += (1.0 - eta) * height * (-0.5 * dg * dg).exp();
}
}
y
})
.collect()
}
#[allow(clippy::excessive_precision)]
mod sunpro_erf {
const ERX: f64 = 8.45062911510467529297e-01;
const EFX8: f64 = 1.02703333676410069053e+00;
const PP0: f64 = 1.28379167095512558561e-01;
const PP1: f64 = -3.25042107247001499370e-01;
const PP2: f64 = -2.84817495755985104766e-02;
const PP3: f64 = -5.77027029648944159157e-03;
const PP4: f64 = -2.37630166566501626084e-05;
const QQ1: f64 = 3.97917223959155352819e-01;
const QQ2: f64 = 6.50222499887672944485e-02;
const QQ3: f64 = 5.08130628187576562776e-03;
const QQ4: f64 = 1.32494738004321644526e-04;
const QQ5: f64 = -3.96022827877536812320e-06;
const PA0: f64 = -2.36211856075265944077e-03;
const PA1: f64 = 4.14856118683748331666e-01;
const PA2: f64 = -3.72207876035701323847e-01;
const PA3: f64 = 3.18346619901161753674e-01;
const PA4: f64 = -1.10894694282396677476e-01;
const PA5: f64 = 3.54783043256182359371e-02;
const PA6: f64 = -2.16637559486879084300e-03;
const QA1: f64 = 1.06420880400844228286e-01;
const QA2: f64 = 5.40397917702171048937e-01;
const QA3: f64 = 7.18286544141962662868e-02;
const QA4: f64 = 1.26171219808761642112e-01;
const QA5: f64 = 1.36370839120290507362e-02;
const QA6: f64 = 1.19844998467991074170e-02;
const RA0: f64 = -9.86494403484714822705e-03;
const RA1: f64 = -6.93858572707181764372e-01;
const RA2: f64 = -1.05586262253232909814e+01;
const RA3: f64 = -6.23753324503260060396e+01;
const RA4: f64 = -1.62396669462573470355e+02;
const RA5: f64 = -1.84605092906711035994e+02;
const RA6: f64 = -8.12874355063065934246e+01;
const RA7: f64 = -9.81432934416914548592e+00;
const SA1: f64 = 1.96512716674392571292e+01;
const SA2: f64 = 1.37657754143519042600e+02;
const SA3: f64 = 4.34565877475229228821e+02;
const SA4: f64 = 6.45387271733267880336e+02;
const SA5: f64 = 4.29008140027567833386e+02;
const SA6: f64 = 1.08635005541779435134e+02;
const SA7: f64 = 6.57024977031928170135e+00;
const SA8: f64 = -6.04244152148580987438e-02;
const RB0: f64 = -9.86494292470009928597e-03;
const RB1: f64 = -7.99283237680523006574e-01;
const RB2: f64 = -1.77579549177547519889e+01;
const RB3: f64 = -1.60636384855821916062e+02;
const RB4: f64 = -6.37566443368389627722e+02;
const RB5: f64 = -1.02509513161107724954e+03;
const RB6: f64 = -4.83519191608651397019e+02;
const SB1: f64 = 3.03380607434824582924e+01;
const SB2: f64 = 3.25792512996573918826e+02;
const SB3: f64 = 1.53672958608443695994e+03;
const SB4: f64 = 3.19985821950859553908e+03;
const SB5: f64 = 2.55305040643316442583e+03;
const SB6: f64 = 4.74528541206955367215e+02;
const SB7: f64 = -2.24409524465858183362e+01;
fn get_high_word(x: f64) -> u32 {
(x.to_bits() >> 32) as u32
}
fn clear_low_word(x: f64) -> f64 {
f64::from_bits(x.to_bits() & 0xffff_ffff_0000_0000)
}
fn erfc1(x: f64) -> f64 {
let s = x.abs() - 1.0;
let p = PA0 + s * (PA1 + s * (PA2 + s * (PA3 + s * (PA4 + s * (PA5 + s * PA6)))));
let q = 1.0 + s * (QA1 + s * (QA2 + s * (QA3 + s * (QA4 + s * (QA5 + s * QA6)))));
1.0 - ERX - p / q
}
fn erfc2(ix: u32, mut x: f64) -> f64 {
if ix < 0x3ff40000 {
return erfc1(x);
}
x = x.abs();
let s = 1.0 / (x * x);
let (r, big_s) = if ix < 0x4006db6d {
(
RA0 + s
* (RA1 + s * (RA2 + s * (RA3 + s * (RA4 + s * (RA5 + s * (RA6 + s * RA7)))))),
1.0 + s
* (SA1
+ s * (SA2
+ s * (SA3 + s * (SA4 + s * (SA5 + s * (SA6 + s * (SA7 + s * SA8))))))),
)
} else {
(
RB0 + s * (RB1 + s * (RB2 + s * (RB3 + s * (RB4 + s * (RB5 + s * RB6))))),
1.0 + s
* (SB1 + s * (SB2 + s * (SB3 + s * (SB4 + s * (SB5 + s * (SB6 + s * SB7)))))),
)
};
let z = clear_low_word(x);
(-z * z - 0.5625).exp() * ((z - x) * (z + x) + r / big_s).exp() / x
}
pub fn erf(x: f64) -> f64 {
let mut ix = get_high_word(x);
let sign = (ix >> 31) as usize;
ix &= 0x7fffffff;
if ix >= 0x7ff00000 {
return 1.0 - 2.0 * (sign as f64) + 1.0 / x;
}
if ix < 0x3feb0000 {
if ix < 0x3e300000 {
return 0.125 * (8.0 * x + EFX8 * x);
}
let z = x * x;
let r = PP0 + z * (PP1 + z * (PP2 + z * (PP3 + z * PP4)));
let s = 1.0 + z * (QQ1 + z * (QQ2 + z * (QQ3 + z * (QQ4 + z * QQ5))));
let y = r / s;
return x + x * y;
}
let y = if ix < 0x40180000 {
1.0 - erfc2(ix, x)
} else {
let x1p_1022 = f64::from_bits(0x0010000000000000);
1.0 - x1p_1022
};
if sign != 0 { -y } else { y }
}
pub fn erfc(x: f64) -> f64 {
let mut ix = get_high_word(x);
let sign = (ix >> 31) as usize;
ix &= 0x7fffffff;
if ix >= 0x7ff00000 {
return 2.0 * (sign as f64) + 1.0 / x;
}
if ix < 0x3feb0000 {
if ix < 0x3c700000 {
return 1.0 - x;
}
let z = x * x;
let r = PP0 + z * (PP1 + z * (PP2 + z * (PP3 + z * PP4)));
let s = 1.0 + z * (QQ1 + z * (QQ2 + z * (QQ3 + z * (QQ4 + z * QQ5))));
let y = r / s;
if sign != 0 || ix < 0x3fd00000 {
return 1.0 - (x + x * y);
}
return 0.5 - (x - 0.5 + x * y);
}
if ix < 0x403c0000 {
if sign != 0 {
return 2.0 - erfc2(ix, x);
} else {
return erfc2(ix, x);
}
}
let x1p_1022 = f64::from_bits(0x0010000000000000);
if sign != 0 {
2.0 - x1p_1022
} else {
x1p_1022 * x1p_1022
}
}
}
pub use sunpro_erf::{erf, erfc};
fn step_denom(fwhm: f64) -> f64 {
fwhm * std::f64::consts::SQRT_2 / fwhm_to_sigma_factor()
}
pub fn stepdown_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
let denom = step_denom(fwhm);
x.iter()
.map(|&xi| {
let mut y = bg;
if denom != 0.0 {
y += height * 0.5 * erfc((xi - centroid) / denom);
}
y
})
.collect()
}
pub fn stepup_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, centroid, fwhm, bg) = (params[0], params[1], params[2], params[3]);
let denom = step_denom(fwhm);
x.iter()
.map(|&xi| {
let mut y = bg;
if denom != 0.0 {
y += height * 0.5 * (1.0 + erf((xi - centroid) / denom));
}
y
})
.collect()
}
pub fn slit_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, position, fwhm, beamfwhm, bg) =
(params[0], params[1], params[2], params[3], params[4]);
let denom = step_denom(beamfwhm);
let c1 = position - 0.5 * fwhm;
let c2 = position + 0.5 * fwhm;
x.iter()
.map(|&xi| {
let mut y = bg;
if denom != 0.0 {
y += height * 0.25 * (1.0 + erf((xi - c1) / denom)) * erfc((xi - c2) / denom);
}
y
})
.collect()
}
pub fn atan_stepup_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let (height, position, width, bg) = (params[0], params[1], params[2], params[3]);
x.iter()
.map(|&xi| {
let mut y = bg;
if width != 0.0 {
y += height * (0.5 + ((xi - position) / width).atan() / std::f64::consts::PI);
}
y
})
.collect()
}
pub fn hypermet_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let area = params[0];
let position = params[1];
let fwhm = params[2];
let st_area_r = params[3];
let st_slope_r = params[4];
let lt_area_r = params[5];
let lt_slope_r = params[6];
let step_height_r = params[7];
let bg = params[8];
let sigma = fwhm / fwhm_to_sigma_factor();
if sigma == 0.0 {
return vec![bg; x.len()];
}
let sqrt2pi = (2.0 * std::f64::consts::PI).sqrt();
let height = area / (sigma * sqrt2pi);
let sigma_sqrt2 = sigma * std::f64::consts::SQRT_2;
const EPSILON: f64 = 1e-11;
x.iter()
.map(|&xi| {
let dx = xi - position;
let c2 = 0.5 * dx * dx / (sigma * sigma);
let mut y = bg + (-c2).exp() * height;
if st_slope_r.abs() > EPSILON {
let c1 = st_area_r * 0.5 * erfc(dx / sigma_sqrt2 + 0.5 * sigma_sqrt2 / st_slope_r);
y += (area * c1 / st_slope_r)
* (0.5 * (sigma / st_slope_r).powi(2) + dx / st_slope_r).exp();
}
if lt_slope_r.abs() > EPSILON {
let c1 = lt_area_r * 0.5 * erfc(dx / sigma_sqrt2 + 0.5 * sigma_sqrt2 / lt_slope_r);
y += (area * c1 / lt_slope_r)
* (0.5 * (sigma / lt_slope_r).powi(2) + dx / lt_slope_r).exp();
}
y += step_height_r * height * 0.5 * erfc(dx / sigma_sqrt2);
y
})
.collect()
}
pub fn estimate_height_position_fwhm(x: &[f64], y: &[f64]) -> Option<(f64, f64, f64, f64)> {
if x.len() != y.len() || x.len() < 3 {
return None;
}
let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
let mut max_y = f64::NEG_INFINITY;
let mut max_idx = 0;
for (i, &yi) in y.iter().enumerate() {
if yi > max_y {
max_y = yi;
max_idx = i;
}
}
let height = max_y - bg;
let centroid = x[max_idx];
let half_max = bg + height / 2.0;
let mut left = max_idx;
while left > 0 && y[left] > half_max {
left -= 1;
}
let mut right = max_idx;
while right < y.len() - 1 && y[right] > half_max {
right += 1;
}
let fwhm = if right > left {
x[right] - x[left]
} else {
(x[x.len() - 1] - x[0]).abs() / 4.0
};
let fwhm = if fwhm > 0.0 {
fwhm
} else {
(x[x.len() - 1] - x[0]).abs() / 4.0
};
Some((height, centroid, fwhm, bg))
}
pub fn estimate_gaussian(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, bg])
}
pub fn estimate_gaussian_area(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
let area = (2.0 * std::f64::consts::PI).sqrt() * h * f / fwhm_to_sigma_factor();
Some(vec![area, c, f, bg])
}
pub fn estimate_lorentzian(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, bg])
}
pub fn estimate_pseudo_voigt(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, 0.5, bg])
}
pub fn estimate_lorentzian_area(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
let area = h * f * 0.5 * std::f64::consts::PI;
Some(vec![area, c, f, bg])
}
pub fn estimate_split_gaussian(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, f, bg])
}
pub fn estimate_split_lorentzian(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, f, bg])
}
pub fn estimate_pseudo_voigt_area(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
let lorentz_area = h * f * 0.5 * std::f64::consts::PI;
let gauss_area = (h * f / fwhm_to_sigma_factor()) * (2.0 * std::f64::consts::PI).sqrt();
let area = 0.5 * lorentz_area + 0.5 * gauss_area;
Some(vec![area, c, f, 0.5, bg])
}
pub fn estimate_split_pseudo_voigt(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, f, 0.5, bg])
}
pub fn estimate_split_pseudo_voigt2(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
Some(vec![h, c, f, f, 0.5, 0.5, bg])
}
fn convolve_valid(y: &[f64], kernel: &[f64]) -> Vec<f64> {
let (n, m) = (y.len(), kernel.len());
if n < m || m == 0 {
return Vec::new();
}
(0..=n - m)
.map(|k| (0..m).map(|j| y[k + j] * kernel[m - 1 - j]).sum())
.collect()
}
fn estimate_step(x: &[f64], y: &[f64], kernel: &[f64], use_deriv_height: bool) -> Option<Vec<f64>> {
if x.len() != y.len() || x.len() < 3 {
return None;
}
let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
let max_y = y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let data_amplitude = max_y - bg;
let cutoff = kernel.len() / 2;
let mut y_deriv = convolve_valid(y, kernel);
let max_deriv = y_deriv.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if max_deriv > 0.0 {
let scale = max_y / max_deriv;
for v in &mut y_deriv {
*v *= scale;
}
}
let (height, center, fwhm) = if y_deriv.len() >= 3 && x.len() > 2 * cutoff {
let x_slice = &x[cutoff..x.len() - cutoff];
match estimate_height_position_fwhm(x_slice, &y_deriv) {
Some((h, c, f, _b)) => {
let height = if use_deriv_height && h > data_amplitude {
h
} else {
data_amplitude
};
(height, c, f)
}
None => {
let (c, f) = step_fallback(x);
(data_amplitude, c, f)
}
}
} else {
let (c, f) = step_fallback(x);
(data_amplitude, c, f)
};
Some(vec![height, center, fwhm, bg])
}
fn step_fallback(x: &[f64]) -> (f64, f64) {
let center = x[x.len() / 2];
let dx = if x.len() > 1 { x[1] - x[0] } else { 1.0 };
(center, 8.0 * dx)
}
pub fn estimate_stepup(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
estimate_step(x, y, &[0.25, 0.75, 0.0, -0.75, -0.25], true)
}
pub fn estimate_stepdown(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
estimate_step(x, y, &[-0.25, -0.75, 0.0, 0.75, 0.25], false)
}
pub fn estimate_atan_stepup(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
estimate_stepup(x, y)
}
pub fn estimate_slit(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let up = estimate_stepup(x, y)?; let down = estimate_stepdown(x, y)?; let (center_up, fwhm_up) = (up[1], up[2]);
let center_down = down[1];
let edge_distance = (center_down - center_up).abs();
let bg = y.iter().copied().fold(f64::INFINITY, f64::min);
let y_minus_bg: Vec<f64> = y.iter().map(|&yi| yi - bg).collect();
let height = y_minus_bg.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let threshold = 0.5 * height;
let first = y_minus_bg.iter().position(|&v| v >= threshold)?;
let last = y_minus_bg.iter().rposition(|&v| v >= threshold)?;
let position = (x[first] + x[last]) / 2.0;
let fwhm = x[last] - x[first];
let mut beamfwhm = 0.5 * (fwhm_up + center_down);
beamfwhm = beamfwhm.min(edge_distance / 10.0);
let xmin = x.iter().copied().fold(f64::INFINITY, f64::min);
let xmax = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
beamfwhm = beamfwhm.max((xmax - xmin) * 3.0 / x.len() as f64);
Some(vec![height, position, fwhm, beamfwhm, bg])
}
pub fn estimate_ahypermet(x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
let (h, c, f, bg) = estimate_height_position_fwhm(x, y)?;
let area = h * f / fwhm_to_sigma_factor() * (2.0 * std::f64::consts::PI).sqrt();
Some(vec![area, c, f, 0.05, 0.70, 0.05, 20.0, 0.002, bg])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeakModel {
Gaussian,
GaussianArea,
SplitGaussian,
Lorentzian,
LorentzianArea,
SplitLorentzian,
PseudoVoigt,
AreaPseudoVoigt,
SplitPseudoVoigt,
SplitPseudoVoigt2,
StepDown,
StepUp,
Slit,
AtanStepUp,
Hypermet,
Polynomial2,
Polynomial3,
Polynomial4,
Polynomial5,
}
impl PeakModel {
fn poly_degree(self) -> Option<usize> {
match self {
PeakModel::Polynomial2 => Some(2),
PeakModel::Polynomial3 => Some(3),
PeakModel::Polynomial4 => Some(4),
PeakModel::Polynomial5 => Some(5),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
PeakModel::Gaussian => "Gaussian",
PeakModel::GaussianArea => "Gaussian (Area)",
PeakModel::SplitGaussian => "Split Gaussian",
PeakModel::Lorentzian => "Lorentzian",
PeakModel::LorentzianArea => "Lorentzian (Area)",
PeakModel::SplitLorentzian => "Split Lorentzian",
PeakModel::PseudoVoigt => "Pseudo-Voigt",
PeakModel::AreaPseudoVoigt => "Pseudo-Voigt (Area)",
PeakModel::SplitPseudoVoigt => "Split Pseudo-Voigt",
PeakModel::SplitPseudoVoigt2 => "Split Pseudo-Voigt 2",
PeakModel::StepDown => "Step Down",
PeakModel::StepUp => "Step Up",
PeakModel::Slit => "Slit",
PeakModel::AtanStepUp => "Arctan Step Up",
PeakModel::Hypermet => "Hypermet",
PeakModel::Polynomial2 => "Degree 2 Polynomial",
PeakModel::Polynomial3 => "Degree 3 Polynomial",
PeakModel::Polynomial4 => "Degree 4 Polynomial",
PeakModel::Polynomial5 => "Degree 5 Polynomial",
}
}
pub fn param_names(self) -> Vec<String> {
let owned = |s: &str| s.to_string();
match self {
PeakModel::Gaussian => vec![
owned("Height"),
owned("Center"),
owned("FWHM"),
owned("Background"),
],
PeakModel::GaussianArea => vec![
owned("Area"),
owned("Center"),
owned("FWHM"),
owned("Background"),
],
PeakModel::SplitGaussian | PeakModel::SplitLorentzian => vec![
owned("Height"),
owned("Center"),
owned("FWHM1"),
owned("FWHM2"),
owned("Background"),
],
PeakModel::Lorentzian => vec![
owned("Height"),
owned("Center"),
owned("FWHM"),
owned("Background"),
],
PeakModel::LorentzianArea => vec![
owned("Area"),
owned("Center"),
owned("FWHM"),
owned("Background"),
],
PeakModel::PseudoVoigt => vec![
owned("Height"),
owned("Center"),
owned("FWHM"),
owned("Eta"),
owned("Background"),
],
PeakModel::AreaPseudoVoigt => vec![
owned("Area"),
owned("Center"),
owned("FWHM"),
owned("Eta"),
owned("Background"),
],
PeakModel::SplitPseudoVoigt => vec![
owned("Height"),
owned("Center"),
owned("FWHM1"),
owned("FWHM2"),
owned("Eta"),
owned("Background"),
],
PeakModel::SplitPseudoVoigt2 => vec![
owned("Height"),
owned("Center"),
owned("FWHM1"),
owned("FWHM2"),
owned("Eta1"),
owned("Eta2"),
owned("Background"),
],
PeakModel::StepDown | PeakModel::StepUp => vec![
owned("Height"),
owned("Center"),
owned("FWHM"),
owned("Background"),
],
PeakModel::Slit => vec![
owned("Height"),
owned("Center"),
owned("FWHM"),
owned("BeamFWHM"),
owned("Background"),
],
PeakModel::AtanStepUp => vec![
owned("Height"),
owned("Center"),
owned("Width"),
owned("Background"),
],
PeakModel::Hypermet => vec![
owned("Area"),
owned("Center"),
owned("FWHM"),
owned("ST Area Ratio"),
owned("ST Slope Ratio"),
owned("LT Area Ratio"),
owned("LT Slope Ratio"),
owned("Step Height Ratio"),
owned("Background"),
],
PeakModel::Polynomial2
| PeakModel::Polynomial3
| PeakModel::Polynomial4
| PeakModel::Polynomial5 => {
let degree = self.poly_degree().expect("polynomial variant");
(0..=degree)
.map(|i| ((b'a' + i as u8) as char).to_string())
.collect()
}
}
}
pub fn eval(self, x: &[f64], params: &[f64]) -> Vec<f64> {
match self {
PeakModel::Gaussian => gaussian_model(x, params),
PeakModel::GaussianArea => gaussian_area_model(x, params),
PeakModel::SplitGaussian => split_gaussian_model(x, params),
PeakModel::Lorentzian => lorentzian_model(x, params),
PeakModel::LorentzianArea => lorentzian_area_model(x, params),
PeakModel::SplitLorentzian => split_lorentzian_model(x, params),
PeakModel::PseudoVoigt => pseudo_voigt_model(x, params),
PeakModel::AreaPseudoVoigt => pseudo_voigt_area_model(x, params),
PeakModel::SplitPseudoVoigt => split_pseudo_voigt_model(x, params),
PeakModel::SplitPseudoVoigt2 => split_pseudo_voigt2_model(x, params),
PeakModel::StepDown => stepdown_model(x, params),
PeakModel::StepUp => stepup_model(x, params),
PeakModel::Slit => slit_model(x, params),
PeakModel::AtanStepUp => atan_stepup_model(x, params),
PeakModel::Hypermet => hypermet_model(x, params),
PeakModel::Polynomial2
| PeakModel::Polynomial3
| PeakModel::Polynomial4
| PeakModel::Polynomial5 => crate::core::background::poly_eval(params, x),
}
}
pub fn estimate(self, x: &[f64], y: &[f64]) -> Option<Vec<f64>> {
match self {
PeakModel::Gaussian => estimate_gaussian(x, y),
PeakModel::GaussianArea => estimate_gaussian_area(x, y),
PeakModel::SplitGaussian => estimate_split_gaussian(x, y),
PeakModel::Lorentzian => estimate_lorentzian(x, y),
PeakModel::LorentzianArea => estimate_lorentzian_area(x, y),
PeakModel::SplitLorentzian => estimate_split_lorentzian(x, y),
PeakModel::PseudoVoigt => estimate_pseudo_voigt(x, y),
PeakModel::AreaPseudoVoigt => estimate_pseudo_voigt_area(x, y),
PeakModel::SplitPseudoVoigt => estimate_split_pseudo_voigt(x, y),
PeakModel::SplitPseudoVoigt2 => estimate_split_pseudo_voigt2(x, y),
PeakModel::StepDown => estimate_stepdown(x, y),
PeakModel::StepUp => estimate_stepup(x, y),
PeakModel::Slit => estimate_slit(x, y),
PeakModel::AtanStepUp => estimate_atan_stepup(x, y),
PeakModel::Hypermet => estimate_ahypermet(x, y),
PeakModel::Polynomial2
| PeakModel::Polynomial3
| PeakModel::Polynomial4
| PeakModel::Polynomial5 => {
let degree = self.poly_degree().expect("polynomial variant");
crate::core::background::polyfit(x, y, degree)
}
}
}
}
#[derive(Debug, Clone)]
pub struct IterativeFitResult {
pub fit: FitResult,
pub solver: LeastSqResult,
}
impl IterativeFitResult {
pub fn std_errors(&self) -> Vec<f64> {
self.solver.std_errors()
}
pub fn uncertainties(&self) -> &[f64] {
&self.solver.uncertainties
}
pub fn reduced_chisq(&self) -> Option<f64> {
self.solver.reduced_chisq
}
}
pub struct IterativeFit {
pub model: PeakModel,
pub max_iter: usize,
pub deltachi: f64,
}
impl IterativeFit {
pub fn new(model: PeakModel) -> Self {
Self {
model,
max_iter: DEFAULT_MAX_ITER,
deltachi: DEFAULT_DELTACHI,
}
}
pub fn fit_full(&self, x: &[f64], y: &[f64]) -> Option<IterativeFitResult> {
let p0 = self.model.estimate(x, y)?;
let model = self.model;
let solver = leastsq(
|xx, pp| model.eval(xx, pp),
x,
y,
&p0,
None,
self.max_iter,
self.deltachi,
true,
)
.ok()?;
let y_fit = self.model.eval(x, &solver.parameters);
let fit = FitResult {
y_fit,
parameters: solver.parameters.clone(),
param_names: self.model.param_names(),
};
Some(IterativeFitResult { fit, solver })
}
}
impl FitFunction for IterativeFit {
fn name(&self) -> &str {
self.model.name()
}
fn fit(&self, x: &[f64], y: &[f64]) -> Option<FitResult> {
self.fit_full(x, y).map(|r| r.fit)
}
}
pub fn fit_in_range(
xs: &[f64],
ys: &[f64],
xmin: f64,
xmax: f64,
model: &IterativeFit,
) -> Option<IterativeFitResult> {
if xs.len() != ys.len() {
return None;
}
let (lo, hi) = if xmin <= xmax {
(xmin, xmax)
} else {
(xmax, xmin)
};
let mut xr = Vec::new();
let mut yr = Vec::new();
for (&xi, &yi) in xs.iter().zip(ys.iter()) {
if xi >= lo && xi <= hi {
xr.push(xi);
yr.push(yi);
}
}
if xr.len() < 3 {
return None;
}
model.fit_full(&xr, &yr)
}
pub fn multi_gaussian_model(x: &[f64], params: &[f64]) -> Vec<f64> {
let inv = 1.0 / fwhm_to_sigma_factor();
let mut y = vec![0.0_f64; x.len()];
for triple in params.chunks_exact(3) {
let (height, centroid, fwhm) = (triple[0], triple[1], triple[2]);
let sigma = fwhm * inv;
if sigma == 0.0 {
continue;
}
for (yi, &xi) in y.iter_mut().zip(x.iter()) {
let dhelp = (xi - centroid) / sigma;
if dhelp <= 20.0 {
*yi += height * (-0.5 * dhelp * dhelp).exp();
}
}
}
y
}
pub fn estimate_multi_gaussian(
x: &[f64],
y: &[f64],
search_fwhm: f64,
sensitivity: f64,
) -> Option<(Vec<f64>, Vec<Constraint>)> {
let npoints = y.len();
if npoints == 0 || x.len() != npoints {
return None;
}
let search_fwhm = search_fwhm.max(3.0);
let search_sens = sensitivity.max(1.0);
let bg = crate::core::background::estimation_strip_bg(y);
let found = crate::core::peaks::padded_peak_search(y, search_fwhm, search_sens);
let peaks: Vec<usize> = if found.is_empty() {
let delta: Vec<f64> = y.iter().zip(bg.iter()).map(|(&a, &b)| a - b).collect();
let maxv = delta.iter().copied().fold(f64::NEG_INFINITY, f64::max);
match delta.iter().position(|&v| v == maxv) {
Some(p) => vec![p],
None => return None,
}
} else {
found.iter().map(|p| p.index).collect()
};
if peaks.is_empty() {
return None;
}
let sig = 5.0 * (x[npoints - 1] - x[0]).abs() / npoints as f64;
let mut param: Vec<f64> = Vec::with_capacity(peaks.len() * 3);
let mut index_largest = 0usize;
let mut height_largest = f64::NEG_INFINITY;
for (k, &pi) in peaks.iter().enumerate() {
let height = y[pi] - bg[pi];
let pos = if k == 0 && x[pi].abs() < 1.0e-16 {
0.0
} else {
x[pi]
};
param.push(height);
param.push(pos);
param.push(sig);
if height > height_largest {
height_largest = height;
index_largest = k;
}
}
let _ = index_largest;
let sf = search_fwhm as usize;
let (fwhmx, use_fwhmx) = if x.len() > sf {
((x[sf] - x[0]).abs(), true)
} else {
(0.0, false)
};
let xmin = x.iter().copied().fold(f64::INFINITY, f64::min);
let xmax = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let mut prelim: Vec<Constraint> = Vec::with_capacity(param.len());
for k in 0..peaks.len() {
let pos = param[3 * k + 1];
prelim.push(Constraint::Positive);
if use_fwhmx && fwhmx > 0.0 {
prelim.push(Constraint::Quoted {
min: pos - 0.5 * fwhmx,
max: pos + 0.5 * fwhmx,
});
} else if xmax > xmin {
prelim.push(Constraint::Quoted {
min: xmin,
max: xmax,
});
} else {
prelim.push(Constraint::Free);
}
prelim.push(Constraint::Positive);
}
let yw: Vec<f64> = y.iter().zip(bg.iter()).map(|(&a, &b)| a - b).collect();
let fittedpar = leastsq_constrained(
multi_gaussian_model,
x,
&yw,
¶m,
&prelim,
None,
4,
DEFAULT_DELTACHI,
false,
)
.map(|r| r.parameters)
.unwrap_or(param);
let mut cons: Vec<Constraint> = Vec::with_capacity(fittedpar.len());
for _ in 0..peaks.len() {
cons.push(Constraint::Positive);
cons.push(Constraint::Free);
cons.push(Constraint::Positive);
}
Some((fittedpar, cons))
}
pub fn fit_multi_gaussian(
x: &[f64],
y: &[f64],
search_fwhm: f64,
sensitivity: f64,
max_iter: usize,
deltachi: f64,
) -> Option<LeastSqResult> {
let (seeds, cons) = estimate_multi_gaussian(x, y, search_fwhm, sensitivity)?;
leastsq_constrained(
multi_gaussian_model,
x,
y,
&seeds,
&cons,
None,
max_iter,
deltachi,
true,
)
.ok()
}
pub fn fit_multi_gaussian_full(
x: &[f64],
y: &[f64],
search_fwhm: f64,
sensitivity: f64,
max_iter: usize,
deltachi: f64,
) -> Option<IterativeFitResult> {
let solver = fit_multi_gaussian(x, y, search_fwhm, sensitivity, max_iter, deltachi)?;
let y_fit = multi_gaussian_model(x, &solver.parameters);
let mut param_names = Vec::with_capacity(solver.parameters.len());
for peak in 0..solver.parameters.len() / 3 {
let i = peak + 1;
param_names.push(format!("Height {i}"));
param_names.push(format!("Center {i}"));
param_names.push(format!("FWHM {i}"));
}
let fit = FitResult {
y_fit,
parameters: solver.parameters.clone(),
param_names,
};
Some(IterativeFitResult { fit, solver })
}
pub fn fit_peak_from(
model: PeakModel,
x: &[f64],
y: &[f64],
p0: &[f64],
constraints: &[Constraint],
max_iter: usize,
deltachi: f64,
) -> Option<IterativeFitResult> {
if constraints.len() != p0.len() {
return None;
}
let solver = leastsq_constrained(
|xx, pp| model.eval(xx, pp),
x,
y,
p0,
constraints,
None,
max_iter,
deltachi,
true,
)
.ok()?;
let y_fit = model.eval(x, &solver.parameters);
let fit = FitResult {
y_fit,
parameters: solver.parameters.clone(),
param_names: model.param_names(),
};
Some(IterativeFitResult { fit, solver })
}
pub fn fit_peak_constrained(
model: PeakModel,
x: &[f64],
y: &[f64],
constraints: &[Constraint],
max_iter: usize,
deltachi: f64,
) -> Option<IterativeFitResult> {
let p0 = model.estimate(x, y)?;
fit_peak_from(model, x, y, &p0, constraints, max_iter, deltachi)
}
#[derive(Debug, Clone)]
pub struct BackgroundPeakFit {
pub peak: IterativeFitResult,
pub background: Vec<f64>,
pub total: Vec<f64>,
}
pub fn fit_peak_with_background(
model: PeakModel,
background: crate::core::background::Background,
x: &[f64],
y: &[f64],
max_iter: usize,
deltachi: f64,
) -> Option<BackgroundPeakFit> {
if x.is_empty() || x.len() != y.len() {
return None;
}
let bg = background.compute(x, y);
let residual: Vec<f64> = y.iter().zip(&bg).map(|(&yi, &bi)| yi - bi).collect();
let fitter = IterativeFit {
model,
max_iter,
deltachi,
};
let peak = fitter.fit_full(x, &residual)?;
let total: Vec<f64> = bg
.iter()
.zip(&peak.fit.y_fit)
.map(|(&bi, &fi)| bi + fi)
.collect();
Some(BackgroundPeakFit {
peak,
background: bg,
total,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_gaussian(xs: &[f64], height: f64, center: f64, fwhm: f64, bg: f64) -> Vec<f64> {
gaussian_model(xs, &[height, center, fwhm, bg])
}
fn linspace(a: f64, b: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| a + (b - a) * (i as f64) / ((n - 1) as f64))
.collect()
}
#[test]
fn invert_identity() {
let id = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let inv = invert_matrix(&id).unwrap();
assert_eq!(inv, id);
}
#[test]
fn invert_known_2x2() {
let m = vec![vec![4.0, 7.0], vec![2.0, 6.0]];
let inv = invert_matrix(&m).unwrap();
let expected = [[0.6, -0.7], [-0.2, 0.4]];
for i in 0..2 {
for j in 0..2 {
assert!((inv[i][j] - expected[i][j]).abs() < 1e-12);
}
}
}
#[test]
fn invert_singular_returns_none() {
let m = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
assert!(invert_matrix(&m).is_none());
}
#[test]
fn lm_budget_counts_lambda_attempts_like_silx() {
let xs = linspace(0.0, 3.0, 16);
let ys: Vec<f64> = xs.iter().map(|&x| x.exp()).collect();
let expo = |x: &[f64], p: &[f64]| {
x.iter()
.map(|&xi| p[0] * (p[1] * xi).exp())
.collect::<Vec<_>>()
};
let r8 = leastsq(
expo,
&xs,
&ys,
&[1.0, 3.0],
None,
8,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert_eq!(r8.niter, 5, "rejected-λ attempts must consume the budget");
assert!(
(r8.parameters[0] - 0.156_263_258_615_617_27).abs() < 1e-6,
"a {}",
r8.parameters[0]
);
assert!(
(r8.parameters[1] - 1.609_532_248_302_758).abs() < 1e-6,
"b {}",
r8.parameters[1]
);
let r100 = leastsq(
expo,
&xs,
&ys,
&[1.0, 3.0],
None,
100,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert_eq!(
r100.niter, 15,
"converged trajectory length must match silx"
);
assert!((r100.parameters[0] - 1.0).abs() < 1e-6);
assert!((r100.parameters[1] - 1.0).abs() < 1e-6);
}
#[test]
fn central_jacobian_probes_both_sides_and_forward_does_not() {
use std::cell::RefCell;
let calls: RefCell<Vec<Vec<f64>>> = RefCell::new(Vec::new());
let model = |x: &[f64], p: &[f64]| {
calls.borrow_mut().push(p.to_vec());
x.iter().map(|&xi| p[0] * xi + p[1]).collect::<Vec<_>>()
};
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
let d = f64::EPSILON.sqrt();
leastsq(
model,
&xs,
&ys,
&[1.0, 1.0],
None,
1,
DEFAULT_DELTACHI,
false,
)
.unwrap();
let fwd = calls.borrow().clone();
assert!(
fwd.iter().any(|p| p[0] == 1.0 + d),
"forward mode probes +δ"
);
assert!(
!fwd.iter().any(|p| p[0] == 1.0 - d || p[1] == 1.0 - d),
"forward mode must not probe −δ"
);
calls.borrow_mut().clear();
leastsq(
model,
&xs,
&ys,
&[1.0, 1.0],
None,
1,
DEFAULT_DELTACHI,
true,
)
.unwrap();
let cen = calls.borrow().clone();
assert!(
cen.iter().any(|p| p[0] == 1.0 + d),
"central mode probes +δ for parameter 0"
);
assert!(
cen.iter().any(|p| p[0] == 1.0 - d),
"central mode probes −δ for parameter 0"
);
assert!(
cen.iter().any(|p| p[1] == 1.0 - d),
"central mode probes −δ for every parameter"
);
}
#[test]
fn central_jacobian_tracks_silx_central_trajectory() {
let xs = linspace(0.0, 3.0, 16);
let ys: Vec<f64> = xs.iter().map(|&x| x.exp()).collect();
let expo = |x: &[f64], p: &[f64]| {
x.iter()
.map(|&xi| p[0] * (p[1] * xi).exp())
.collect::<Vec<_>>()
};
let r5 = leastsq(expo, &xs, &ys, &[1.0, 3.0], None, 5, DEFAULT_DELTACHI, true).unwrap();
assert_eq!(r5.niter, 5);
assert!(
(r5.parameters[0] - 0.156_263_240_765_631_53).abs() < 1e-6,
"a {}",
r5.parameters[0]
);
assert!(
(r5.parameters[1] - 1.609_532_321_396_835_6).abs() < 1e-6,
"b {}",
r5.parameters[1]
);
}
#[test]
fn central_jacobian_expands_constraints_on_the_minus_probe() {
use std::cell::RefCell;
let calls: RefCell<Vec<Vec<f64>>> = RefCell::new(Vec::new());
let model = |x: &[f64], p: &[f64]| {
calls.borrow_mut().push(p.to_vec());
x.iter().map(|&xi| p[0] * xi + p[1]).collect::<Vec<_>>()
};
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x + 5.0).collect();
let cons = [
Constraint::Free,
Constraint::Factor {
reference: 0,
factor: 2.0,
},
];
let d = f64::EPSILON.sqrt();
leastsq_constrained(
model,
&xs,
&ys,
&[1.0, 2.0],
&cons,
None,
1,
DEFAULT_DELTACHI,
true,
)
.unwrap();
let cen = calls.borrow().clone();
assert!(
cen.iter()
.any(|p| p[0] == 1.0 + d && p[1] == 2.0 * (1.0 + d)),
"+δ probe arrives constraint-expanded"
);
assert!(
cen.iter()
.any(|p| p[0] == 1.0 - d && p[1] == 2.0 * (1.0 - d)),
"−δ probe arrives constraint-expanded"
);
}
#[test]
fn leastsq_recovers_noiseless_line_exactly() {
let xs = linspace(-5.0, 5.0, 21);
let (a_true, b_true) = (2.5, -1.0);
let ys: Vec<f64> = xs.iter().map(|&x| a_true * x + b_true).collect();
let model = |x: &[f64], p: &[f64]| x.iter().map(|&xi| p[0] * xi + p[1]).collect::<Vec<_>>();
let res = leastsq(
model,
&xs,
&ys,
&[0.0, 0.0],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(res.parameters[0] - a_true).abs() < 1e-6,
"slope {} vs {}",
res.parameters[0],
a_true
);
assert!(
(res.parameters[1] - b_true).abs() < 1e-6,
"intercept {} vs {}",
res.parameters[1],
b_true
);
assert!(res.chisq < 1e-12, "chisq {}", res.chisq);
}
#[test]
fn leastsq_converges_on_noisy_gaussian() {
let xs = linspace(-3.0, 7.0, 101);
let clean = synth_gaussian(&xs, 10.0, 2.0, 1.5, 1.0);
let ys: Vec<f64> = clean
.iter()
.enumerate()
.map(|(i, &c)| c + 0.05 * ((i as f64) * 0.7).sin())
.collect();
let fit = IterativeFit::new(PeakModel::Gaussian)
.fit_full(&xs, &ys)
.expect("fit should succeed");
let p = &fit.fit.parameters;
assert!((p[0] - 10.0).abs() < 0.2, "height {}", p[0]);
assert!((p[1] - 2.0).abs() < 0.05, "center {}", p[1]);
assert!((p[2] - 1.5).abs() < 0.1, "fwhm {}", p[2]);
assert!((p[3] - 1.0).abs() < 0.1, "bg {}", p[3]);
let rc = fit.reduced_chisq().unwrap();
assert!(rc < 0.01, "reduced chisq {}", rc);
}
#[test]
fn gaussian_model_recovers_own_peak() {
let xs = linspace(0.0, 20.0, 201);
let ys = synth_gaussian(&xs, 5.0, 8.0, 2.0, 0.5);
let fit = IterativeFit::new(PeakModel::Gaussian)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - 5.0).abs() < 1e-3, "height {}", p[0]);
assert!((p[1] - 8.0).abs() < 1e-3, "center {}", p[1]);
assert!((p[2] - 2.0).abs() < 1e-3, "fwhm {}", p[2]);
assert!((p[3] - 0.5).abs() < 1e-3, "bg {}", p[3]);
assert!(fit.reduced_chisq().unwrap() < 1e-6);
}
#[test]
fn gaussian_area_model_recovers_own_peak() {
let xs = linspace(0.0, 20.0, 201);
let area = 12.0;
let ys = gaussian_area_model(&xs, &[area, 9.0, 2.5, 0.2]);
let fit = IterativeFit::new(PeakModel::GaussianArea)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - area).abs() < 1e-2, "area {}", p[0]);
assert!((p[1] - 9.0).abs() < 1e-3, "center {}", p[1]);
assert!((p[2] - 2.5).abs() < 1e-3, "fwhm {}", p[2]);
assert!((p[3] - 0.2).abs() < 1e-3, "bg {}", p[3]);
assert!(fit.reduced_chisq().unwrap() < 1e-6);
}
#[test]
fn lorentzian_model_recovers_own_peak() {
let xs = linspace(0.0, 20.0, 201);
let ys = lorentzian_model(&xs, &[7.0, 11.0, 3.0, 1.0]);
let fit = IterativeFit::new(PeakModel::Lorentzian)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - 7.0).abs() < 1e-2, "height {}", p[0]);
assert!((p[1] - 11.0).abs() < 1e-3, "center {}", p[1]);
assert!((p[2] - 3.0).abs() < 1e-2, "fwhm {}", p[2]);
assert!((p[3] - 1.0).abs() < 1e-2, "bg {}", p[3]);
assert!(fit.reduced_chisq().unwrap() < 1e-6);
}
#[test]
fn pseudo_voigt_model_recovers_own_peak() {
let xs = linspace(0.0, 20.0, 301);
let ys = pseudo_voigt_model(&xs, &[6.0, 10.0, 2.0, 0.4, 0.5]);
let fit = IterativeFit::new(PeakModel::PseudoVoigt)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - 6.0).abs() < 5e-2, "height {}", p[0]);
assert!((p[1] - 10.0).abs() < 1e-2, "center {}", p[1]);
assert!((p[2] - 2.0).abs() < 5e-2, "fwhm {}", p[2]);
assert!((p[3] - 0.4).abs() < 5e-2, "eta {}", p[3]);
assert!((p[4] - 0.5).abs() < 5e-2, "bg {}", p[4]);
assert!(fit.reduced_chisq().unwrap() < 1e-4);
}
#[test]
fn lorentzian_area_model_recovers_own_peak() {
let xs = linspace(0.0, 20.0, 201);
let area = 9.0;
let ys = lorentzian_area_model(&xs, &[area, 11.0, 3.0, 0.5]);
let fit = IterativeFit::new(PeakModel::LorentzianArea)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - area).abs() < 5e-2, "area {}", p[0]);
assert!((p[1] - 11.0).abs() < 1e-3, "center {}", p[1]);
assert!((p[2] - 3.0).abs() < 1e-2, "fwhm {}", p[2]);
assert!((p[3] - 0.5).abs() < 1e-2, "bg {}", p[3]);
assert!(fit.reduced_chisq().unwrap() < 1e-6);
}
#[test]
fn lorentzian_area_peak_value_matches_area_conversion() {
let height = 4.0;
let fwhm = 2.5;
let area = height * fwhm * 0.5 * std::f64::consts::PI;
let peak = lorentzian_area_model(&[7.0], &[area, 7.0, fwhm, 0.0])[0];
assert!(
(peak - height).abs() < 1e-12,
"peak {peak} vs height {height}"
);
}
#[test]
fn split_gaussian_model_recovers_asymmetric_peak() {
let xs = linspace(0.0, 20.0, 401);
let ys = split_gaussian_model(&xs, &[5.0, 10.0, 2.0, 4.0, 0.3]);
let fit = IterativeFit::new(PeakModel::SplitGaussian)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - 5.0).abs() < 5e-2, "height {}", p[0]);
assert!((p[1] - 10.0).abs() < 1e-2, "center {}", p[1]);
assert!((p[2] - 2.0).abs() < 5e-2, "fwhm1 {}", p[2]);
assert!((p[3] - 4.0).abs() < 5e-2, "fwhm2 {}", p[3]);
assert!((p[4] - 0.3).abs() < 1e-2, "bg {}", p[4]);
assert!(fit.reduced_chisq().unwrap() < 1e-4);
}
#[test]
fn split_lorentzian_model_recovers_asymmetric_peak() {
let xs = linspace(0.0, 20.0, 401);
let ys = split_lorentzian_model(&xs, &[6.0, 9.0, 2.0, 5.0, 0.4]);
let fit = IterativeFit::new(PeakModel::SplitLorentzian)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - 6.0).abs() < 5e-2, "height {}", p[0]);
assert!((p[1] - 9.0).abs() < 1e-2, "center {}", p[1]);
assert!((p[2] - 2.0).abs() < 5e-2, "fwhm1 {}", p[2]);
assert!((p[3] - 5.0).abs() < 5e-2, "fwhm2 {}", p[3]);
assert!((p[4] - 0.4).abs() < 1e-2, "bg {}", p[4]);
assert!(fit.reduced_chisq().unwrap() < 1e-4);
}
#[test]
fn split_models_reduce_to_symmetric_when_fwhms_equal() {
let xs = linspace(0.0, 20.0, 101);
let sg = split_gaussian_model(&xs, &[5.0, 10.0, 3.0, 3.0, 0.0]);
let g = gaussian_model(&xs, &[5.0, 10.0, 3.0, 0.0]);
for (a, b) in sg.iter().zip(&g) {
assert!((a - b).abs() < 1e-12, "split gauss {a} vs gauss {b}");
}
let sl = split_lorentzian_model(&xs, &[5.0, 10.0, 3.0, 3.0, 0.0]);
let l = lorentzian_model(&xs, &[5.0, 10.0, 3.0, 0.0]);
for (a, b) in sl.iter().zip(&l) {
assert!((a - b).abs() < 1e-12, "split lorentz {a} vs lorentz {b}");
}
}
#[test]
fn pseudo_voigt_area_model_recovers_own_peak() {
let xs = linspace(0.0, 20.0, 401);
let area = 10.0;
let ys = pseudo_voigt_area_model(&xs, &[area, 10.0, 2.5, 0.4, 0.3]);
let fit = IterativeFit::new(PeakModel::AreaPseudoVoigt)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - area).abs() < 1e-1, "area {}", p[0]);
assert!((p[1] - 10.0).abs() < 1e-2, "center {}", p[1]);
assert!((p[2] - 2.5).abs() < 5e-2, "fwhm {}", p[2]);
assert!((p[3] - 0.4).abs() < 5e-2, "eta {}", p[3]);
assert!((p[4] - 0.3).abs() < 1e-2, "bg {}", p[4]);
assert!(fit.reduced_chisq().unwrap() < 1e-4);
}
#[test]
fn split_pseudo_voigt_model_recovers_asymmetric_peak() {
let xs = linspace(0.0, 20.0, 501);
let ys = split_pseudo_voigt_model(&xs, &[5.0, 10.0, 2.0, 4.0, 0.4, 0.3]);
let fit = IterativeFit::new(PeakModel::SplitPseudoVoigt)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[1] - 10.0).abs() < 2e-2, "center {}", p[1]);
assert!((p[2] - 2.0).abs() < 1e-1, "fwhm1 {}", p[2]);
assert!((p[3] - 4.0).abs() < 1e-1, "fwhm2 {}", p[3]);
assert!((p[5] - 0.3).abs() < 2e-2, "bg {}", p[5]);
assert!(fit.reduced_chisq().unwrap() < 1e-4);
}
#[test]
fn split_pseudo_voigt2_model_recovers_per_side_eta() {
let xs = linspace(0.0, 20.0, 501);
let ys = split_pseudo_voigt2_model(&xs, &[5.0, 10.0, 2.5, 4.0, 0.2, 0.7, 0.3]);
let fit = IterativeFit::new(PeakModel::SplitPseudoVoigt2)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[1] - 10.0).abs() < 2e-2, "center {}", p[1]);
assert!((p[2] - 2.5).abs() < 1e-1, "fwhm1 {}", p[2]);
assert!((p[3] - 4.0).abs() < 1e-1, "fwhm2 {}", p[3]);
assert!((p[6] - 0.3).abs() < 2e-2, "bg {}", p[6]);
assert!(fit.reduced_chisq().unwrap() < 1e-4);
}
#[test]
fn split_pseudo_voigt_reduces_to_pseudo_voigt_when_symmetric() {
let xs = linspace(0.0, 20.0, 101);
let spv = split_pseudo_voigt_model(&xs, &[5.0, 10.0, 3.0, 3.0, 0.4, 0.0]);
let pv = pseudo_voigt_model(&xs, &[5.0, 10.0, 3.0, 0.4, 0.0]);
for (a, b) in spv.iter().zip(&pv) {
assert!((a - b).abs() < 1e-12, "split pvoigt {a} vs pvoigt {b}");
}
}
#[test]
fn split_pseudo_voigt2_reduces_to_pseudo_voigt_when_symmetric() {
let xs = linspace(0.0, 20.0, 101);
let spv2 = split_pseudo_voigt2_model(&xs, &[5.0, 10.0, 3.0, 3.0, 0.4, 0.4, 0.0]);
let pv = pseudo_voigt_model(&xs, &[5.0, 10.0, 3.0, 0.4, 0.0]);
for (a, b) in spv2.iter().zip(&pv) {
assert!((a - b).abs() < 1e-12, "split pvoigt2 {a} vs pvoigt {b}");
}
}
#[test]
fn polynomial_models_recover_their_coefficients() {
let xs = linspace(-3.0, 3.0, 81);
let q = [1.5, -2.0, 0.7];
let ys = PeakModel::Polynomial2.eval(&xs, &q);
let fit = IterativeFit::new(PeakModel::Polynomial2)
.fit_full(&xs, &ys)
.unwrap();
for (got, want) in fit.fit.parameters.iter().zip(&q) {
assert!((got - want).abs() < 1e-6, "deg2 coef {got} vs {want}");
}
assert!(fit.reduced_chisq().unwrap() < 1e-9);
let c3 = [0.4, -0.5, 0.6, -0.7];
let ys3 = PeakModel::Polynomial3.eval(&xs, &c3);
let fit3 = IterativeFit::new(PeakModel::Polynomial3)
.fit_full(&xs, &ys3)
.unwrap();
for (got, want) in fit3.fit.parameters.iter().zip(&c3) {
assert!((got - want).abs() < 1e-5, "deg3 coef {got} vs {want}");
}
let c5 = [0.05, -0.1, 0.2, -0.3, 0.4, -0.5];
let ys5 = PeakModel::Polynomial5.eval(&xs, &c5);
let fit5 = IterativeFit::new(PeakModel::Polynomial5)
.fit_full(&xs, &ys5)
.unwrap();
assert!(
fit5.reduced_chisq().unwrap() < 1e-6,
"deg5 chisq {}",
fit5.reduced_chisq().unwrap()
);
}
#[test]
fn polynomial_param_names_match_degree() {
assert_eq!(PeakModel::Polynomial2.param_names(), ["a", "b", "c"]);
assert_eq!(PeakModel::Polynomial3.param_names(), ["a", "b", "c", "d"]);
assert_eq!(
PeakModel::Polynomial4.param_names(),
["a", "b", "c", "d", "e"]
);
assert_eq!(
PeakModel::Polynomial5.param_names(),
["a", "b", "c", "d", "e", "f"]
);
}
#[test]
fn hypermet_model_reduces_to_area_gaussian_when_tails_zero() {
let xs = linspace(0.0, 20.0, 201);
let h = hypermet_model(&xs, &[12.0, 10.0, 2.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4]);
let g = gaussian_area_model(&xs, &[12.0, 10.0, 2.5, 0.4]);
for (a, b) in h.iter().zip(&g) {
assert!((a - b).abs() < 1e-12, "hypermet {a} vs area-gauss {b}");
}
}
#[test]
fn hypermet_tails_contribute_beyond_the_gaussian() {
let xs = linspace(0.0, 20.0, 201);
let area = 12.0;
let tailed = hypermet_model(&xs, &[area, 10.0, 2.5, 0.05, 0.70, 0.05, 20.0, 0.002, 0.0]);
let plain = hypermet_model(&xs, &[area, 10.0, 2.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let max_diff = tailed
.iter()
.zip(&plain)
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max);
let peak = plain.iter().cloned().fold(0.0_f64, f64::max);
assert!(
max_diff > 0.01 * peak,
"tail contribution {max_diff} (peak {peak})"
);
}
#[test]
fn hypermet_model_recovers_own_curve() {
let xs = linspace(0.0, 20.0, 401);
let truth = [12.0, 10.0, 2.5, 0.05, 0.70, 0.05, 20.0, 0.002, 0.3];
let ys = hypermet_model(&xs, &truth);
let fit = IterativeFit::new(PeakModel::Hypermet)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[1] - 10.0).abs() < 5e-2, "center {}", p[1]);
assert!(
fit.reduced_chisq().unwrap() < 1e-4,
"chisq {}",
fit.reduced_chisq().unwrap()
);
}
#[test]
fn estimate_ahypermet_seeds_silx_initial_tail_ratios() {
let xs = linspace(0.0, 20.0, 201);
let ys = hypermet_model(&xs, &[12.0, 10.0, 2.5, 0.05, 0.70, 0.05, 20.0, 0.002, 0.3]);
let seed = estimate_ahypermet(&xs, &ys).unwrap();
assert_eq!(seed.len(), 9, "9 seed params");
assert!((seed[1] - 10.0).abs() < 0.2, "center seed {}", seed[1]);
assert_eq!(seed[3], 0.05, "ST area ratio seed");
assert_eq!(seed[4], 0.70, "ST slope ratio seed");
assert_eq!(seed[5], 0.05, "LT area ratio seed");
assert_eq!(seed[6], 20.0, "LT slope ratio seed");
assert_eq!(seed[7], 0.002, "step height ratio seed");
}
#[test]
fn pseudo_voigt_eta_limits_match_gauss_and_lorentz() {
let xs = linspace(0.0, 10.0, 51);
let g = gaussian_model(&xs, &[3.0, 5.0, 2.0, 0.0]);
let pv_g = pseudo_voigt_model(&xs, &[3.0, 5.0, 2.0, 0.0, 0.0]);
for (a, b) in g.iter().zip(pv_g.iter()) {
assert!((a - b).abs() < 1e-12);
}
let l = lorentzian_model(&xs, &[3.0, 5.0, 2.0, 0.0]);
let pv_l = pseudo_voigt_model(&xs, &[3.0, 5.0, 2.0, 1.0, 0.0]);
for (a, b) in l.iter().zip(pv_l.iter()) {
assert!((a - b).abs() < 1e-12);
}
}
#[test]
fn fit_in_range_ignores_outside_points() {
let xs = linspace(0.0, 20.0, 201);
let in_range: Vec<f64> = xs
.iter()
.map(|&x| {
if (4.0..=12.0).contains(&x) {
let sigma = 2.0 / fwhm_to_sigma_factor();
let d = (x - 8.0) / sigma;
5.0 * (-0.5 * d * d).exp() + 0.5
} else {
100.0 + 50.0 * x
}
})
.collect();
let fitter = IterativeFit::new(PeakModel::Gaussian);
let res = fit_in_range(&xs, &in_range, 4.0, 12.0, &fitter).unwrap();
let p = &res.fit.parameters;
assert!((p[1] - 8.0).abs() < 0.05, "center pulled to {}", p[1]);
assert!((p[2] - 2.0).abs() < 0.1, "fwhm {}", p[2]);
assert!((p[0] - 5.0).abs() < 0.2, "height {}", p[0]);
}
#[test]
fn fit_in_range_reversed_bounds_equivalent() {
let xs = linspace(0.0, 20.0, 201);
let ys = synth_gaussian(&xs, 4.0, 10.0, 2.0, 0.3);
let fitter = IterativeFit::new(PeakModel::Gaussian);
let a = fit_in_range(&xs, &ys, 6.0, 14.0, &fitter).unwrap();
let b = fit_in_range(&xs, &ys, 14.0, 6.0, &fitter).unwrap();
for (pa, pb) in a.fit.parameters.iter().zip(b.fit.parameters.iter()) {
assert!((pa - pb).abs() < 1e-12);
}
}
#[test]
fn std_errors_from_covariance_diagonal() {
let res = LeastSqResult {
parameters: vec![1.0, 2.0, 3.0],
covariance: vec![
vec![4.0, 0.1, 0.0],
vec![0.1, 9.0, 0.2],
vec![0.0, 0.2, 16.0],
],
uncertainties: vec![2.0, 3.0, 4.0],
chisq: 0.0,
reduced_chisq: Some(0.0),
niter: 1,
nfev: 1,
};
let errs = res.std_errors();
assert!((errs[0] - 2.0).abs() < 1e-12);
assert!((errs[1] - 3.0).abs() < 1e-12);
assert!((errs[2] - 4.0).abs() < 1e-12);
}
#[test]
fn std_errors_guard_negative_diagonal() {
let res = LeastSqResult {
parameters: vec![1.0],
covariance: vec![vec![-1e-15]],
uncertainties: vec![0.0],
chisq: 0.0,
reduced_chisq: None,
niter: 0,
nfev: 0,
};
let e = res.std_errors();
assert!(e[0].is_finite() && e[0] >= 0.0);
}
#[test]
fn leastsq_length_mismatch_errors() {
let r = leastsq(
|x: &[f64], _p: &[f64]| x.to_vec(),
&[1.0, 2.0, 3.0],
&[1.0, 2.0],
&[0.0],
None,
10,
DEFAULT_DELTACHI,
false,
);
assert_eq!(r.unwrap_err(), FitError::LengthMismatch);
}
#[test]
fn leastsq_rejects_nonfinite() {
let r = leastsq(
|x: &[f64], p: &[f64]| x.iter().map(|&xi| p[0] * xi).collect::<Vec<_>>(),
&[1.0, f64::NAN, 3.0],
&[1.0, 2.0, 3.0],
&[1.0],
None,
10,
DEFAULT_DELTACHI,
false,
);
assert_eq!(r.unwrap_err(), FitError::NonFinite);
}
#[test]
fn estimate_seeds_are_close() {
let xs = linspace(0.0, 20.0, 201);
let ys = synth_gaussian(&xs, 5.0, 8.0, 2.0, 0.5);
let (h, c, f, bg) = estimate_height_position_fwhm(&xs, &ys).unwrap();
assert!((h - 5.0).abs() < 0.5, "height seed {}", h);
assert!((c - 8.0).abs() < 0.2, "center seed {}", c);
assert!((f - 2.0).abs() < 0.5, "fwhm seed {}", f);
assert!((bg - 0.5).abs() < 0.1, "bg seed {}", bg);
}
fn line(x: &[f64], p: &[f64]) -> Vec<f64> {
x.iter().map(|&xi| p[0] * xi + p[1]).collect()
}
fn constant(x: &[f64], p: &[f64]) -> Vec<f64> {
vec![p[0]; x.len()]
}
#[test]
fn constrained_all_free_matches_unconstrained() {
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
let free = leastsq_constrained(
line,
&xs,
&ys,
&[0.0, 0.0],
&[Constraint::Free, Constraint::Free],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
let plain = leastsq(
line,
&xs,
&ys,
&[0.0, 0.0],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!((free.parameters[0] - plain.parameters[0]).abs() < 1e-6);
assert!((free.parameters[1] - plain.parameters[1]).abs() < 1e-6);
assert!((free.parameters[0] - 2.5).abs() < 1e-6);
assert!((free.parameters[1] + 1.0).abs() < 1e-6);
}
#[test]
fn constrained_fixed_holds_parameter() {
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
let res = leastsq_constrained(
line,
&xs,
&ys,
&[0.0, -1.0],
&[Constraint::Free, Constraint::Fixed],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert_eq!(res.parameters[1], -1.0, "fixed b must not move");
assert!(
(res.parameters[0] - 2.5).abs() < 1e-6,
"a {}",
res.parameters[0]
);
}
#[test]
fn constrained_fixed_gets_full_uncertainty() {
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 2.5 * x - 1.0).collect();
let res = leastsq_constrained(
line,
&xs,
&ys,
&[0.0, -1.0],
&[Constraint::Free, Constraint::Fixed],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert_eq!(res.uncertainties[1], res.parameters[1]);
assert!((res.covariance[1][1] - res.parameters[1] * res.parameters[1]).abs() < 1e-12);
}
#[test]
fn constrained_positive_enforces_and_recovers() {
let xs = linspace(0.0, 10.0, 21);
let neg: Vec<f64> = vec![-3.0; xs.len()];
let r_neg = leastsq_constrained(
constant,
&xs,
&neg,
&[1.0],
&[Constraint::Positive],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
r_neg.parameters[0] >= 0.0,
"positive violated: {}",
r_neg.parameters[0]
);
assert!(
r_neg.chisq > 1.0,
"constraint should prevent fitting the negative target, chisq {}",
r_neg.chisq
);
let pos: Vec<f64> = vec![4.0; xs.len()];
let r_pos = leastsq_constrained(
constant,
&xs,
&pos,
&[1.0],
&[Constraint::Positive],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(r_pos.parameters[0] - 4.0).abs() < 0.1,
"recover {}",
r_pos.parameters[0]
);
}
#[test]
fn constrained_quoted_clamps_to_bounds() {
let xs = linspace(0.0, 10.0, 21);
let high: Vec<f64> = vec![10.0; xs.len()];
let r_hi = leastsq_constrained(
constant,
&xs,
&high,
&[2.5],
&[Constraint::Quoted { min: 0.0, max: 5.0 }],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(0.0..=5.0).contains(&r_hi.parameters[0]),
"out of bounds: {}",
r_hi.parameters[0]
);
assert!(
r_hi.parameters[0] > 4.5,
"did not saturate near 5: {}",
r_hi.parameters[0]
);
let mid: Vec<f64> = vec![3.0; xs.len()];
let r_mid = leastsq_constrained(
constant,
&xs,
&mid,
&[2.5],
&[Constraint::Quoted { min: 0.0, max: 5.0 }],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(r_mid.parameters[0] - 3.0).abs() < 0.05,
"recover {}",
r_mid.parameters[0]
);
}
#[test]
fn constrained_factor_ties_parameters() {
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 3.0 * x + 6.0).collect();
let res = leastsq_constrained(
line,
&xs,
&ys,
&[1.0, 0.0],
&[
Constraint::Free,
Constraint::Factor {
reference: 0,
factor: 2.0,
},
],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(res.parameters[0] - 3.0).abs() < 1e-4,
"a {}",
res.parameters[0]
);
assert!(
(res.parameters[1] - 6.0).abs() < 1e-4,
"b {}",
res.parameters[1]
);
assert!(
(res.parameters[1] - 2.0 * res.parameters[0]).abs() < 1e-9,
"tie broken"
);
}
#[test]
fn factor_tied_sigma_scales_by_the_factor() {
let sigmas = get_sigma_parameters(
&[3.0, 0.0, 0.0, 0.0],
&[0.5],
&[
Constraint::Free,
Constraint::Factor {
reference: 0,
factor: 2.0,
},
Constraint::Delta {
reference: 0,
delta: 5.0,
},
Constraint::Sum {
reference: 0,
sum: 9.0,
},
],
);
assert_eq!(sigmas[0], 0.5);
assert_eq!(sigmas[1], 1.0, "FACTOR sigma must be factor * reference");
assert_eq!(sigmas[2], 0.5, "DELTA sigma copies the reference unscaled");
assert_eq!(sigmas[3], 0.5, "SUM sigma copies the reference unscaled");
}
#[test]
fn constrained_delta_ties_parameters() {
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 2.0 * x + 7.0).collect();
let res = leastsq_constrained(
line,
&xs,
&ys,
&[0.0, 0.0],
&[
Constraint::Free,
Constraint::Delta {
reference: 0,
delta: 5.0,
},
],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(res.parameters[0] - 2.0).abs() < 1e-4,
"a {}",
res.parameters[0]
);
assert!(
(res.parameters[1] - res.parameters[0] - 5.0).abs() < 1e-9,
"tie broken"
);
}
#[test]
fn constrained_sum_ties_parameters() {
let xs = linspace(-5.0, 5.0, 21);
let ys: Vec<f64> = xs.iter().map(|&x| 4.0 * x + 6.0).collect();
let res = leastsq_constrained(
line,
&xs,
&ys,
&[0.0, 0.0],
&[
Constraint::Free,
Constraint::Sum {
reference: 0,
sum: 10.0,
},
],
None,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
false,
)
.unwrap();
assert!(
(res.parameters[0] - 4.0).abs() < 1e-4,
"a {}",
res.parameters[0]
);
assert!(
(res.parameters[0] + res.parameters[1] - 10.0).abs() < 1e-9,
"tie broken"
);
}
#[test]
fn constrained_rejects_bad_spec() {
let xs = linspace(0.0, 4.0, 5);
let ys = vec![1.0; 5];
assert_eq!(
leastsq_constrained(
constant,
&xs,
&ys,
&[1.0],
&[],
None,
10,
DEFAULT_DELTACHI,
false
)
.unwrap_err(),
FitError::BadConstraintReference
);
assert_eq!(
leastsq_constrained(
constant,
&xs,
&ys,
&[1.0],
&[Constraint::Quoted { min: 5.0, max: 5.0 }],
None,
10,
DEFAULT_DELTACHI,
false,
)
.unwrap_err(),
FitError::InvalidConstraint
);
assert_eq!(
leastsq_constrained(
line,
&xs,
&ys,
&[1.0, 0.0],
&[
Constraint::Free,
Constraint::Factor {
reference: 9,
factor: 2.0
}
],
None,
10,
DEFAULT_DELTACHI,
false,
)
.unwrap_err(),
FitError::BadConstraintReference
);
assert_eq!(
leastsq_constrained(
line,
&xs,
&ys,
&[1.0, 2.0],
&[Constraint::Fixed, Constraint::Fixed],
None,
10,
DEFAULT_DELTACHI,
false,
)
.unwrap_err(),
FitError::NoFreeParameters
);
}
fn grid(n: usize) -> Vec<f64> {
(0..n).map(|i| i as f64).collect()
}
fn nearest_peak(params: &[f64], target: f64) -> [f64; 3] {
params
.chunks_exact(3)
.min_by(|a, b| {
(a[1] - target)
.abs()
.partial_cmp(&(b[1] - target).abs())
.unwrap()
})
.map(|t| [t[0], t[1], t[2]])
.unwrap()
}
#[test]
fn multi_gaussian_model_is_sum_of_single_gaussians() {
let xs = grid(100);
let a = gaussian_model(&xs, &[100.0, 30.0, 8.0, 0.0]);
let b = gaussian_model(&xs, &[60.0, 70.0, 5.0, 0.0]);
let sum = multi_gaussian_model(&xs, &[100.0, 30.0, 8.0, 60.0, 70.0, 5.0]);
for i in 0..xs.len() {
assert!((sum[i] - (a[i] + b[i])).abs() < 1e-9, "mismatch at {i}");
}
}
#[test]
fn fit_multi_gaussian_recovers_two_peaks() {
let xs = grid(100);
let mut ys = gaussian_model(&xs, &[100.0, 30.0, 8.0, 0.0]);
for (yi, g) in ys
.iter_mut()
.zip(gaussian_model(&xs, &[80.0, 70.0, 6.0, 0.0]))
{
*yi += g;
}
let res = fit_multi_gaussian(
&xs,
&ys,
8.0,
DEFAULT_FIT_SENSITIVITY,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.expect("multi-peak fit should succeed");
assert!(res.parameters.len() >= 6, "expected >=2 peaks");
let p1 = nearest_peak(&res.parameters, 30.0);
let p2 = nearest_peak(&res.parameters, 70.0);
assert!((p1[1] - 30.0).abs() < 1.0, "centre1 {}", p1[1]);
assert!((p1[0] - 100.0).abs() < 5.0, "height1 {}", p1[0]);
assert!((p1[2] - 8.0).abs() < 1.0, "fwhm1 {}", p1[2]);
assert!((p2[1] - 70.0).abs() < 1.0, "centre2 {}", p2[1]);
assert!((p2[0] - 80.0).abs() < 5.0, "height2 {}", p2[0]);
assert!((p2[2] - 6.0).abs() < 1.0, "fwhm2 {}", p2[2]);
}
#[test]
fn fit_multi_gaussian_full_packages_names_errors_and_curve() {
let xs = grid(100);
let mut ys = gaussian_model(&xs, &[100.0, 30.0, 8.0, 0.0]);
for (yi, g) in ys
.iter_mut()
.zip(gaussian_model(&xs, &[80.0, 70.0, 6.0, 0.0]))
{
*yi += g;
}
let ir = fit_multi_gaussian_full(
&xs,
&ys,
8.0,
DEFAULT_FIT_SENSITIVITY,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.expect("multi-peak fit should succeed");
let n = ir.fit.parameters.len();
assert!(n >= 6 && n.is_multiple_of(3), "param count {n}");
assert_eq!(ir.fit.param_names.len(), n);
assert_eq!(ir.std_errors().len(), n);
assert_eq!(ir.fit.y_fit.len(), xs.len());
assert_eq!(ir.fit.param_names[0], "Height 1");
assert_eq!(ir.fit.param_names[1], "Center 1");
assert_eq!(ir.fit.param_names[2], "FWHM 1");
assert_eq!(ir.fit.y_fit, multi_gaussian_model(&xs, &ir.fit.parameters));
assert!((nearest_peak(&ir.fit.parameters, 30.0)[1] - 30.0).abs() < 1.0);
assert!((nearest_peak(&ir.fit.parameters, 70.0)[1] - 70.0).abs() < 1.0);
}
#[test]
fn fit_peak_constrained_all_free_recovers_peak() {
let xs = grid(100);
let ys = gaussian_model(&xs, &[60.0, 45.0, 7.0, 5.0]);
let cons = vec![Constraint::Free; 4];
let ir = fit_peak_constrained(
PeakModel::Gaussian,
&xs,
&ys,
&cons,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
let p = &ir.fit.parameters;
assert!((p[1] - 45.0).abs() < 1.0, "centre {}", p[1]);
assert!((p[2] - 7.0).abs() < 1.0, "fwhm {}", p[2]);
assert_eq!(ir.fit.param_names, PeakModel::Gaussian.param_names());
}
#[test]
fn fit_peak_constrained_fixed_holds_param_at_estimate() {
let xs = grid(100);
let ys = gaussian_model(&xs, &[60.0, 45.0, 7.0, 5.0]);
let p0 = PeakModel::Gaussian.estimate(&xs, &ys).unwrap();
let mut cons = vec![Constraint::Free; 4];
cons[1] = Constraint::Fixed; let ir = fit_peak_constrained(
PeakModel::Gaussian,
&xs,
&ys,
&cons,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
assert_eq!(ir.fit.parameters[1], p0[1]);
}
#[test]
fn fit_peak_from_uses_explicit_p0_and_recovers() {
let xs = grid(100);
let ys = gaussian_model(&xs, &[60.0, 45.0, 7.0, 5.0]);
let p0 = [20.0, 40.0, 12.0, 0.0];
let cons = vec![Constraint::Free; 4];
let ir = fit_peak_from(
PeakModel::Gaussian,
&xs,
&ys,
&p0,
&cons,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
let p = &ir.fit.parameters;
assert!((p[1] - 45.0).abs() < 1.0, "centre {}", p[1]);
assert!((p[2] - 7.0).abs() < 1.0, "fwhm {}", p[2]);
let est = PeakModel::Gaussian.estimate(&xs, &ys).unwrap();
let via_estimate = fit_peak_from(
PeakModel::Gaussian,
&xs,
&ys,
&est,
&cons,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
let direct = fit_peak_constrained(
PeakModel::Gaussian,
&xs,
&ys,
&cons,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
assert_eq!(via_estimate.fit.parameters, direct.fit.parameters);
}
#[test]
fn fit_peak_constrained_rejects_count_mismatch() {
let xs = grid(50);
let ys = gaussian_model(&xs, &[60.0, 25.0, 7.0, 0.0]);
assert!(
fit_peak_constrained(
PeakModel::Gaussian,
&xs,
&ys,
&[Constraint::Free; 3],
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.is_none()
);
}
#[test]
fn fit_multi_gaussian_single_peak() {
let xs = grid(100);
let ys = gaussian_model(&xs, &[50.0, 45.0, 7.0, 0.0]);
let res = fit_multi_gaussian(
&xs,
&ys,
7.0,
DEFAULT_FIT_SENSITIVITY,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
let p = nearest_peak(&res.parameters, 45.0);
assert!((p[0] - 50.0).abs() < 2.0, "height {}", p[0]);
assert!((p[1] - 45.0).abs() < 0.5, "centre {}", p[1]);
assert!((p[2] - 7.0).abs() < 0.5, "fwhm {}", p[2]);
}
#[test]
fn estimate_multi_gaussian_seeds_height_and_position() {
let xs = grid(100);
let ys = gaussian_model(&xs, &[50.0, 45.0, 7.0, 0.0]);
let (seeds, cons) =
estimate_multi_gaussian(&xs, &ys, 7.0, DEFAULT_FIT_SENSITIVITY).unwrap();
assert_eq!(seeds.len() % 3, 0);
assert_eq!(seeds.len(), cons.len());
assert_eq!(cons[0], Constraint::Positive);
assert_eq!(cons[1], Constraint::Free);
assert_eq!(cons[2], Constraint::Positive);
}
#[test]
fn estimate_multi_gaussian_rejects_empty_and_mismatch() {
assert!(estimate_multi_gaussian(&[], &[], 5.0, 2.5).is_none());
assert!(estimate_multi_gaussian(&[0.0, 1.0], &[1.0], 5.0, 2.5).is_none());
}
#[test]
fn estimate_multi_gaussian_seeds_an_edge_adjacent_peak() {
let xs = grid(100);
let ys: Vec<f64> = xs
.iter()
.map(|&x| {
gaussian_model(&[x], &[100.0, 40.0, 8.0, 0.0])[0]
+ gaussian_model(&[x], &[80.0, 99.0, 8.0, 0.0])[0]
})
.collect();
let (seeds, _) = estimate_multi_gaussian(&xs, &ys, 8.0, DEFAULT_FIT_SENSITIVITY).unwrap();
assert_eq!(seeds.len(), 6, "both peaks seeded, got {seeds:?}");
let centers: Vec<f64> = seeds.chunks(3).map(|c| c[1]).collect();
assert!(
centers.iter().any(|&c| (c - 40.0).abs() < 2.0),
"{centers:?}"
);
assert!(
centers.iter().any(|&c| (c - 99.0).abs() < 2.0),
"{centers:?}"
);
}
#[test]
fn fit_manager_sensitivity_seeds_marginal_peaks_the_pyx_default_misses() {
assert_eq!(DEFAULT_FIT_SENSITIVITY, 2.5);
let fwhm = 8.0;
let xs = grid(200);
let ys: Vec<f64> = xs
.iter()
.map(|&x| {
10000.0
+ gaussian_model(&[x], &[2000.0, 60.0, fwhm, 0.0])[0]
+ gaussian_model(&[x], &[100.0, 140.0, fwhm, 0.0])[0]
})
.collect();
let (seeds_fit, _) =
estimate_multi_gaussian(&xs, &ys, fwhm, DEFAULT_FIT_SENSITIVITY).unwrap();
assert_eq!(
seeds_fit.len(),
6,
"FitManager sensitivity must seed both peaks"
);
let (seeds_pyx, _) =
estimate_multi_gaussian(&xs, &ys, fwhm, crate::core::peaks::DEFAULT_PEAK_SENSITIVITY)
.unwrap();
assert_eq!(
seeds_pyx.len(),
3,
"the pyx default 3.5 drops the marginal peak"
);
}
#[test]
fn estimation_seeds_baseline_corrected_heights() {
let xs: Vec<f64> = (0..81).map(|i| i as f64 * 0.5).collect();
let ys: Vec<f64> = xs
.iter()
.map(|&x| 100.0 + gaussian_model(&[x], &[10.0, 20.0, 3.0, 0.0])[0])
.collect();
let (seeds, _) = estimate_multi_gaussian(&xs, &ys, 6.0, DEFAULT_FIT_SENSITIVITY).unwrap();
assert_eq!(seeds.len(), 3, "one peak, got {seeds:?}");
assert!(
seeds[0] > 5.0 && seeds[0] < 30.0,
"height must be baseline-corrected, got {}",
seeds[0]
);
assert!((seeds[1] - 20.0).abs() < 2.0, "centre {}", seeds[1]);
}
#[test]
fn forced_peak_is_picked_from_the_stripped_signal() {
let xs: Vec<f64> = (0..81).map(|i| i as f64 * 0.5).collect();
let ys: Vec<f64> = xs
.iter()
.map(|&x| 2.0 * x + gaussian_model(&[x], &[10.0, 20.0, 3.0, 0.0])[0])
.collect();
let (seeds, _) = estimate_multi_gaussian(&xs, &ys, 6.0, 1e9).unwrap();
assert_eq!(seeds.len(), 3, "forced single peak, got {seeds:?}");
assert!(
(seeds[1] - 20.0).abs() < 3.0,
"forced pick must come from y − bg (peak at 20), got centre {}",
seeds[1]
);
}
#[test]
fn erf_matches_known_values() {
assert_eq!(erf(0.0), 0.0);
assert!((erf(0.5) - 0.520_499_877_813_046_5).abs() < 1e-6);
assert!((erf(1.0) - 0.842_700_792_949_714_9).abs() < 1e-6);
assert!((erf(2.0) - 0.995_322_265_018_952_7).abs() < 1e-6);
assert!((erf(-1.0) + erf(1.0)).abs() < 1e-12);
}
#[test]
fn erfc_keeps_relative_precision_into_the_far_tail() {
for &(x, reference) in &[
(0.5, 0.479_500_122_186_953_5),
(2.0, 4.677_734_981_047_264_5e-3),
(5.0, 1.537_459_794_428_035e-12),
(6.0, 2.151_973_671_249_891_3e-17),
(10.0, 2.088_487_583_762_544_6e-45),
(15.0, 7.212_994_172_451_208e-100),
(26.0, 5.663_192_408_856_143e-296),
] {
let rel = (erfc(x) - reference) / reference;
assert!(rel.abs() < 1e-13, "erfc({x}) rel err {rel:e}");
}
assert_eq!(erfc(0.0), 1.0);
assert!((erfc(-2.0) - (2.0 - erfc(2.0))).abs() < 1e-15);
assert!((erfc(-10.0) - 2.0).abs() < 1e-12);
for &x in &[-2.0, -0.3, 0.0, 0.7, 1.5] {
assert!((erfc(x) - (1.0 - erf(x))).abs() < 1e-15);
}
}
#[test]
fn hypermet_tail_survives_the_large_erfc_argument_regime() {
let fwhm = 11.774_100_225_154_747; let params = [100.0, 50.0, fwhm, 1.0, 0.7, 0.0, 0.0, 0.0, 0.0];
let y = hypermet_model(&[55.0], ¶ms)[0];
let reference = 9.023_190_791_169_228;
assert!(
((y - reference) / reference).abs() < 1e-9,
"hypermet tail regime: {y} vs {reference}"
);
}
#[test]
fn convolve_valid_matches_numpy_reversed_kernel() {
assert_eq!(
convolve_valid(&[1.0, 2.0, 3.0, 4.0], &[1.0, 0.0, 0.0]),
vec![3.0, 4.0]
);
let out = convolve_valid(&[1.0, 2.0, 3.0, 4.0, 5.0], &[0.25, 0.75, 0.0, -0.75, -0.25]);
assert_eq!(out.len(), 1);
assert!((out[0] - 2.5).abs() < 1e-12);
assert!(convolve_valid(&[1.0], &[1.0, 2.0]).is_empty());
}
#[test]
fn stepup_model_half_height_at_center_and_asymptotes() {
let p = [10.0, 0.0, 4.0, 2.0]; assert_eq!(stepup_model(&[0.0], &p)[0], 7.0);
assert!((stepup_model(&[-1000.0], &p)[0] - 2.0).abs() < 1e-9);
assert!((stepup_model(&[1000.0], &p)[0] - 12.0).abs() < 1e-9);
let xs = linspace(-20.0, 20.0, 81);
let ys = stepup_model(&xs, &p);
assert!(ys.windows(2).all(|w| w[1] >= w[0]));
}
#[test]
fn stepdown_model_half_height_at_center_and_asymptotes() {
let p = [10.0, 0.0, 4.0, 2.0];
assert_eq!(stepdown_model(&[0.0], &p)[0], 7.0); assert!((stepdown_model(&[-1000.0], &p)[0] - 12.0).abs() < 1e-9);
assert!((stepdown_model(&[1000.0], &p)[0] - 2.0).abs() < 1e-9);
let xs = linspace(-20.0, 20.0, 81);
let ys = stepdown_model(&xs, &p);
assert!(ys.windows(2).all(|w| w[1] <= w[0]));
}
#[test]
fn atan_stepup_model_half_height_at_center_and_monotonic() {
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);
assert!((atan_stepup_model(&[1.0e8], &p)[0] - 12.0).abs() < 1e-6);
let xs = linspace(-50.0, 50.0, 101);
let ys = atan_stepup_model(&xs, &p);
assert!(ys.windows(2).all(|w| w[1] >= w[0]));
}
#[test]
fn slit_model_is_symmetric_and_localised() {
let p = [10.0, 0.0, 10.0, 2.0, 1.0]; for &d in &[1.0, 3.0, 7.0, 12.0] {
let a = slit_model(&[d], &p)[0];
let b = slit_model(&[-d], &p)[0];
assert!((a - b).abs() < 1e-12, "slit asymmetric at {d}: {a} vs {b}");
}
assert!(slit_model(&[0.0], &p)[0] > 5.0);
assert!((slit_model(&[1000.0], &p)[0] - 1.0).abs() < 1e-9);
assert!((slit_model(&[-1000.0], &p)[0] - 1.0).abs() < 1e-9);
}
#[test]
fn estimate_stepup_recovers_center_height_bg() {
let xs = linspace(0.0, 100.0, 101);
let ys = stepup_model(&xs, &[8.0, 40.0, 6.0, 3.0]);
let s = estimate_stepup(&xs, &ys).unwrap();
assert!((s[0] - 11.0).abs() < 0.5, "height seed {}", s[0]);
assert!((s[1] - 40.0).abs() < 3.0, "center seed {}", s[1]);
assert!((s[3] - 3.0).abs() < 0.5, "bg seed {}", s[3]);
}
#[test]
fn estimate_stepup_zero_baseline_height_is_the_amplitude() {
let xs = linspace(0.0, 100.0, 101);
let ys = stepup_model(&xs, &[8.0, 40.0, 6.0, 0.0]);
let s = estimate_stepup(&xs, &ys).unwrap();
assert!((s[0] - 8.0).abs() < 0.5, "height seed {}", s[0]);
}
#[test]
fn estimate_atan_stepup_inherits_the_stepup_height_rule() {
let xs = linspace(0.0, 100.0, 101);
let ys = stepup_model(&xs, &[8.0, 40.0, 6.0, 3.0]);
let s = estimate_atan_stepup(&xs, &ys).unwrap();
assert!((s[0] - 11.0).abs() < 0.5, "height seed {}", s[0]);
}
#[test]
fn estimate_stepdown_recovers_center_height_bg() {
let xs = linspace(0.0, 100.0, 101);
let ys = stepdown_model(&xs, &[8.0, 40.0, 6.0, 3.0]);
let s = estimate_stepdown(&xs, &ys).unwrap();
assert!((s[0] - 8.0).abs() < 0.5, "height seed {}", s[0]);
assert!((s[1] - 40.0).abs() < 3.0, "center seed {}", s[1]);
assert!((s[3] - 3.0).abs() < 0.5, "bg seed {}", s[3]);
}
#[test]
fn estimate_slit_centers_on_the_slit() {
let xs = linspace(0.0, 100.0, 201);
let ys = slit_model(&xs, &[10.0, 50.0, 20.0, 3.0, 2.0]);
let s = estimate_slit(&xs, &ys).unwrap();
assert_eq!(s.len(), 5);
assert!((s[1] - 50.0).abs() < 2.0, "position seed {}", s[1]);
assert!((s[3] - 2.0).abs() < 1e-9, "beamfwhm seed {}", s[3]);
}
#[test]
fn estimate_slit_beamfwhm_reproduces_the_silx_index_quirk() {
let xs = linspace(-100.0, 0.0, 201);
let ys = slit_model(&xs, &[10.0, -50.0, 20.0, 3.0, 2.0]);
let s = estimate_slit(&xs, &ys).unwrap();
let floor = 100.0 * 3.0 / 201.0;
assert!(
(s[3] - floor).abs() < 1e-9,
"beamfwhm {} must sit on the range*3/n floor {}",
s[3],
floor
);
}
#[test]
fn peak_model_step_variants_delegate() {
assert_eq!(PeakModel::StepUp.name(), "Step Up");
assert_eq!(PeakModel::StepDown.name(), "Step Down");
assert_eq!(PeakModel::Slit.name(), "Slit");
assert_eq!(PeakModel::AtanStepUp.name(), "Arctan Step Up");
assert_eq!(
PeakModel::StepUp.param_names(),
vec!["Height", "Center", "FWHM", "Background"]
);
assert_eq!(PeakModel::Slit.param_names().len(), 5);
assert_eq!(
PeakModel::AtanStepUp.param_names(),
vec!["Height", "Center", "Width", "Background"]
);
let xs = linspace(-5.0, 5.0, 11);
let p = [3.0, 0.0, 2.0, 1.0];
assert_eq!(PeakModel::StepUp.eval(&xs, &p), stepup_model(&xs, &p));
assert_eq!(PeakModel::StepDown.eval(&xs, &p), stepdown_model(&xs, &p));
assert_eq!(
PeakModel::AtanStepUp.eval(&xs, &p),
atan_stepup_model(&xs, &p)
);
}
#[test]
fn iterative_fit_recovers_stepup() {
let xs = linspace(0.0, 100.0, 101);
let truth = [8.0, 40.0, 6.0, 3.0];
let ys = stepup_model(&xs, &truth);
let fit = IterativeFit::new(PeakModel::StepUp)
.fit_full(&xs, &ys)
.unwrap();
let p = &fit.fit.parameters;
assert!((p[0] - truth[0]).abs() < 1.0, "height {}", p[0]);
assert!((p[1] - truth[1]).abs() < 1.0, "center {}", p[1]);
assert!((p[3] - truth[3]).abs() < 1.0, "bg {}", p[3]);
}
#[test]
fn fit_peak_with_background_none_is_byte_identical_to_plain_fit() {
use crate::core::background::Background;
let xs = grid(100);
let ys = gaussian_model(&xs, &[100.0, 50.0, 8.0, 0.0]);
let bgfit = fit_peak_with_background(
PeakModel::Gaussian,
Background::None,
&xs,
&ys,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
let plain = IterativeFit::new(PeakModel::Gaussian)
.fit_full(&xs, &ys)
.unwrap();
assert!(bgfit.background.iter().all(|&b| b == 0.0));
assert_eq!(bgfit.peak.fit.parameters, plain.fit.parameters);
assert_eq!(bgfit.total, bgfit.peak.fit.y_fit);
assert_eq!(bgfit.total, plain.fit.y_fit);
}
#[test]
fn fit_peak_with_background_constant_offset_recovers_peak() {
use crate::core::background::Background;
let xs = grid(100);
let ys = gaussian_model(&xs, &[100.0, 50.0, 8.0, 25.0]);
let bgfit = fit_peak_with_background(
PeakModel::Gaussian,
Background::Constant,
&xs,
&ys,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
assert!(
(bgfit.background[0] - 25.0).abs() < 1.0,
"background {}",
bgfit.background[0]
);
assert!(bgfit.background.iter().all(|&b| b == bgfit.background[0]));
let p = &bgfit.peak.fit.parameters;
assert!((p[1] - 50.0).abs() < 2.0, "centre {}", p[1]);
let max_err = ys
.iter()
.zip(&bgfit.total)
.map(|(&d, &t)| (d - t).abs())
.fold(0.0_f64, f64::max);
assert!(max_err < 5.0, "max |data - total| = {max_err}");
}
#[test]
fn fit_peak_with_background_linear_recovers_peak_on_slope() {
use crate::core::background::Background;
let xs = grid(120);
let base = line(&xs, &[0.5, 10.0]);
let peak = gaussian_model(&xs, &[80.0, 60.0, 8.0, 0.0]);
let ys: Vec<f64> = base.iter().zip(&peak).map(|(&b, &p)| b + p).collect();
let bgfit = fit_peak_with_background(
PeakModel::Gaussian,
Background::Linear,
&xs,
&ys,
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.unwrap();
assert!(
*bgfit.background.last().unwrap() > bgfit.background[0],
"background not rising: {} -> {}",
bgfit.background[0],
bgfit.background.last().unwrap()
);
let p = &bgfit.peak.fit.parameters;
assert!((p[1] - 60.0).abs() < 3.0, "centre {}", p[1]);
assert!((p[2] - 8.0).abs() < 4.0, "fwhm {}", p[2]);
let max_err = ys
.iter()
.zip(&bgfit.total)
.map(|(&d, &t)| (d - t).abs())
.fold(0.0_f64, f64::max);
assert!(max_err < 12.0, "max |data - total| = {max_err}");
}
#[test]
fn fit_peak_with_background_rejects_bad_input() {
use crate::core::background::Background;
let xs = grid(10);
let ys = constant(&xs, &[1.0]);
assert!(
fit_peak_with_background(
PeakModel::Gaussian,
Background::None,
&xs,
&ys[..5],
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.is_none()
);
assert!(
fit_peak_with_background(
PeakModel::Gaussian,
Background::None,
&[],
&[],
DEFAULT_MAX_ITER,
DEFAULT_DELTACHI,
)
.is_none()
);
}
}