use crate::acov::{acf, autocovariance, bartlett_se};
use crate::levinson::levinson;
use crate::linalg::ols;
use crate::threshold::{polylog_memory, Memory, Threshold};
use crate::{check_input, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Statistic {
Pacf,
LongArYw,
LongArOls,
Acf,
AcfBartlett,
InversePacf,
JirakBand,
PacfRobust,
AcfRobust,
}
impl Statistic {
pub const ALL: [Statistic; 9] = [
Statistic::Pacf,
Statistic::LongArYw,
Statistic::LongArOls,
Statistic::Acf,
Statistic::AcfBartlett,
Statistic::InversePacf,
Statistic::JirakBand,
Statistic::PacfRobust,
Statistic::AcfRobust,
];
pub fn name(self) -> &'static str {
match self {
Statistic::Pacf => "pacf",
Statistic::LongArYw => "long_ar_yw",
Statistic::LongArOls => "long_ar_ols",
Statistic::Acf => "acf",
Statistic::AcfBartlett => "acf_bartlett",
Statistic::InversePacf => "inverse_pacf",
Statistic::JirakBand => "jirak_band",
Statistic::PacfRobust => "pacf_robust",
Statistic::AcfRobust => "acf_robust",
}
}
pub fn default_memory(self, n: usize) -> usize {
match self {
Statistic::PacfRobust => polylog_memory(n, 1.0),
_ => polylog_memory(n, 2.0),
}
}
pub fn target(self) -> &'static str {
match self {
Statistic::Pacf
| Statistic::LongArYw
| Statistic::LongArOls
| Statistic::JirakBand
| Statistic::PacfRobust => "AR",
Statistic::Acf
| Statistic::AcfBartlett
| Statistic::InversePacf
| Statistic::AcfRobust => "MA",
}
}
pub fn compute(self, x: &[f64], p_n: usize) -> Result<(Vec<f64>, Vec<f64>)> {
let n = x.len();
let root_n_se = vec![1.0 / (n as f64).sqrt(); p_n];
match self {
Statistic::Pacf => {
let r = autocovariance(x, p_n)?;
let l = levinson(&r)?;
Ok((l.pacf, root_n_se))
}
Statistic::LongArYw => {
let r = autocovariance(x, p_n)?;
let l = levinson(&r)?;
Ok((l.phi_last, root_n_se))
}
Statistic::LongArOls => {
let mean = x.iter().sum::<f64>() / n as f64;
let xc: Vec<f64> = x.iter().map(|v| v - mean).collect();
let rows = n - p_n;
let mut design = vec![0.0; rows * p_n];
let mut y = vec![0.0; rows];
for t in 0..rows {
y[t] = xc[p_n + t];
for j in 0..p_n {
design[t * p_n + j] = xc[p_n + t - j - 1];
}
}
let (beta, _s2, se) = ols(&design, rows, p_n, &y)?;
Ok((beta, se))
}
Statistic::Acf => {
let r = acf(x, p_n)?;
Ok((r[1..].to_vec(), root_n_se))
}
Statistic::AcfBartlett => {
let r = acf(x, p_n)?;
let se = bartlett_se(&r, n);
Ok((r[1..].to_vec(), se[1..].to_vec()))
}
Statistic::InversePacf => {
let r = autocovariance(x, p_n)?;
let l = levinson(&r)?;
let vi = crate::acov::inverse_acov_from_ar(&l.phi_last);
let li = levinson(&vi)?;
Ok((li.pacf, root_n_se))
}
Statistic::JirakBand => {
let r = autocovariance(x, p_n)?;
let l = levinson(&r)?;
let mut g = crate::linalg::SquareMatrix::zeros(p_n);
for i in 0..p_n {
for j in 0..p_n {
g.set(i, j, r[i.abs_diff(j)]);
}
}
let ch = g.cholesky()?;
let diag = crate::linalg::SquareMatrix::cholesky_inverse_diag(&ch);
let sigma2 = l.sigma2[p_n];
let nf = n as f64;
let se = diag.iter().map(|d| (d * sigma2 / nf).sqrt()).collect();
Ok((l.phi_last, se))
}
Statistic::PacfRobust => {
let r = crate::robust::robust_autocorrelation(x, p_n)?;
let r = crate::robust::shrink_to_psd(&r)?;
let l = levinson(&r)?;
let se = vec![2.0 / (n as f64).sqrt(); p_n];
Ok((l.pacf, se))
}
Statistic::AcfRobust => {
let r = crate::robust::robust_autocorrelation(x, p_n)?;
let se: Vec<f64> = bartlett_se(&r, n)[1..].iter().map(|s| 1.15 * s).collect();
Ok((r[1..].to_vec(), se))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FhConfig {
pub statistic: Statistic,
pub threshold: Threshold,
pub memory: Memory,
}
impl Default for FhConfig {
fn default() -> Self {
FhConfig {
statistic: Statistic::Pacf,
threshold: Threshold::default(),
memory: Memory::Default,
}
}
}
#[derive(Debug, Clone)]
pub struct FhResult {
pub config: FhConfig,
pub order: usize,
pub p_n: usize,
pub tau: f64,
pub values: Vec<f64>,
pub studentized: Vec<f64>,
}
pub fn fh_order(x: &[f64], config: FhConfig) -> Result<FhResult> {
let n = x.len();
config.threshold.validate()?;
let p_n = config.memory.resolve(n, config.statistic.default_memory(n));
if p_n < 1 {
return Err(crate::Error::Invalid("memory p_n must be >= 1"));
}
check_input(x, 2 * p_n + 2)?;
let (values, se) = config.statistic.compute(x, p_n)?;
let tau = config.threshold.value(n, p_n);
if !tau.is_finite() || tau < 0.0 {
return Err(crate::Error::Invalid(
"threshold evaluated to a non-finite or negative value",
));
}
let studentized: Vec<f64> = values.iter().zip(&se).map(|(v, s)| v / s).collect();
let order = studentized
.iter()
.rposition(|t| t.abs() > tau)
.map(|i| i + 1)
.unwrap_or(0);
Ok(FhResult {
config,
order,
p_n,
tau,
values,
studentized,
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct JirakConfig {
pub alpha: f64,
pub memory: Memory,
}
impl Default for JirakConfig {
fn default() -> Self {
JirakConfig {
alpha: 0.05,
memory: Memory::Default,
}
}
}
#[derive(Debug, Clone)]
pub struct JirakResult {
pub config: JirakConfig,
pub order: usize,
pub p_n: usize,
pub tau: f64,
pub per_dimension: Vec<usize>,
}
pub fn jirak_order(x: &[f64], config: JirakConfig) -> Result<JirakResult> {
let n = x.len();
if !(config.alpha.is_finite() && config.alpha > 0.0 && config.alpha < 1.0) {
return Err(crate::Error::Invalid("alpha must be in (0, 1)"));
}
let p_n = config
.memory
.resolve(n, Statistic::JirakBand.default_memory(n));
if p_n < 1 {
return Err(crate::Error::Invalid("memory p_n must be >= 1"));
}
check_input(x, 2 * p_n + 2)?;
let r = autocovariance(x, p_n)?;
let tau = Threshold::Gumbel {
alpha: config.alpha,
}
.value(n, p_n);
let nf = n as f64;
let mut per_dimension = Vec::with_capacity(p_n);
for k in 1..=p_n {
let l = levinson(&r[..=k])?;
let mut g = crate::linalg::SquareMatrix::zeros(k);
for i in 0..k {
for j in 0..k {
g.set(i, j, r[i.abs_diff(j)]);
}
}
let ch = g.cholesky()?;
let diag = crate::linalg::SquareMatrix::cholesky_inverse_diag(&ch);
let sigma2 = l.sigma2[k];
let q_k = l
.phi_last
.iter()
.zip(&diag)
.rposition(|(phi, d)| phi.abs() / (d * sigma2 / nf).sqrt() > tau)
.map(|i| i + 1)
.unwrap_or(0);
per_dimension.push(q_k);
}
let order = per_dimension.iter().copied().max().unwrap_or(0);
Ok(JirakResult {
config,
order,
p_n,
tau,
per_dimension,
})
}
#[cfg(feature = "ensemble")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnsembleRule {
Majority,
Median,
Min,
Max,
}
#[cfg(feature = "ensemble")]
#[derive(Debug, Clone)]
pub struct EnsembleResult {
pub rule: EnsembleRule,
pub order: usize,
pub members: Vec<FhResult>,
}
#[cfg(feature = "ensemble")]
pub fn ensemble_order(
x: &[f64],
configs: &[FhConfig],
rule: EnsembleRule,
) -> Result<EnsembleResult> {
if configs.is_empty() {
return Err(crate::Error::Invalid("ensemble needs at least one config"));
}
let members: Vec<FhResult> = configs
.iter()
.map(|&c| fh_order(x, c))
.collect::<Result<_>>()?;
let mut orders: Vec<usize> = members.iter().map(|m| m.order).collect();
orders.sort_unstable();
let order = match rule {
EnsembleRule::Min => orders[0],
EnsembleRule::Max => orders[orders.len() - 1],
EnsembleRule::Median => orders[(orders.len() - 1) / 2],
EnsembleRule::Majority => {
let mut best = orders[0];
let mut best_count = 0;
let mut i = 0;
while i < orders.len() {
let mut j = i;
while j < orders.len() && orders[j] == orders[i] {
j += 1;
}
if j - i > best_count {
best_count = j - i;
best = orders[i];
}
i = j;
}
best
}
};
Ok(EnsembleResult {
rule,
order,
members,
})
}