use ndarray::Array1;
use super::ols_slope_intercept_r2;
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
use crate::compute::traits::Fit;
#[derive(Debug, Clone)]
pub struct DebyeFitResult {
pub tau: f64,
pub amplitude: f64,
pub n_samples: usize,
}
impl ComputeResult for DebyeFitResult {}
#[derive(Debug, Clone, Copy, Default)]
pub struct DebyeFit;
impl Fit for DebyeFit {
type Input<'a> = (&'a Array1<f64>, f64);
type Output = DebyeFitResult;
fn fit<'a>(&self, input: Self::Input<'a>) -> Result<Self::Output, ComputeError> {
let (phi, dt) = input;
if dt <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "dt",
value: dt.to_string(),
});
}
let mut t = Vec::new();
let mut log_phi = Vec::new();
for (k, &v) in phi.iter().enumerate() {
if v <= 0.0 {
break;
}
t.push(k as f64 * dt);
log_phi.push(v.ln());
}
let n_samples = t.len();
if n_samples < 2 {
return Err(ComputeError::EmptyInput);
}
let (slope, intercept, _r2) = ols_slope_intercept_r2(&t, &log_phi, 0, n_samples - 1)
.ok_or(ComputeError::OutOfRange {
field: "debye fit (degenerate time axis)",
value: format!("n_samples={n_samples}"),
})?;
if slope >= 0.0 {
return Err(ComputeError::OutOfRange {
field: "debye fit slope (require negative for decay)",
value: slope.to_string(),
});
}
Ok(DebyeFitResult {
tau: -1.0 / slope,
amplitude: intercept.exp(),
n_samples,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recovers_known_tau_and_amplitude() {
let tau = 5.0;
let dt = 0.5;
let phi = Array1::from_iter((0..40).map(|k| (-(k as f64) * dt / tau).exp()));
let res = DebyeFit.fit((&phi, dt)).unwrap();
assert!((res.tau - tau).abs() < 1e-9, "tau {}", res.tau);
assert!((res.amplitude - 1.0).abs() < 1e-9, "amp {}", res.amplitude);
}
#[test]
fn recovers_amplitude_below_one() {
let tau = 3.0;
let dt = 0.25;
let a = 0.8;
let phi = Array1::from_iter((0..60).map(|k| a * (-(k as f64) * dt / tau).exp()));
let res = DebyeFit.fit((&phi, dt)).unwrap();
assert!((res.tau - tau).abs() < 1e-9);
assert!((res.amplitude - a).abs() < 1e-9);
}
#[test]
fn stops_at_first_nonpositive_sample() {
let dt = 1.0;
let mut v: Vec<f64> = (0..5).map(|k| (-(k as f64) / 4.0).exp()).collect();
v.extend([-0.1, -0.2, 0.05]);
let phi = Array1::from_vec(v);
let res = DebyeFit.fit((&phi, dt)).unwrap();
assert_eq!(res.n_samples, 5);
}
#[test]
fn non_decaying_acf_errors() {
let phi = Array1::from_iter((0..10).map(|k| (k as f64 / 5.0).exp()));
assert!(matches!(
DebyeFit.fit((&phi, 1.0)),
Err(ComputeError::OutOfRange { .. })
));
}
#[test]
fn too_few_positive_samples_errors() {
let phi = Array1::from_vec(vec![1.0, -1.0, -2.0]);
assert!(matches!(
DebyeFit.fit((&phi, 1.0)),
Err(ComputeError::EmptyInput)
));
}
#[test]
fn nonpositive_dt_errors() {
let phi = Array1::from_vec(vec![1.0, 0.5, 0.25]);
assert!(matches!(
DebyeFit.fit((&phi, 0.0)),
Err(ComputeError::OutOfRange { .. })
));
}
}