use super::FacfResult;
use crate::error::FdarError;
use crate::helpers::{simpsons_weights, trapz, NUMERICAL_EPS};
use crate::matrix::FdMatrix;
use rand::rngs::StdRng;
use rand::SeedableRng;
fn validate_fts_input(data: &FdMatrix, argvals: &[f64]) -> Result<(usize, usize), FdarError> {
let (n, m) = data.shape();
if n == 0 || m == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "non-empty matrix".to_string(),
actual: format!("{n} rows, {m} columns"),
});
}
if argvals.len() != m {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("{m} elements (matching data columns)"),
actual: format!("{} elements", argvals.len()),
});
}
Ok((n, m))
}
fn mean_curve(data: &FdMatrix, n: usize, m: usize) -> Vec<f64> {
let mut xbar = vec![0.0f64; m];
let inv_n = 1.0 / n as f64;
for j in 0..m {
let mut s = 0.0;
for i in 0..n {
s += data[(i, j)];
}
xbar[j] = s * inv_n;
}
xbar
}
pub(crate) fn autocovariance_matrix(
data: &FdMatrix,
xbar: &[f64],
h: usize,
n: usize,
m: usize,
) -> Vec<f64> {
let mut c_h = vec![0.0f64; m * m];
let inv_n = 1.0 / n as f64;
for i in 0..(n - h) {
for j1 in 0..m {
let xi1 = data[(i, j1)] - xbar[j1];
for j2 in 0..m {
let xi2 = data[(i + h, j2)] - xbar[j2];
c_h[j1 + j2 * m] += xi1 * xi2;
}
}
}
for x in &mut c_h {
*x *= inv_n;
}
c_h
}
fn hs_norm_sq(c_h: &[f64], m: usize, weights: &[f64]) -> f64 {
let mut sum = 0.0f64;
for j1 in 0..m {
let w1 = weights[j1];
for j2 in 0..m {
let val = c_h[j1 + j2 * m];
sum += val * val * w1 * weights[j2];
}
}
sum
}
fn acf_normalization(c0: &[f64], m: usize, argvals: &[f64]) -> Result<f64, FdarError> {
let diag: Vec<f64> = (0..m).map(|j| c0[j + j * m]).collect();
let norm = trapz(&diag, argvals);
if norm.abs() < NUMERICAL_EPS {
return Err(FdarError::ComputationFailed {
operation: "functional_acf",
detail: "lag-0 covariance diagonal integrates to near zero (degenerate data)"
.to_string(),
});
}
Ok(norm)
}
fn mc_band_threshold(eigenvalues: &[f64], n: usize, n_sim: usize, ci: f64, seed: u64) -> f64 {
use rand_distr::{ChiSquared, Distribution};
let mut rng = StdRng::seed_from_u64(seed);
let chi2 = ChiSquared::new(1.0).expect("df=1 is always valid");
let mut realizations = Vec::with_capacity(n_sim);
for _ in 0..n_sim {
let mut q = 0.0f64;
for &lj in eigenvalues {
for &lk in eigenvalues {
q += lj * lk * chi2.sample(&mut rng);
}
}
realizations.push(q / n as f64);
}
realizations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((ci * n_sim as f64) as usize).min(n_sim - 1);
realizations[idx]
}
fn durbin_levinson_pacf(rho: &[f64]) -> Vec<f64> {
let p = rho.len();
if p == 0 {
return vec![];
}
let mut phi = vec![vec![0.0f64; p + 1]; p + 1];
let mut pacf = vec![0.0f64; p];
phi[1][1] = rho[0];
pacf[0] = rho[0];
for k in 2..=p {
let num = rho[k - 1] - (1..k).map(|j| phi[k - 1][j] * rho[k - 1 - j]).sum::<f64>();
let den = 1.0 - (1..k).map(|j| phi[k - 1][j] * rho[j - 1]).sum::<f64>();
if den.abs() < 1e-12 {
break;
}
phi[k][k] = num / den;
for j in 1..k {
phi[k][j] = phi[k - 1][j] - phi[k][k] * phi[k - 1][k - j];
}
pacf[k - 1] = phi[k][k];
}
pacf
}
#[must_use = "returns functional ACF result; result should be examined"]
pub fn functional_acf(
data: &FdMatrix,
argvals: &[f64],
max_lag: Option<usize>,
n_sim: usize,
ci: f64,
seed: u64,
) -> Result<FacfResult, FdarError> {
use nalgebra::DMatrix;
let (n, m) = validate_fts_input(data, argvals)?;
if n_sim == 0 {
return Err(FdarError::InvalidParameter {
parameter: "n_sim",
message: "must be >= 1".to_string(),
});
}
if !(ci > 0.0 && ci < 1.0) {
return Err(FdarError::InvalidParameter {
parameter: "ci",
message: "must be in the open interval (0.0, 1.0)".to_string(),
});
}
let ml = match max_lag {
Some(0) => {
return Err(FdarError::InvalidParameter {
parameter: "max_lag",
message: "must be >= 1".to_string(),
});
}
Some(v) => v,
None => 20usize.min(n / 4).max(1),
};
if ml + 1 > n {
return Err(FdarError::InvalidDimension {
parameter: "max_lag",
expected: format!("<= {}", n - 1),
actual: format!("{ml}"),
});
}
let weights = simpsons_weights(argvals);
let xbar = mean_curve(data, n, m);
let c0 = autocovariance_matrix(data, &xbar, 0, n, m);
let normalization = acf_normalization(&c0, m, argvals)?;
let mut lags = Vec::with_capacity(ml);
let mut acf_vals = Vec::with_capacity(ml);
for h in 1..=ml {
let c_h = autocovariance_matrix(data, &xbar, h, n, m);
let norm_sq = hs_norm_sq(&c_h, m, &weights);
let rho_h = norm_sq.sqrt() / normalization;
lags.push(h as u32);
acf_vals.push(rho_h);
}
let sqrt_w: Vec<f64> = weights.iter().map(|w| w.sqrt()).collect();
let mut c0_mat = DMatrix::from_fn(m, m, |j1, j2| c0[j1 + j2 * m] * sqrt_w[j1] * sqrt_w[j2]);
for j1 in 0..m {
for j2 in (j1 + 1)..m {
let avg = 0.5 * (c0_mat[(j1, j2)] + c0_mat[(j2, j1)]);
c0_mat[(j1, j2)] = avg;
c0_mat[(j2, j1)] = avg;
}
}
let eig = nalgebra::SymmetricEigen::new(c0_mat);
let mut eigenvalues: Vec<f64> = eig.eigenvalues.iter().copied().collect();
eigenvalues.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let lambda_max = eigenvalues.first().copied().unwrap_or(0.0);
let truncated: Vec<f64> = eigenvalues
.into_iter()
.filter(|&lj| lj > 0.0 && lambda_max > 0.0 && lj / lambda_max > 1e-4)
.collect();
let band = if truncated.is_empty() {
0.0
} else {
let q = mc_band_threshold(&truncated, n, n_sim, ci, seed);
q.sqrt() / normalization
};
let upper_band = vec![band; ml];
let pacf = durbin_levinson_pacf(&acf_vals);
Ok(FacfResult {
lags,
acf: acf_vals,
pacf,
upper_band,
})
}
#[must_use = "returns functional PACF result; result should be examined"]
pub fn functional_pacf(
data: &FdMatrix,
argvals: &[f64],
max_lag: Option<usize>,
n_sim: usize,
ci: f64,
seed: u64,
) -> Result<FacfResult, FdarError> {
functional_acf(data, argvals, max_lag, n_sim, ci, seed)
}
#[must_use = "returns first-difference curve series; result should be examined"]
pub fn functional_difference(data: &FdMatrix) -> Result<FdMatrix, FdarError> {
let (n, m) = data.shape();
if n < 2 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: ">= 2 rows".to_string(),
actual: format!("{n} rows"),
});
}
let mut out = FdMatrix::zeros(n - 1, m);
for i in 0..(n - 1) {
for j in 0..m {
out[(i, j)] = data[(i + 1, j)] - data[(i, j)];
}
}
Ok(out)
}
#[must_use = "returns stationarity test result; result should be examined"]
pub fn stationarity_test(
data: &FdMatrix,
argvals: &[f64],
n_perm: usize,
seed: u64,
) -> Result<super::StationarityResult, FdarError> {
let (n, m) = validate_fts_input(data, argvals)?;
if n_perm == 0 {
return Err(FdarError::InvalidParameter {
parameter: "n_perm",
message: "must be >= 1".to_string(),
});
}
let weights = simpsons_weights(argvals);
let xbar = mean_curve(data, n, m);
let mut centered = vec![0.0f64; n * m];
for i in 0..n {
for j in 0..m {
centered[i * m + j] = data[(i, j)] - xbar[j];
}
}
let stationarity_statistic = |row_order: &[usize]| -> f64 {
let mut partial_sum = vec![0.0f64; m];
let mut t = 0.0f64;
let inv_n2 = 1.0 / (n * n) as f64;
for k in 0..n {
let row = row_order[k];
for j in 0..m {
partial_sum[j] += centered[row * m + j];
}
let mut norm_sq = 0.0f64;
for j in 0..m {
norm_sq += partial_sum[j] * partial_sum[j] * weights[j];
}
t += norm_sq;
}
t * inv_n2
};
let natural_order: Vec<usize> = (0..n).collect();
let observed_t = stationarity_statistic(&natural_order);
use rand::Rng;
let mut rng = StdRng::seed_from_u64(seed);
let mut row_indices: Vec<usize> = (0..n).collect();
let mut n_ge = 0usize;
for _ in 0..n_perm {
for i in (1..n).rev() {
let j = rng.gen_range(0..=i);
row_indices.swap(i, j);
}
let perm_t = stationarity_statistic(&row_indices);
if perm_t >= observed_t {
n_ge += 1;
}
}
let p_value = (n_ge as f64 + 1.0) / (n_perm as f64 + 1.0);
Ok(super::StationarityResult {
statistic: observed_t,
p_value,
n_perm,
})
}
#[must_use = "returns long-run covariance result; result should be examined"]
pub fn long_run_covariance(
data: &FdMatrix,
argvals: &[f64],
bandwidth: Option<usize>,
) -> Result<super::LongRunCovResult, FdarError> {
let (n, m) = validate_fts_input(data, argvals)?;
let resolved_bandwidth = match bandwidth {
None => (n as f64).cbrt().floor() as usize,
Some(b) => b,
};
let xbar = mean_curve(data, n, m);
let c0 = autocovariance_matrix(data, &xbar, 0, n, m);
if resolved_bandwidth == 0 {
return Ok(super::LongRunCovResult {
cov_matrix: c0,
m,
bandwidth: 0,
n_curves: n,
});
}
let mut acc = c0;
let max_h = resolved_bandwidth.min(n - 1);
for h in 1..max_h {
let w_h = 1.0 - (h as f64) / (resolved_bandwidth as f64);
let c_h = autocovariance_matrix(data, &xbar, h, n, m);
for j2 in 0..m {
for j1 in 0..m {
let val = w_h * c_h[j1 + j2 * m];
acc[j1 + j2 * m] += val;
acc[j2 + j1 * m] += val;
}
}
}
Ok(super::LongRunCovResult {
cov_matrix: acc,
m,
bandwidth: resolved_bandwidth,
n_curves: n,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::covariance::{generate_gaussian_process, CovKernel};
use crate::test_helpers::uniform_grid;
fn make_whitenoise_curves(n: usize, m: usize, seed: u64) -> (FdMatrix, Vec<f64>) {
let argvals = uniform_grid(m);
let kernel = CovKernel::WhiteNoise { variance: 1.0 };
let gp = generate_gaussian_process(n, &kernel, &argvals, None, Some(seed)).unwrap();
(gp.samples, argvals)
}
fn make_ar1_curves(n: usize, m: usize, seed: u64) -> (FdMatrix, Vec<f64>) {
let argvals = uniform_grid(m);
let kernel = CovKernel::Gaussian {
length_scale: 0.3,
variance: 1.0,
};
let eps = generate_gaussian_process(n, &kernel, &argvals, None, Some(seed))
.unwrap()
.samples;
let mut data = FdMatrix::zeros(n, m);
for j in 0..m {
data[(0, j)] = eps[(0, j)];
}
for i in 1..n {
for j in 0..m {
data[(i, j)] = 0.8 * data[(i - 1, j)] + eps[(i, j)];
}
}
(data, argvals)
}
#[test]
fn facf_lags_start_at_one_and_finite() {
let (data, argvals) = make_whitenoise_curves(60, 20, 1);
let result = functional_acf(&data, &argvals, None, 200, 0.95, 42).unwrap();
assert!(!result.lags.is_empty(), "lags must be non-empty");
assert_eq!(result.lags[0], 1, "first lag must be 1");
assert_eq!(result.acf.len(), result.lags.len());
for &rho in &result.acf {
assert!(rho.is_finite(), "all fACF values must be finite");
assert!(rho >= 0.0, "all fACF values must be non-negative (L2 norm)");
}
}
#[test]
fn autocovariance_c0_is_symmetric() {
let (data, _argvals) = make_whitenoise_curves(40, 10, 2);
let (n, m) = data.shape();
let xbar = mean_curve(&data, n, m);
let c0 = autocovariance_matrix(&data, &xbar, 0, n, m);
for j1 in 0..m {
for j2 in 0..m {
let c_j1j2 = c0[j1 + j2 * m];
let c_j2j1 = c0[j2 + j1 * m];
assert!(
(c_j1j2 - c_j2j1).abs() < 1e-12,
"C_0[{j1},{j2}] = {c_j1j2} != C_0[{j2},{j1}] = {c_j2j1}"
);
}
}
}
#[test]
fn error_empty_data() {
let argvals = uniform_grid(20);
let empty = FdMatrix::zeros(0, 20);
assert!(matches!(
functional_acf(&empty, &argvals, None, 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
));
}
#[test]
fn error_argvals_mismatch() {
let (data, _) = make_whitenoise_curves(30, 20, 3);
let bad_argvals = uniform_grid(15); assert!(matches!(
functional_acf(&data, &bad_argvals, None, 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
));
}
#[test]
fn error_too_few_curves() {
let (data, argvals) = make_whitenoise_curves(5, 10, 4);
assert!(matches!(
functional_acf(&data, &argvals, Some(5), 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
));
}
#[test]
fn deterministic_seed() {
let (data, argvals) = make_whitenoise_curves(50, 20, 1);
let r1 = functional_acf(&data, &argvals, None, 200, 0.95, 42).unwrap();
let r2 = functional_acf(&data, &argvals, None, 200, 0.95, 42).unwrap();
assert_eq!(r1, r2, "same seed must give bit-identical FacfResult");
}
#[test]
fn error_handling() {
let m = 15usize;
let argvals = uniform_grid(m);
let bad_argvals = uniform_grid(m / 2); let empty = FdMatrix::zeros(0, m);
let one_row = FdMatrix::zeros(1, m);
let (good, _) = make_whitenoise_curves(30, m, 1);
assert!(
matches!(
functional_acf(&empty, &argvals, None, 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
),
"functional_acf: empty matrix must return InvalidDimension"
);
assert!(
matches!(
functional_acf(&good, &bad_argvals, None, 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
),
"functional_acf: argvals mismatch must return InvalidDimension"
);
assert!(
matches!(
functional_pacf(&empty, &argvals, None, 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
),
"functional_pacf: empty matrix must return InvalidDimension"
);
assert!(
matches!(
functional_pacf(&good, &bad_argvals, None, 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
),
"functional_pacf: argvals mismatch must return InvalidDimension"
);
assert!(
matches!(
stationarity_test(&empty, &argvals, 99, 1),
Err(FdarError::InvalidDimension { .. })
),
"stationarity_test: empty matrix must return InvalidDimension"
);
assert!(
matches!(
stationarity_test(&good, &bad_argvals, 99, 1),
Err(FdarError::InvalidDimension { .. })
),
"stationarity_test: argvals mismatch must return InvalidDimension"
);
assert!(
matches!(
long_run_covariance(&empty, &argvals, None),
Err(FdarError::InvalidDimension { .. })
),
"long_run_covariance: empty matrix must return InvalidDimension"
);
assert!(
matches!(
long_run_covariance(&good, &bad_argvals, None),
Err(FdarError::InvalidDimension { .. })
),
"long_run_covariance: argvals mismatch must return InvalidDimension"
);
assert!(
matches!(
functional_difference(&one_row),
Err(FdarError::InvalidDimension { .. })
),
"functional_difference: 1-row matrix must return InvalidDimension"
);
}
#[test]
fn too_few_curves() {
let (data, argvals) = make_whitenoise_curves(5, 10, 88);
assert!(
matches!(
functional_acf(&data, &argvals, Some(5), 99, 0.95, 1),
Err(FdarError::InvalidDimension { .. })
),
"max_lag >= n must return InvalidDimension"
);
}
#[test]
fn degenerate_columns() {
let n = 10usize;
let m = 8usize;
let argvals = uniform_grid(m);
let mut data = FdMatrix::zeros(n, m);
for i in 0..n {
for j in 0..m {
data[(i, j)] = argvals[j]; }
}
assert!(
matches!(
functional_acf(&data, &argvals, None, 99, 0.95, 1),
Err(FdarError::ComputationFailed { .. })
),
"constant-row matrix must return ComputationFailed (degenerate lag-0 diagonal)"
);
}
#[test]
fn deterministic_seed_all() {
let (data, argvals) = make_whitenoise_curves(50, 15, 5);
let acf1 = functional_acf(&data, &argvals, None, 200, 0.95, 42).unwrap();
let acf2 = functional_acf(&data, &argvals, None, 200, 0.95, 42).unwrap();
assert_eq!(
acf1, acf2,
"functional_acf: same seed must give bit-identical result"
);
let st1 = stationarity_test(&data, &argvals, 99, 123).unwrap();
let st2 = stationarity_test(&data, &argvals, 99, 123).unwrap();
assert_eq!(
st1, st2,
"stationarity_test: same seed must give bit-identical result"
);
}
#[test]
fn facf_whitenoise_inside_band() {
let (data, argvals) = make_whitenoise_curves(80, 20, 7);
let result = functional_acf(&data, &argvals, Some(10), 1000, 0.95, 99).unwrap();
assert_eq!(result.upper_band.len(), result.lags.len());
let mut all_inside = true;
for (h, (&rho, &band)) in result.acf.iter().zip(result.upper_band.iter()).enumerate() {
assert!(
band.is_finite() && band > 0.0,
"band must be positive at lag {}",
h + 1
);
if rho > band {
all_inside = false;
}
}
assert!(
all_inside,
"on i.i.d. white-noise all fACF lags should be inside the 95% band"
);
}
#[test]
fn facf_ar1_exceeds_band() {
let (data, argvals) = make_ar1_curves(120, 20, 13);
let result = functional_acf(&data, &argvals, Some(5), 1000, 0.95, 77).unwrap();
let lag1_acf = result.acf[0];
let band = result.upper_band[0];
assert!(
lag1_acf > band,
"lag-1 fACF ({lag1_acf:.4}) must exceed the 95% band ({band:.4}) for AR(1) data"
);
}
#[test]
fn dl_pacf_single_rho() {
let rho = [0.6];
let pacf = durbin_levinson_pacf(&rho);
assert_eq!(pacf.len(), 1);
assert!((pacf[0] - 0.6).abs() < 1e-12, "pacf[1] must equal rho[1]");
}
#[test]
fn fpacf_ar1_cutoff() {
let (data, argvals) = make_ar1_curves(120, 20, 17);
let result = functional_pacf(&data, &argvals, Some(5), 1000, 0.95, 55).unwrap();
assert_eq!(result.pacf.len(), result.lags.len());
let lag1_pacf = result.pacf[0].abs();
for (k, &v) in result.pacf.iter().enumerate().skip(1) {
assert!(
lag1_pacf > v.abs() * 1.5,
"AR(1) fPACF: lag-1 |pacf| ({lag1_pacf:.4}) should exceed lag-{} |pacf| ({:.4}) by 1.5x",
k + 1,
v.abs()
);
}
}
#[test]
fn fpacf_returns_populated_pacf() {
let (data, argvals) = make_ar1_curves(80, 20, 19);
let result = functional_pacf(&data, &argvals, Some(4), 500, 0.95, 33).unwrap();
assert_eq!(result.pacf.len(), result.lags.len());
assert!(
result.pacf.iter().any(|&v| v.abs() > 0.05),
"fPACF should have at least one nonzero entry on AR(1) data"
);
}
#[test]
fn diff_roundtrip() {
let m = 15usize;
let n = 8usize;
let argvals = uniform_grid(m);
let mut data = FdMatrix::zeros(n, m);
for i in 0..n {
for (j, &t) in argvals.iter().enumerate() {
data[(i, j)] = (i as f64 + t).sin();
}
}
let diff = functional_difference(&data).expect("functional_difference should succeed");
assert_eq!(diff.shape(), (n - 1, m), "output shape must be (N-1) x m");
let mut recon = FdMatrix::zeros(n, m);
for j in 0..m {
recon[(0, j)] = data[(0, j)];
}
for i in 1..n {
for j in 0..m {
recon[(i, j)] = recon[(i - 1, j)] + diff[(i - 1, j)];
}
}
for i in 0..n {
for j in 0..m {
let err = (recon[(i, j)] - data[(i, j)]).abs();
assert!(
err < 1e-10,
"round-trip error at ({i},{j}): {err} exceeds 1e-10"
);
}
}
}
#[test]
fn diff_too_few_rows() {
let m = 10usize;
let one_row = FdMatrix::zeros(1, m);
assert!(
matches!(
functional_difference(&one_row),
Err(FdarError::InvalidDimension {
parameter: "data",
..
})
),
"1-row matrix should return InvalidDimension"
);
let zero_row = FdMatrix::zeros(0, m);
assert!(
matches!(
functional_difference(&zero_row),
Err(FdarError::InvalidDimension {
parameter: "data",
..
})
),
"0-row matrix should return InvalidDimension"
);
}
#[test]
fn lrc_bandwidth_zero() {
let (data, argvals) = make_whitenoise_curves(40, 10, 55);
let (n, m) = data.shape();
let xbar = mean_curve(&data, n, m);
let c0 = autocovariance_matrix(&data, &xbar, 0, n, m);
let result = long_run_covariance(&data, &argvals, Some(0)).unwrap();
assert_eq!(result.bandwidth, 0, "bandwidth field must be 0");
assert_eq!(result.m, m, "m field must match data columns");
assert_eq!(result.n_curves, n, "n_curves must match data rows");
assert_eq!(result.cov_matrix.len(), m * m, "cov_matrix must be m×m");
for (idx, (&lrc_val, &c0_val)) in result.cov_matrix.iter().zip(c0.iter()).enumerate() {
assert!(
(lrc_val - c0_val).abs() < 1e-12,
"LRC at index {idx}: {lrc_val} != C_0 {c0_val} (bandwidth=0 must equal C_0)"
);
}
}
#[test]
fn lrc_symmetric() {
let (data, argvals) = make_ar1_curves(60, 10, 66);
let result = long_run_covariance(&data, &argvals, None).unwrap();
let m = result.m;
for j1 in 0..m {
for j2 in 0..m {
let upper = result.cov_matrix[j1 + j2 * m];
let lower = result.cov_matrix[j2 + j1 * m];
assert!(
(upper - lower).abs() < 1e-10,
"LRC[{j1},{j2}]={upper} != LRC[{j2},{j1}]={lower} (must be symmetric)"
);
}
}
}
#[test]
fn lrc_default_bandwidth() {
let n = 50usize;
let m = 10usize;
let (data, argvals) = make_whitenoise_curves(n, m, 77);
let result = long_run_covariance(&data, &argvals, None).unwrap();
let expected_bw = (n as f64).cbrt().floor() as usize;
assert_eq!(
result.bandwidth, expected_bw,
"default bandwidth must be ⌊N^{{1/3}}⌋ = {expected_bw}"
);
assert_eq!(result.m, m);
assert_eq!(result.n_curves, n);
assert_eq!(result.cov_matrix.len(), m * m);
for &v in &result.cov_matrix {
assert!(v.is_finite(), "all cov_matrix entries must be finite");
}
}
#[test]
fn stat_test_stationary() {
let (data, argvals) = make_whitenoise_curves(60, 20, 101);
let result = stationarity_test(&data, &argvals, 499, 42).unwrap();
assert!(
result.p_value > 0.05,
"stationary series should NOT be rejected at 0.05 (p = {:.4})",
result.p_value
);
assert!(result.statistic.is_finite(), "statistic must be finite");
assert_eq!(result.n_perm, 499);
}
#[test]
fn stat_test_nonstationary() {
let n = 50usize;
let m = 20usize;
let (gp_data, argvals) = make_whitenoise_curves(n, m, 202);
let mut data = FdMatrix::zeros(n, m);
for i in 0..n {
for (j, &t) in argvals.iter().enumerate() {
data[(i, j)] = (i as f64) * t + gp_data[(i, j)];
}
}
let result = stationarity_test(&data, &argvals, 499, 77).unwrap();
assert!(
result.p_value <= 0.05,
"trended series should be rejected at 0.05 (p = {:.4})",
result.p_value
);
}
#[test]
fn stat_test_deterministic() {
let (data, argvals) = make_whitenoise_curves(40, 15, 303);
let r1 = stationarity_test(&data, &argvals, 199, 123).unwrap();
let r2 = stationarity_test(&data, &argvals, 199, 123).unwrap();
assert_eq!(
r1, r2,
"same seed must give bit-identical StationarityResult"
);
}
#[test]
fn stat_test_invalid() {
let (data, argvals) = make_whitenoise_curves(30, 15, 1);
assert!(
matches!(
stationarity_test(&data, &argvals, 0, 1),
Err(FdarError::InvalidParameter {
parameter: "n_perm",
..
})
),
"n_perm == 0 must return InvalidParameter"
);
let empty = FdMatrix::zeros(0, 15);
assert!(
matches!(
stationarity_test(&empty, &argvals, 99, 1),
Err(FdarError::InvalidDimension { .. })
),
"empty matrix must return InvalidDimension"
);
let bad_argvals = uniform_grid(10);
assert!(
matches!(
stationarity_test(&data, &bad_argvals, 99, 1),
Err(FdarError::InvalidDimension {
parameter: "argvals",
..
})
),
"argvals mismatch must return InvalidDimension"
);
}
#[test]
fn invalid_parameter_guards() {
let (good, argvals) = make_whitenoise_curves(20, 10, 99);
assert!(
matches!(
functional_acf(&good, &argvals, None, 0, 0.95, 1),
Err(FdarError::InvalidParameter {
parameter: "n_sim",
..
})
),
"functional_acf: n_sim == 0 must return InvalidParameter"
);
assert!(
matches!(
functional_pacf(&good, &argvals, None, 0, 0.95, 1),
Err(FdarError::InvalidParameter {
parameter: "n_sim",
..
})
),
"functional_pacf: n_sim == 0 must return InvalidParameter"
);
assert!(
matches!(
functional_acf(&good, &argvals, None, 99, 1.5, 1),
Err(FdarError::InvalidParameter {
parameter: "ci",
..
})
),
"functional_acf: ci = 1.5 must return InvalidParameter"
);
assert!(
matches!(
functional_acf(&good, &argvals, None, 99, 0.0, 1),
Err(FdarError::InvalidParameter {
parameter: "ci",
..
})
),
"functional_acf: ci = 0.0 must return InvalidParameter"
);
assert!(
matches!(
functional_acf(&good, &argvals, None, 99, -0.1, 1),
Err(FdarError::InvalidParameter {
parameter: "ci",
..
})
),
"functional_acf: ci = -0.1 must return InvalidParameter"
);
assert!(
matches!(
functional_pacf(&good, &argvals, None, 99, 1.0, 1),
Err(FdarError::InvalidParameter {
parameter: "ci",
..
})
),
"functional_pacf: ci = 1.0 must return InvalidParameter"
);
}
}