use crate::acov::autocovariance;
use crate::dist::{imhof_sf, satterthwaite_sf};
use crate::linalg::{singular_values, SquareMatrix};
use crate::threshold::polylog_memory;
use crate::Result;
#[derive(Debug, Clone, Copy)]
pub struct HankelSvtConfig {
pub k: Option<usize>,
pub c: f64,
}
impl Default for HankelSvtConfig {
fn default() -> Self {
HankelSvtConfig { k: None, c: 1.0 }
}
}
#[derive(Debug, Clone)]
pub struct HankelSvtResult {
pub order: usize,
pub singular_values: Vec<f64>,
pub xi: f64,
pub k: usize,
}
pub fn hankel_svt_order(x: &[f64], config: HankelSvtConfig) -> Result<HankelSvtResult> {
let n = x.len();
let k = config.k.unwrap_or_else(|| polylog_memory(n, 2.0)).max(2);
let r = autocovariance(x, 2 * k)?;
let mut h = vec![0.0; k * k];
for i in 0..k {
for j in 0..k {
h[i * k + j] = r[i + j + 1];
}
}
let sv = singular_values(&h, k, k);
let s2 = 2.0 * r.iter().map(|g| g * g).sum::<f64>();
let xi = config.c * s2.sqrt() * ((k as f64) * (n as f64).ln() / n as f64).sqrt();
let order = sv.iter().take_while(|s| **s > xi).count();
Ok(HankelSvtResult {
order,
singular_values: sv,
xi,
k,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_ar1_hankel_has_rank_one_structure() {
let phi: f64 = 0.8;
let n = 10;
let r: Vec<f64> = (0..=2 * n)
.map(|h| phi.powi(h as i32) / (1.0 - phi * phi))
.collect();
let mut h = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
h[i * n + j] = r[i + j + 1];
}
}
let sv = singular_values(&h, n, n);
assert!(sv[0] > 1.0);
assert!(sv[1].abs() < 1e-10, "{sv:?}");
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RankAlpha {
Fixed(f64),
LogDecay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RankPvalue {
Imhof,
Satterthwaite,
}
#[derive(Debug, Clone, Copy)]
pub struct HankelRankConfig {
pub k: Option<usize>,
pub alpha: RankAlpha,
pub pvalue: RankPvalue,
}
impl Default for HankelRankConfig {
fn default() -> Self {
HankelRankConfig {
k: None,
alpha: RankAlpha::LogDecay,
pvalue: RankPvalue::Imhof,
}
}
}
#[derive(Debug, Clone)]
pub struct HankelRankResult {
pub order: usize,
pub pvalues: Vec<f64>,
pub alpha: f64,
pub k: usize,
}
pub fn hankel_rank_order(x: &[f64], config: HankelRankConfig) -> Result<HankelRankResult> {
let n = x.len();
let k = config
.k
.unwrap_or_else(|| polylog_memory(n, 1.0).max(3))
.max(2);
let lags = 2 * k - 1; let big = 2 * lags; let r = autocovariance(x, big.min(n / 3))?;
let gamma = |h: i64| -> f64 {
let a = h.unsigned_abs() as usize;
if a < r.len() {
r[a]
} else {
0.0
}
};
let mut h = vec![0.0; k * k];
for i in 0..k {
for j in 0..k {
h[i * k + j] = r[i + j + 1];
}
}
let u_max = (r.len() - 1) as i64;
let mut xi = SquareMatrix::zeros(lags);
for a in 1..=lags {
for b in a..=lags {
let (ga, gb) = (a as i64, b as i64);
let mut s = 0.0;
for u in -u_max..=u_max {
s += gamma(u) * gamma(u + gb - ga) + gamma(u + gb) * gamma(u - ga);
}
xi.set(a - 1, b - 1, s);
xi.set(b - 1, a - 1, s);
}
}
let lxi = match xi.cholesky() {
Ok(l) => l,
Err(_) => {
let ridge = 1e-8 * (1..=lags).map(|i| xi.get(i - 1, i - 1)).sum::<f64>() / lags as f64;
for i in 0..lags {
let v = xi.get(i, i) + ridge;
xi.set(i, i, v);
}
xi.cholesky()?
}
};
let (sv, u_vecs, v_vecs) = svd_small(&h, k);
let alpha = match config.alpha {
RankAlpha::Fixed(a) => a,
RankAlpha::LogDecay => 0.05 * 50f64.ln() / (n as f64).ln().max(1.0),
};
let mut pvalues = Vec::new();
let mut order = k; for rr in 0..k {
let crt: f64 = (rr..k).map(|i| sv[i] * sv[i]).sum::<f64>() * n as f64;
let m = k - rr;
let mut w = vec![0.0; lags * m * m];
for (ai, a) in (rr..k).enumerate() {
for (bi, b) in (rr..k).enumerate() {
let cidx = ai * m + bi;
for i in 0..k {
for j in 0..k {
let l = i + j + 1;
w[(l - 1) * m * m + cidx] += u_vecs[i * k + a] * v_vecs[j * k + b];
}
}
}
}
let mut lw = vec![0.0; lags * m * m];
for row in 0..lags {
for c in 0..m * m {
let mut s = 0.0;
for t in row..lags {
s += lxi.get(t, row) * w[t * m * m + c];
}
lw[row * m * m + c] = s;
}
}
let wsv = singular_values(&lw, lags, m * m);
let weights: Vec<f64> = wsv.iter().map(|s| s * s).filter(|v| *v > 1e-12).collect();
let p = match config.pvalue {
RankPvalue::Imhof => imhof_sf(crt, &weights),
RankPvalue::Satterthwaite => satterthwaite_sf(crt, &weights),
};
pvalues.push(p);
if p >= alpha {
order = rr;
break;
}
}
Ok(HankelRankResult {
order,
pvalues,
alpha,
k,
})
}
fn svd_small(h: &[f64], k: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let mut g = SquareMatrix::zeros(k);
for i in 0..k {
for j in 0..k {
let mut s = 0.0;
for t in 0..k {
s += h[t * k + i] * h[t * k + j];
}
g.set(i, j, s);
}
}
let mut a = g;
let mut v = SquareMatrix::zeros(k);
for i in 0..k {
v.set(i, i, 1.0);
}
for _ in 0..100 {
let mut off = 0.0;
for i in 0..k {
for j in i + 1..k {
off += a.get(i, j).abs();
}
}
if off < 1e-13 {
break;
}
for i in 0..k {
for j in i + 1..k {
let apq = a.get(i, j);
if apq.abs() < 1e-300 {
continue;
}
let (app, aqq) = (a.get(i, i), a.get(j, j));
let zeta = (aqq - app) / (2.0 * apq);
let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
let c = 1.0 / (1.0 + t * t).sqrt();
let s = c * t;
for l in 0..k {
let (ail, ajl) = (a.get(i, l), a.get(j, l));
a.set(i, l, c * ail - s * ajl);
a.set(j, l, s * ail + c * ajl);
}
for l in 0..k {
let (ali, alj) = (a.get(l, i), a.get(l, j));
a.set(l, i, c * ali - s * alj);
a.set(l, j, s * ali + c * alj);
}
for l in 0..k {
let (vli, vlj) = (v.get(l, i), v.get(l, j));
v.set(l, i, c * vli - s * vlj);
v.set(l, j, s * vli + c * vlj);
}
}
}
}
let mut idx: Vec<usize> = (0..k).collect();
idx.sort_by(|&p, &q| a.get(q, q).total_cmp(&a.get(p, p)));
let mut sv = Vec::with_capacity(k);
let mut vv = vec![0.0; k * k];
for (col, &p) in idx.iter().enumerate() {
sv.push(a.get(p, p).max(0.0).sqrt());
for i in 0..k {
vv[i * k + col] = v.get(i, p);
}
}
let mut uu = vec![0.0; k * k];
for col in 0..k {
if sv[col] > 1e-12 * sv[0].max(1e-300) {
for i in 0..k {
let mut s = 0.0;
for j in 0..k {
s += h[i * k + j] * vv[j * k + col];
}
uu[i * k + col] = s / sv[col];
}
} else {
for seed in 0..k {
for i in 0..k {
uu[i * k + col] = if i == seed { 1.0 } else { 0.0 };
}
for prev in 0..col {
let dot: f64 = (0..k).map(|i| uu[i * k + col] * uu[i * k + prev]).sum();
for i in 0..k {
uu[i * k + col] -= dot * uu[i * k + prev];
}
}
let norm: f64 = (0..k).map(|i| uu[i * k + col].powi(2)).sum::<f64>().sqrt();
if norm > 1e-8 {
for i in 0..k {
uu[i * k + col] /= norm;
}
break;
}
}
}
}
(sv, uu, vv)
}