use ndarray::Array1;
use super::running_trapezoid;
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
use crate::compute::traits::Fit;
#[derive(Debug, Clone)]
pub struct RunningIntegralResult {
pub integral: Array1<f64>,
}
impl ComputeResult for RunningIntegralResult {}
#[derive(Debug, Clone, Copy, Default)]
pub struct RunningIntegral;
impl Fit for RunningIntegral {
type Input<'a> = (&'a Array1<f64>, f64, Option<usize>);
type Output = RunningIntegralResult;
fn fit<'a>(&self, input: Self::Input<'a>) -> Result<Self::Output, ComputeError> {
let (y, dt, n_lags) = input;
if y.is_empty() {
return Err(ComputeError::EmptyInput);
}
if dt <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "dt",
value: dt.to_string(),
});
}
let take = match n_lags {
None => y.len(),
Some(0) => return Err(ComputeError::EmptyInput),
Some(m) if m > y.len() => {
return Err(ComputeError::OutOfRange {
field: "n_lags (exceeds curve length)",
value: format!("{m} > {}", y.len()),
});
}
Some(m) => m,
};
let ys = y.as_slice().ok_or(ComputeError::BadShape {
expected: "contiguous y".into(),
got: "non-contiguous".into(),
})?;
let integral = Array1::from_vec(running_trapezoid(&ys[..take], dt));
Ok(RunningIntegralResult { integral })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_curve_linear_integral() {
let c = 3.0;
let dt = 0.25;
let y = Array1::from_vec(vec![c; 8]);
let res = RunningIntegral.fit((&y, dt, None)).unwrap();
for k in 0..8 {
let expected = c * k as f64 * dt;
assert!(
(res.integral[k] - expected).abs() < 1e-12,
"k={k}: {} != {expected}",
res.integral[k]
);
}
}
#[test]
fn triangle_signal_analytic_integral() {
let y = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
let res = RunningIntegral.fit((&y, 1.0, None)).unwrap();
for k in 0..5 {
let expected = (k as f64) * (k as f64) / 2.0;
assert!((res.integral[k] - expected).abs() < 1e-12);
}
}
#[test]
fn matches_jacf_running_trapezoid() {
let jacf = Array1::from_vec(vec![1.0, 0.8, 0.5, 0.2, 0.05]);
let dt = 0.5;
let res = RunningIntegral.fit((&jacf, dt, None)).unwrap();
let mut expected = vec![0.0; jacf.len()];
let mut integral = 0.0;
for tau in 1..jacf.len() {
integral += 0.5 * (jacf[tau - 1] + jacf[tau]) * dt;
expected[tau] = integral;
}
for (k, (&got, &want)) in res.integral.iter().zip(expected.iter()).enumerate() {
assert!((got - want).abs() < 1e-12, "k={k}");
}
}
#[test]
fn n_lags_subset_integrates_only_prefix() {
let y = Array1::from_vec(vec![1.0; 10]);
let res = RunningIntegral.fit((&y, 1.0, Some(4))).unwrap();
assert_eq!(res.integral.len(), 4);
}
#[test]
fn n_lags_exceeding_length_errors() {
let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
assert!(matches!(
RunningIntegral.fit((&y, 1.0, Some(10))),
Err(ComputeError::OutOfRange { .. })
));
}
#[test]
fn empty_curve_errors() {
let y: Array1<f64> = Array1::from_vec(vec![]);
assert!(matches!(
RunningIntegral.fit((&y, 1.0, None)),
Err(ComputeError::EmptyInput)
));
}
#[test]
fn zero_lags_errors() {
let y = Array1::from_vec(vec![1.0, 2.0]);
assert!(matches!(
RunningIntegral.fit((&y, 1.0, Some(0))),
Err(ComputeError::EmptyInput)
));
}
#[test]
fn nonpositive_dt_errors() {
let y = Array1::from_vec(vec![1.0, 2.0]);
assert!(matches!(
RunningIntegral.fit((&y, 0.0, None)),
Err(ComputeError::OutOfRange { .. })
));
}
}