use crate::levinson::{burg_from_series, levinson_from_series};
use crate::linalg::SquareMatrix;
use crate::{check_input, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Criterion {
Aic,
Aicc,
Bic,
Hq,
Fpe,
Cat,
Mdl,
}
impl Criterion {
pub const ALL: [Criterion; 7] = [
Criterion::Aic,
Criterion::Aicc,
Criterion::Bic,
Criterion::Hq,
Criterion::Fpe,
Criterion::Cat,
Criterion::Mdl,
];
pub fn name(self) -> &'static str {
match self {
Criterion::Aic => "AIC",
Criterion::Aicc => "AICc",
Criterion::Bic => "BIC",
Criterion::Hq => "HQ",
Criterion::Fpe => "FPE",
Criterion::Cat => "CAT",
Criterion::Mdl => "MDL",
}
}
pub fn n_params(k: usize) -> usize {
k + 1
}
pub fn value(self, sigma2: &[f64], k: usize, n: usize) -> f64 {
let nf = n as f64;
let kf = k as f64;
let s2 = sigma2[k];
let ll = nf * s2.ln();
match self {
Criterion::Aic => ll + 2.0 * kf,
Criterion::Aicc => {
let kp = Self::n_params(k) as f64;
ll + 2.0 * kp + 2.0 * kp * (kp + 1.0) / (nf - kp - 1.0).max(1.0)
}
Criterion::Bic => ll + kf * nf.ln(),
Criterion::Hq => ll + 2.0 * kf * nf.ln().ln().max(0.0),
Criterion::Fpe => s2 * (nf + kf) / (nf - kf).max(1.0),
Criterion::Cat => {
if k == 0 {
return -(1.0 + 1.0 / nf);
}
let inv_bar = |j: usize| (nf - j as f64) * sigma2[0] / (nf * sigma2[j]);
(1..=k).map(inv_bar).sum::<f64>() / nf - inv_bar(k)
}
Criterion::Mdl => 0.5 * (ll + kf * nf.ln()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IcMethod {
YuleWalker,
Ols,
Burg,
}
impl IcMethod {
pub const ALL: [IcMethod; 3] = [IcMethod::YuleWalker, IcMethod::Ols, IcMethod::Burg];
pub fn name(self) -> &'static str {
match self {
IcMethod::YuleWalker => "yule_walker",
IcMethod::Ols => "ols",
IcMethod::Burg => "burg",
}
}
}
#[derive(Debug, Clone)]
pub struct IcResult {
pub criterion: Criterion,
pub method: IcMethod,
pub order: usize,
pub values: Vec<f64>,
pub sigma2: Vec<f64>,
}
pub fn default_max_order(n: usize) -> usize {
let m = (10.0 * (n as f64).log10()).floor() as usize;
m.min(n.saturating_sub(1)).max(1)
}
pub fn ic_order(x: &[f64], criterion: Criterion, max_order: Option<usize>) -> Result<IcResult> {
ic_order_with(x, criterion, IcMethod::YuleWalker, max_order)
}
pub fn ic_order_with(
x: &[f64],
criterion: Criterion,
method: IcMethod,
max_order: Option<usize>,
) -> Result<IcResult> {
let n = x.len();
let k_max = max_order.unwrap_or_else(|| default_max_order(n));
let sigma2 = match method {
IcMethod::YuleWalker => levinson_from_series(x, k_max)?.sigma2,
IcMethod::Burg => burg_from_series(x, k_max)?.sigma2,
IcMethod::Ols => ols_sigma2_path(x, k_max)?,
};
let values: Vec<f64> = (0..sigma2.len())
.map(|k| criterion.value(&sigma2, k, n))
.collect();
let order = argmin(&values);
Ok(IcResult {
criterion,
method,
order,
values,
sigma2,
})
}
fn ols_sigma2_path(x: &[f64], k_max: usize) -> Result<Vec<f64>> {
let n = x.len();
check_input(x, 2 * k_max + 2)?;
if k_max == 0 {
return Err(Error::Invalid("max_order must be >= 1"));
}
let mean = x.iter().sum::<f64>() / n as f64;
let xc: Vec<f64> = x.iter().map(|v| v - mean).collect();
let rows = n - k_max;
let mut gram = SquareMatrix::zeros(k_max);
let mut xty = vec![0.0; k_max];
let mut yy = 0.0;
for t in 0..rows {
let y = xc[k_max + t];
yy += y * y;
for i in 0..k_max {
let xi = xc[k_max + t - i - 1];
xty[i] += xi * y;
for j in 0..=i {
let v = gram.get(i, j) + xi * xc[k_max + t - j - 1];
gram.set(i, j, v);
gram.set(j, i, v);
}
}
}
if yy <= 0.0 {
return Err(Error::Singular);
}
let l = gram.cholesky()?;
let mut out = Vec::with_capacity(k_max + 1);
out.push(yy / rows as f64);
for k in 1..=k_max {
let beta = SquareMatrix::cholesky_solve_leading(&l, k, &xty);
let explained: f64 = beta.iter().zip(&xty).map(|(b, c)| b * c).sum();
let rss = (yy - explained).max(f64::EPSILON * yy);
out.push(rss / rows as f64);
}
Ok(out)
}
pub fn ic_order_all(x: &[f64], max_order: Option<usize>) -> Result<Vec<IcResult>> {
let n = x.len();
let k_max = max_order.unwrap_or_else(|| default_max_order(n));
let l = levinson_from_series(x, k_max)?;
Ok(Criterion::ALL
.iter()
.map(|&c| {
let values: Vec<f64> = (0..l.sigma2.len())
.map(|k| c.value(&l.sigma2, k, n))
.collect();
IcResult {
criterion: c,
method: IcMethod::YuleWalker,
order: argmin(&values),
values,
sigma2: l.sigma2.clone(),
}
})
.collect())
}
pub(crate) fn argmin(v: &[f64]) -> usize {
let mut best = 0;
for (i, &x) in v.iter().enumerate() {
if x < v[best] {
best = i;
}
}
best
}