use crate::acov::autocovariance;
use crate::criteria::{ic_order, Criterion};
use crate::linalg::{singular_values, SquareMatrix};
use crate::{check_input, Error, Result};
#[derive(Debug, Clone)]
pub struct Cancorr {
pub correlations: Vec<f64>,
pub f: usize,
pub p: usize,
}
pub fn canonical_correlations(x: &[f64], f: usize, p: usize) -> Result<Cancorr> {
if f < 1 || p < 1 {
return Err(Error::Invalid("window sizes f and p must be >= 1"));
}
let r = autocovariance(x, f + p - 1)?;
canonical_correlations_from_acov(&r, f, p)
}
pub fn canonical_correlations_from_acov(r: &[f64], f: usize, p: usize) -> Result<Cancorr> {
if r.len() < f + p {
return Err(Error::TooShort {
n: r.len(),
needed: f + p,
});
}
let toeplitz = |m: usize| -> SquareMatrix {
let mut g = SquareMatrix::zeros(m);
for i in 0..m {
for j in 0..m {
g.set(i, j, r[i.abs_diff(j)]);
}
}
g
};
let lf = toeplitz(f).cholesky()?;
let lp = toeplitz(p).cholesky()?;
let mut gfp = vec![0.0; f * p];
for i in 0..f {
for j in 0..p {
gfp[i * p + j] = r[i + j + 1];
}
}
let mut xm = vec![0.0; f * p];
for j in 0..p {
for i in 0..f {
let mut s = gfp[i * p + j];
for k in 0..i {
s -= lf.get(i, k) * xm[k * p + j];
}
xm[i * p + j] = s / lf.get(i, i);
}
}
let mut m = vec![0.0; f * p];
for i in 0..f {
for j in 0..p {
let mut s = xm[i * p + j];
for k in 0..j {
s -= lp.get(j, k) * m[i * p + k];
}
m[i * p + j] = s / lp.get(j, j);
}
}
let mut correlations = singular_values(&m, f, p);
correlations.truncate(f.min(p));
for c in correlations.iter_mut() {
if *c > 1.0 && *c < 1.0 + 1e-10 {
*c = 1.0;
}
}
Ok(Cancorr { correlations, f, p })
}
pub fn canonical_correlations_stacked(x: &[f64], f: usize, p: usize) -> Result<Cancorr> {
if f < 1 || p < 1 {
return Err(Error::Invalid("window sizes f and p must be >= 1"));
}
check_input(x, 2 * (f + p))?;
let t_len = x.len();
if t_len < f + p + f + p {
return Err(Error::TooShort {
n: t_len,
needed: 2 * (f + p),
});
}
let mean = x.iter().sum::<f64>() / t_len as f64;
let xc: Vec<f64> = x.iter().map(|v| v - mean).collect();
let mut gff = SquareMatrix::zeros(f);
let mut gpp = SquareMatrix::zeros(p);
let mut gfp = vec![0.0; f * p];
for t in p..t_len - f {
for i in 0..f {
let yi = xc[t + i];
for j in 0..=i {
let v = gff.get(i, j) + yi * xc[t + j];
gff.set(i, j, v);
gff.set(j, i, v);
}
for j in 0..p {
gfp[i * p + j] += yi * xc[t - 1 - j];
}
}
for i in 0..p {
let zi = xc[t - 1 - i];
for j in 0..=i {
let v = gpp.get(i, j) + zi * xc[t - 1 - j];
gpp.set(i, j, v);
gpp.set(j, i, v);
}
}
}
whitened_correlations(gff, gpp, gfp, f, p)
}
fn whitened_correlations(
gff: SquareMatrix,
gpp: SquareMatrix,
gfp: Vec<f64>,
f: usize,
p: usize,
) -> Result<Cancorr> {
let lf = gff.cholesky()?;
let lp = gpp.cholesky()?;
let mut xm = vec![0.0; f * p];
for j in 0..p {
for i in 0..f {
let mut s = gfp[i * p + j];
for k in 0..i {
s -= lf.get(i, k) * xm[k * p + j];
}
xm[i * p + j] = s / lf.get(i, i);
}
}
let mut m = vec![0.0; f * p];
for i in 0..f {
for j in 0..p {
let mut s = xm[i * p + j];
for k in 0..j {
s -= lp.get(j, k) * m[i * p + k];
}
m[i * p + j] = s / lp.get(j, j);
}
}
let mut correlations = singular_values(&m, f, p);
correlations.truncate(f.min(p));
for c in correlations.iter_mut() {
if *c > 1.0 && *c < 1.0 + 1e-10 {
*c = 1.0;
}
}
Ok(Cancorr { correlations, f, p })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SvcPenalty {
LogT,
FpLogT,
}
#[derive(Debug, Clone, Copy)]
pub struct SvcConfig {
pub window: Option<usize>,
pub window_factor: f64,
pub penalty: SvcPenalty,
}
impl Default for SvcConfig {
fn default() -> Self {
SvcConfig {
window: None,
window_factor: 1.0,
penalty: SvcPenalty::LogT,
}
}
}
#[derive(Debug, Clone)]
pub struct SvcResult {
pub order: usize,
pub correlations: Vec<f64>,
pub values: Vec<f64>,
pub f: usize,
pub p: usize,
}
pub fn svc_order(x: &[f64], config: SvcConfig) -> Result<SvcResult> {
let t_len = x.len();
let w = match config.window {
Some(w) => w.max(1),
None => {
let aic = ic_order(x, Criterion::Aic, None)?;
((config.window_factor * aic.order as f64).floor() as usize).max(2)
}
};
let cc = canonical_correlations_stacked(x, w, w)?;
let m = w;
let t = t_len as f64;
let c_t = match config.penalty {
SvcPenalty::LogT => t.ln(),
SvcPenalty::FpLogT => (w * w) as f64 * t.ln(),
};
let values: Vec<f64> = (0..=m)
.map(|k| {
let sig2 = if k < m {
cc.correlations[k].powi(2)
} else {
0.0
};
sig2 + c_t * (2 * k) as f64 / t
})
.collect();
let order = crate::criteria::argmin(&values);
Ok(SvcResult {
order,
correlations: cc.correlations,
values,
f: w,
p: w,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ar1_has_exactly_one_canonical_correlation_equal_to_phi() {
let phi: f64 = 0.8;
let r: Vec<f64> = (0..12).map(|k| phi.powi(k) / (1.0 - phi * phi)).collect();
let cc = canonical_correlations_from_acov(&r, 6, 6).unwrap();
assert!(
(cc.correlations[0] - phi).abs() < 1e-10,
"{:?}",
cc.correlations
);
for c in &cc.correlations[1..] {
assert!(c.abs() < 1e-8, "{:?}", cc.correlations);
}
}
#[test]
fn ma1_has_rank_one_past_future_covariance() {
let theta = 0.6f64;
let mut r = vec![0.0; 12];
r[0] = 1.0 + theta * theta;
r[1] = theta;
let cc = canonical_correlations_from_acov(&r, 6, 6).unwrap();
assert!(cc.correlations[0] > 0.3);
for c in &cc.correlations[1..] {
assert!(c.abs() < 1e-10, "{:?}", cc.correlations);
}
}
#[test]
fn white_noise_correlations_are_zero() {
let mut r = vec![0.0; 10];
r[0] = 2.5;
let cc = canonical_correlations_from_acov(&r, 5, 5).unwrap();
for c in &cc.correlations {
assert!(c.abs() < 1e-12);
}
}
}