use crate::error::{StatsError, StatsResult};
use scirs2_core::ndarray::{Array1, Array2, Axis};
#[derive(Clone, Debug)]
pub struct DynamicFactorModel {
pub loadings: Array2<f64>,
pub ar_matrices: Vec<Array2<f64>>,
pub factors: Array2<f64>,
pub r: Array2<f64>,
pub q: Array2<f64>,
pub n_factors: usize,
pub n_vars: usize,
pub n_lags: usize,
pub log_lik_history: Vec<f64>,
}
impl DynamicFactorModel {
pub fn extract_factors(&self) -> Array2<f64> {
self.factors.clone()
}
pub fn forecast_one_step(&self) -> StatsResult<Array1<f64>> {
let t = self.factors.nrows();
if t == 0 {
return Err(StatsError::InsufficientData(
"No factor estimates available for forecasting".into(),
));
}
let r = self.n_factors;
let p = self.n_lags;
let comp_dim = r * p;
let mut s_last = Array1::<f64>::zeros(comp_dim);
for lag in 0..p {
if t >= lag + 1 {
let f_row = self.factors.row(t - 1 - lag).to_owned();
for j in 0..r {
s_last[lag * r + j] = f_row[j];
}
}
}
let comp = build_companion(&self.ar_matrices, r, p)?;
let s_next = mat_vec_mul(&comp, &s_last)?;
let f_next = s_next.slice(scirs2_core::ndarray::s![0..r]).to_owned();
let x_hat = mat_vec_mul(&self.loadings, &f_next)?;
Ok(x_hat)
}
}
#[derive(Clone, Debug)]
pub struct DynamicFactor {
pub n_factors: usize,
pub n_vars: usize,
pub n_lags: usize,
}
impl DynamicFactor {
pub fn new(n_factors: usize, n_vars: usize, n_lags: usize) -> StatsResult<Self> {
if n_factors == 0 {
return Err(StatsError::InvalidInput(
"DynamicFactor: n_factors must be >= 1".into(),
));
}
if n_vars == 0 {
return Err(StatsError::InvalidInput(
"DynamicFactor: n_vars must be >= 1".into(),
));
}
if n_lags == 0 {
return Err(StatsError::InvalidInput(
"DynamicFactor: n_lags must be >= 1".into(),
));
}
if n_factors > n_vars {
return Err(StatsError::InvalidInput(
"DynamicFactor: n_factors cannot exceed n_vars".into(),
));
}
Ok(Self {
n_factors,
n_vars,
n_lags,
})
}
}
pub fn fit_dynamic_factor(
x: &Array2<f64>,
n_factors: usize,
n_lags: usize,
max_iter: usize,
tol: Option<f64>,
) -> StatsResult<DynamicFactorModel> {
let big_t = x.nrows();
let k = x.ncols();
let r = n_factors;
let p = n_lags;
let convergence_tol = tol.unwrap_or(1e-6);
if big_t <= p + 1 {
return Err(StatsError::InsufficientData(format!(
"Need > {} observations for n_lags={}, got {}",
p + 1,
p,
big_t
)));
}
if r == 0 || r > k {
return Err(StatsError::InvalidInput(format!(
"n_factors={} must be in 1..=n_vars={}",
r, k
)));
}
if max_iter == 0 {
return Err(StatsError::InvalidInput("max_iter must be >= 1".into()));
}
let x_clean = impute_column_means(x);
let (mut lambda, mut factors_init) = pca_init(&x_clean, r)?;
let mut r_mat = init_idiosyncratic_cov(&x_clean, &lambda, &factors_init, k)?;
let mut ar_mats = init_ar_matrices(&factors_init, r, p)?;
let mut q_mat = Array2::from_diag(&Array1::from_elem(r, 0.1_f64));
let mut log_lik_history: Vec<f64> = Vec::with_capacity(max_iter);
let mut prev_log_lik = f64::NEG_INFINITY;
let mut smoothed_factors = factors_init.clone();
for _iter in 0..max_iter {
let (new_factors, ptt, ptt1, log_lik) =
e_step(&x_clean, &lambda, &ar_mats, &r_mat, &q_mat, r, p)?;
log_lik_history.push(log_lik);
smoothed_factors = new_factors;
let (new_lambda, new_r) = m_step_loadings(&x_clean, &smoothed_factors, &ptt, k, r)?;
let (new_ar, new_q) = m_step_ar(&smoothed_factors, &ptt, &ptt1, r, p)?;
lambda = new_lambda;
r_mat = new_r;
ar_mats = new_ar;
q_mat = new_q;
let rel_change = if prev_log_lik.is_finite() && prev_log_lik != 0.0 {
(log_lik - prev_log_lik).abs() / prev_log_lik.abs().max(1.0)
} else {
f64::INFINITY
};
if rel_change < convergence_tol && _iter > 0 {
break;
}
prev_log_lik = log_lik;
}
Ok(DynamicFactorModel {
loadings: lambda,
ar_matrices: ar_mats,
factors: smoothed_factors,
r: r_mat,
q: q_mat,
n_factors: r,
n_vars: k,
n_lags: p,
log_lik_history,
})
}
fn e_step(
x: &Array2<f64>,
lambda: &Array2<f64>, ar_mats: &[Array2<f64>], r_mat: &Array2<f64>, q_mat: &Array2<f64>, r_dim: usize, p: usize, ) -> StatsResult<(
Array2<f64>, // smoothed factors (T, r)
Vec<Array2<f64>>, // P_{t|T}
Vec<Array2<f64>>, // P_{t,t-1|T}
f64, // log-likelihood
)> {
let big_t = x.nrows();
let k = x.ncols();
let comp_dim = r_dim * p;
let comp = build_companion(ar_mats, r_dim, p)?;
let comp_q = build_companion_q(q_mat, r_dim, p);
let mut z_obs = Array2::<f64>::zeros((k, comp_dim));
for i in 0..k {
for j in 0..r_dim {
z_obs[[i, j]] = lambda[[i, j]];
}
}
let p0 = Array2::from_diag(&Array1::from_elem(comp_dim, 1e4_f64));
let x0 = Array1::<f64>::zeros(comp_dim);
let mut filt_mean: Vec<Array1<f64>> = Vec::with_capacity(big_t);
let mut filt_cov: Vec<Array2<f64>> = Vec::with_capacity(big_t);
let mut pred_mean: Vec<Array1<f64>> = Vec::with_capacity(big_t);
let mut pred_cov: Vec<Array2<f64>> = Vec::with_capacity(big_t);
let log2pi = (2.0 * std::f64::consts::PI).ln();
let mut log_lik = 0.0_f64;
let mut x_cur = x0;
let mut p_cur = p0;
for t in 0..big_t {
let x_pred = mat_vec_mul(&comp, &x_cur)?;
let cp = mat_mat_mul(&comp, &p_cur)?;
let mut p_pred = mat_mat_mul_bt(&cp, &comp)? + &comp_q;
symmetrize(&mut p_pred);
regularise_pd(&mut p_pred, 1e-10);
pred_mean.push(x_pred.clone());
pred_cov.push(p_pred.clone());
let y_t = x.row(t).to_owned();
let z_xp = mat_vec_mul(&z_obs, &x_pred)?;
let innovation = &y_t - &z_xp;
let zp = mat_mat_mul(&z_obs, &p_pred)?;
let mut s = mat_mat_mul_bt(&zp, &z_obs)? + r_mat;
symmetrize(&mut s);
regularise_pd(&mut s, 1e-6);
{
let s_max_diag = (0..s.nrows())
.map(|ii| s[[ii, ii]].abs())
.fold(0.0_f64, f64::max);
let s_min_diag = (0..s.nrows())
.map(|ii| s[[ii, ii]].abs())
.fold(f64::INFINITY, f64::min);
if s_min_diag < 1e-10 * s_max_diag || s_min_diag < 1e-12 {
let eps = (1e-6 * s_max_diag).max(1e-8);
for ii in 0..s.nrows() {
s[[ii, ii]] += eps;
}
}
}
let s_inv = inv_symmetric(s.clone())?;
let log_det_s = log_det_posdef(&s)?;
let sv = mat_vec_mul(&s_inv, &innovation)?;
let quad: f64 = innovation.iter().zip(sv.iter()).map(|(&a, &b)| a * b).sum();
log_lik += -0.5 * (k as f64 * log2pi + log_det_s + quad);
let pzt = mat_mat_mul_bt(&p_pred, &z_obs)?;
let kgain = mat_mat_mul(&pzt, &s_inv)?;
let kv = mat_vec_mul(&kgain, &innovation)?;
let x_upd = &x_pred + &kv;
let kz = mat_mat_mul(&kgain, &z_obs)?;
let i_kz = eye_minus(kz)?;
let mut p_upd = mat_mat_mul(&i_kz, &p_pred)?;
symmetrize(&mut p_upd);
regularise_pd(&mut p_upd, 1e-8);
filt_mean.push(x_upd.clone());
filt_cov.push(p_upd.clone());
x_cur = x_upd;
p_cur = p_upd;
}
let mut smooth_mean: Vec<Array1<f64>> = filt_mean.clone();
let mut smooth_cov: Vec<Array2<f64>> = filt_cov.clone();
let mut cross_cov: Vec<Array2<f64>> = vec![Array2::zeros((comp_dim, comp_dim)); big_t];
for t in (0..big_t - 1).rev() {
let p_pred_tp1 = &filt_cov[t] * 0.0 + &pred_cov[t + 1]; let p_pred_tp1_inv = inv_symmetric(p_pred_tp1.clone())?;
let pct = mat_mat_mul_bt(&filt_cov[t], &comp)?;
let g = mat_mat_mul(&pct, &p_pred_tp1_inv)?;
let diff = &smooth_mean[t + 1] - &pred_mean[t + 1];
let g_diff = mat_vec_mul(&g, &diff)?;
let x_smooth = &filt_mean[t] + &g_diff;
let dp = &smooth_cov[t + 1] - &p_pred_tp1;
let g_dp = mat_mat_mul(&g, &dp)?;
let correction = mat_mat_mul_bt(&g_dp, &g)?;
let p_smooth = &filt_cov[t] + &correction;
let cross = mat_mat_mul_bt(&smooth_cov[t + 1], &g)?;
cross_cov[t + 1] = cross;
smooth_mean[t] = x_smooth;
smooth_cov[t] = p_smooth;
}
cross_cov[0] = Array2::zeros((comp_dim, comp_dim));
let mut smoothed_factors = Array2::<f64>::zeros((big_t, r_dim));
for t in 0..big_t {
for j in 0..r_dim {
smoothed_factors[[t, j]] = smooth_mean[t][j];
}
}
let p_tt: Vec<Array2<f64>> = smooth_cov
.iter()
.map(|p| Array2::from_shape_fn((r_dim, r_dim), |(i, j)| p[[i, j]]))
.collect();
let p_tt1: Vec<Array2<f64>> = cross_cov
.iter()
.skip(1)
.map(|cc| Array2::from_shape_fn((r_dim, r_dim), |(i, j)| cc[[i, j]]))
.collect();
Ok((smoothed_factors, p_tt, p_tt1, log_lik))
}
fn m_step_loadings(
x: &Array2<f64>,
factors: &Array2<f64>, p_tt: &[Array2<f64>], k: usize,
r: usize,
) -> StatsResult<(Array2<f64>, Array2<f64>)> {
let big_t = x.nrows();
let mut s_xf = Array2::<f64>::zeros((k, r));
let mut s_ff = Array2::<f64>::zeros((r, r));
for t in 0..big_t {
let x_t = x.row(t).to_owned();
let f_t = factors.row(t).to_owned();
for i in 0..k {
for j in 0..r {
s_xf[[i, j]] += x_t[i] * f_t[j];
}
}
for i in 0..r {
for j in 0..r {
s_ff[[i, j]] += f_t[i] * f_t[j] + p_tt[t][[i, j]];
}
}
}
let s_ff_inv = inv_symmetric(s_ff)?;
let lambda_new = mat_mat_mul(&s_xf, &s_ff_inv)?;
let mut r_diag = Array1::<f64>::zeros(k);
for t in 0..big_t {
let x_t = x.row(t).to_owned();
let f_t = factors.row(t).to_owned();
let lf = mat_vec_mul(&lambda_new, &f_t)?;
let resid = &x_t - &lf;
let lp = mat_mat_mul(&lambda_new, &p_tt[t])?;
let lpl = mat_mat_mul_bt(&lp, &lambda_new)?;
for i in 0..k {
r_diag[i] += resid[i] * resid[i] + lpl[[i, i]];
}
}
r_diag.mapv_inplace(|v| v / big_t as f64);
r_diag.mapv_inplace(|v| v.max(1e-6));
let r_new = Array2::from_diag(&r_diag);
Ok((lambda_new, r_new))
}
fn m_step_ar(
factors: &Array2<f64>, p_tt: &[Array2<f64>], p_tt1: &[Array2<f64>], r: usize,
p: usize,
) -> StatsResult<(Vec<Array2<f64>>, Array2<f64>)> {
let big_t = factors.nrows();
if big_t <= p {
return Err(StatsError::InsufficientData(
"m_step_ar: not enough observations for the given number of lags".into(),
));
}
let n = big_t - p;
let y_reg = factors.slice(scirs2_core::ndarray::s![p.., ..]).to_owned();
let mut x_reg = Array2::<f64>::zeros((n, r * p));
for t_idx in 0..n {
let t = t_idx + p;
for lag in 0..p {
let f_lag = factors.row(t - 1 - lag);
for j in 0..r {
x_reg[[t_idx, lag * r + j]] = f_lag[j];
}
}
}
let x_reg_t = x_reg.t().to_owned();
let xt_x = mat_mat_mul(&x_reg_t, &x_reg)?; let xt_y = mat_mat_mul(&x_reg_t, &y_reg)?;
let xt_x_inv = inv_symmetric_regularised(xt_x, 1e-8)?;
let b_hat = mat_mat_mul(&xt_x_inv, &xt_y)?;
let mut ar_mats: Vec<Array2<f64>> = Vec::with_capacity(p);
for lag in 0..p {
let a_t = b_hat
.slice(scirs2_core::ndarray::s![lag * r..(lag + 1) * r, ..])
.to_owned();
ar_mats.push(a_t.t().to_owned());
}
let mut q_new = Array2::<f64>::zeros((r, r));
for t_idx in 0..n {
let t = t_idx + p;
let f_t = factors.row(t).to_owned();
let reg_t = x_reg.row(t_idx).to_owned();
let f_pred: Array1<f64> = {
let reg_t_mat = reg_t
.view()
.into_shape_with_order((1, r * p))
.map_err(|_| StatsError::ComputationError("reshape failed".into()))?
.to_owned();
let pred_mat = mat_mat_mul(®_t_mat, &b_hat)?;
pred_mat.row(0).to_owned()
};
let resid = &f_t - &f_pred;
for i in 0..r {
for j in 0..r {
q_new[[i, j]] += resid[i] * resid[j];
}
}
if t < p_tt.len() {
for i in 0..r {
for j in 0..r {
q_new[[i, j]] += p_tt[t][[i, j]];
}
}
}
}
q_new.mapv_inplace(|v| v / n as f64);
symmetrize(&mut q_new);
regularise_pd(&mut q_new, 1e-8);
Ok((ar_mats, q_new))
}
fn pca_init(x: &Array2<f64>, r: usize) -> StatsResult<(Array2<f64>, Array2<f64>)> {
let big_t = x.nrows();
let k = x.ncols();
if r > k.min(big_t) {
return Err(StatsError::InvalidInput(format!(
"pca_init: r={} > min(T,K)={}",
r,
k.min(big_t)
)));
}
let col_means: Array1<f64> = x
.mean_axis(Axis(0))
.ok_or_else(|| StatsError::ComputationError("pca_init: mean_axis failed".into()))?;
let mut x_dm = x.clone();
for mut row in x_dm.rows_mut() {
for (v, &m) in row.iter_mut().zip(col_means.iter()) {
*v -= m;
}
}
let xt = x_dm.t().to_owned();
let cov = mat_mat_mul(&xt, &x_dm).map(|c| c.mapv(|v| v / big_t as f64))?;
let evecs = power_iter_top_r(&cov, r, 500)?;
let factors = mat_mat_mul(&x_dm, &evecs)?;
Ok((evecs, factors))
}
fn power_iter_top_r(a: &Array2<f64>, r: usize, n_iter: usize) -> StatsResult<Array2<f64>> {
let n = a.nrows();
let mut current = a.clone();
let mut evecs = Array2::<f64>::zeros((n, r));
for k in 0..r {
let mut v = Array1::<f64>::zeros(n);
v[k % n] = 1.0;
if k < n {
v[k] = 1.0;
} else {
v[0] = 1.0;
}
let norm = (v.iter().map(|x| x * x).sum::<f64>()).sqrt();
if norm > 1e-15 {
v.mapv_inplace(|x| x / norm);
}
for _ in 0..n_iter {
let av = mat_vec_mul(¤t, &v)?;
let new_norm = (av.iter().map(|x| x * x).sum::<f64>()).sqrt();
if new_norm < 1e-15 {
break;
}
v = av.mapv(|x| x / new_norm);
}
for i in 0..n {
evecs[[i, k]] = v[i];
}
let eigenvalue = {
let av = mat_vec_mul(¤t, &v)?;
av.iter().zip(v.iter()).map(|(&a, &b)| a * b).sum::<f64>()
};
for i in 0..n {
for j in 0..n {
current[[i, j]] -= eigenvalue * v[i] * v[j];
}
}
}
Ok(evecs)
}
fn init_idiosyncratic_cov(
x: &Array2<f64>,
lambda: &Array2<f64>,
factors: &Array2<f64>,
k: usize,
) -> StatsResult<Array2<f64>> {
let big_t = x.nrows();
let lf = mat_mat_mul(factors, &lambda.t().to_owned())?; let resid = x - &lf;
let mut r_diag = Array1::<f64>::zeros(k);
for t in 0..big_t {
let row = resid.row(t);
for i in 0..k {
r_diag[i] += row[i] * row[i];
}
}
r_diag.mapv_inplace(|v| (v / big_t as f64).max(1e-6));
Ok(Array2::from_diag(&r_diag))
}
fn init_ar_matrices(
factors: &Array2<f64>, r: usize,
p: usize,
) -> StatsResult<Vec<Array2<f64>>> {
let big_t = factors.nrows();
if big_t <= p {
return Ok(vec![Array2::zeros((r, r)); p]);
}
let n = big_t - p;
let y_reg = factors.slice(scirs2_core::ndarray::s![p.., ..]).to_owned();
let mut x_reg = Array2::<f64>::zeros((n, r * p));
for t_idx in 0..n {
let t = t_idx + p;
for lag in 0..p {
let f_lag = factors.row(t - 1 - lag);
for j in 0..r {
x_reg[[t_idx, lag * r + j]] = f_lag[j];
}
}
}
let x_reg_t = x_reg.t().to_owned();
let xt_x = mat_mat_mul(&x_reg_t, &x_reg)?; let xt_y = mat_mat_mul(&x_reg_t, &y_reg)?; let xt_x_inv = inv_symmetric_regularised(xt_x, 1e-8)?; let b_hat = mat_mat_mul(&xt_x_inv, &xt_y)?;
let mut ar_mats: Vec<Array2<f64>> = Vec::with_capacity(p);
for lag in 0..p {
let a_t = b_hat
.slice(scirs2_core::ndarray::s![lag * r..(lag + 1) * r, ..])
.to_owned();
ar_mats.push(a_t.t().to_owned());
}
Ok(ar_mats)
}
fn build_companion(ar_mats: &[Array2<f64>], r: usize, p: usize) -> StatsResult<Array2<f64>> {
let dim = r * p;
let mut comp = Array2::<f64>::zeros((dim, dim));
for (lag, a) in ar_mats.iter().enumerate() {
if a.nrows() != r || a.ncols() != r {
return Err(StatsError::DimensionMismatch(format!(
"AR matrix {} has shape {}×{}, expected {}×{}",
lag,
a.nrows(),
a.ncols(),
r,
r
)));
}
for i in 0..r {
for j in 0..r {
comp[[i, lag * r + j]] = a[[i, j]];
}
}
}
for i in 1..p {
for j in 0..r {
comp[[i * r + j, (i - 1) * r + j]] = 1.0;
}
}
Ok(comp)
}
fn build_companion_q(q: &Array2<f64>, r: usize, p: usize) -> Array2<f64> {
let dim = r * p;
let mut cq = Array2::<f64>::zeros((dim, dim));
for i in 0..r {
for j in 0..r {
cq[[i, j]] = q[[i, j]];
}
}
if p > 1 {
let q_trace = (0..r).map(|i| q[[i, i]]).sum::<f64>() / r as f64;
let eps = (1e-8 * q_trace).max(1e-12);
for i in r..dim {
cq[[i, i]] = eps;
}
}
cq
}
fn impute_column_means(x: &Array2<f64>) -> Array2<f64> {
let big_t = x.nrows();
let k = x.ncols();
let mut out = x.clone();
for j in 0..k {
let col = x.column(j);
let (sum, cnt) = col
.iter()
.filter(|v| v.is_finite())
.fold((0.0_f64, 0usize), |(s, n), &v| (s + v, n + 1));
if cnt == 0 {
continue;
}
let mean = sum / cnt as f64;
for i in 0..big_t {
if !out[[i, j]].is_finite() {
out[[i, j]] = mean;
}
}
}
out
}
fn mat_vec_mul(a: &Array2<f64>, x: &Array1<f64>) -> StatsResult<Array1<f64>> {
if a.ncols() != x.len() {
return Err(StatsError::DimensionMismatch(format!(
"mat_vec_mul: A is {}×{} but x has len {}",
a.nrows(),
a.ncols(),
x.len()
)));
}
let n = a.nrows();
let m = a.ncols();
let mut y = Array1::<f64>::zeros(n);
for i in 0..n {
let mut s = 0.0_f64;
for k in 0..m {
s += a[[i, k]] * x[k];
}
y[i] = s;
}
Ok(y)
}
fn mat_mat_mul(a: &Array2<f64>, b: &Array2<f64>) -> StatsResult<Array2<f64>> {
if a.ncols() != b.nrows() {
return Err(StatsError::DimensionMismatch(format!(
"mat_mat_mul: A is {}×{} but B is {}×{}",
a.nrows(),
a.ncols(),
b.nrows(),
b.ncols()
)));
}
let n = a.nrows();
let kk = a.ncols();
let m = b.ncols();
let mut c = Array2::<f64>::zeros((n, m));
for i in 0..n {
for j in 0..m {
let mut s = 0.0_f64;
for l in 0..kk {
s += a[[i, l]] * b[[l, j]];
}
c[[i, j]] = s;
}
}
Ok(c)
}
fn mat_mat_mul_bt(a: &Array2<f64>, b: &Array2<f64>) -> StatsResult<Array2<f64>> {
if a.ncols() != b.ncols() {
return Err(StatsError::DimensionMismatch(format!(
"mat_mat_mul_bt: A is {}×{} but B^T is {}×{}",
a.nrows(),
a.ncols(),
b.ncols(),
b.nrows()
)));
}
let n = a.nrows();
let kk = a.ncols();
let m = b.nrows();
let mut c = Array2::<f64>::zeros((n, m));
for i in 0..n {
for j in 0..m {
let mut s = 0.0_f64;
for l in 0..kk {
s += a[[i, l]] * b[[j, l]];
}
c[[i, j]] = s;
}
}
Ok(c)
}
fn eye_minus(a: Array2<f64>) -> StatsResult<Array2<f64>> {
let n = a.nrows();
if a.ncols() != n {
return Err(StatsError::DimensionMismatch(
"eye_minus: not square".into(),
));
}
let mut result = -a;
for i in 0..n {
result[[i, i]] += 1.0;
}
Ok(result)
}
fn cholesky_lower(a: Array2<f64>) -> StatsResult<Array2<f64>> {
let n = a.nrows();
if a.ncols() != n {
return Err(StatsError::DimensionMismatch(
"cholesky_lower: not square".into(),
));
}
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut s = a[[i, j]];
for kk in 0..j {
s -= l[[i, kk]] * l[[j, kk]];
}
if i == j {
if s <= 0.0 {
let eps = (1e-10_f64).max(s.abs() * 1e-8);
let s_reg = s + eps;
if s_reg <= 0.0 {
return Err(StatsError::ComputationError(format!(
"Cholesky failed at ({},{}): s={}",
i, j, s
)));
}
l[[i, j]] = s_reg.sqrt();
} else {
l[[i, j]] = s.sqrt();
}
} else {
let ljj = l[[j, j]];
if ljj.abs() < 1e-15 {
return Err(StatsError::ComputationError(
"Cholesky: near-zero diagonal".into(),
));
}
l[[i, j]] = s / ljj;
}
}
}
Ok(l)
}
fn lower_tri_inv(l: &Array2<f64>) -> StatsResult<Array2<f64>> {
let n = l.nrows();
let mut inv = Array2::<f64>::zeros((n, n));
for j in 0..n {
inv[[j, j]] = 1.0 / l[[j, j]];
for i in j + 1..n {
let mut s = 0.0_f64;
for kk in j..i {
s += l[[i, kk]] * inv[[kk, j]];
}
inv[[i, j]] = -s / l[[i, i]];
}
}
Ok(inv)
}
fn inv_symmetric(a: Array2<f64>) -> StatsResult<Array2<f64>> {
let n = a.nrows();
if a.ncols() != n {
return Err(StatsError::DimensionMismatch(
"inv_symmetric: not square".into(),
));
}
if n == 1 {
let val = a[[0, 0]];
if val.abs() < 1e-15 {
return Err(StatsError::ComputationError(
"inv_symmetric: singular 1×1".into(),
));
}
let mut inv = Array2::<f64>::zeros((1, 1));
inv[[0, 0]] = 1.0 / val;
return Ok(inv);
}
let max_diag = (0..n)
.map(|i| a[[i, i]].abs())
.fold(0.0_f64, f64::max)
.max(1e-12);
let regularisation_levels = [0.0, 1e-10, 1e-8, 1e-6, 1e-4, 1e-2, 1e-1, 1.0, 10.0];
for &eps_scale in ®ularisation_levels {
let mut reg_a = a.clone();
let eps = eps_scale * max_diag;
if eps > 0.0 {
for i in 0..n {
reg_a[[i, i]] += eps;
}
}
match cholesky_lower(reg_a) {
Ok(l) => {
let l_inv = lower_tri_inv(&l)?;
let l_inv_t = l_inv.t().to_owned();
return mat_mat_mul(&l_inv_t, &l_inv);
}
Err(_) => continue,
}
}
gauss_jordan_inv(&a)
}
fn gauss_jordan_inv(a: &Array2<f64>) -> StatsResult<Array2<f64>> {
let n = a.nrows();
let mut aug = Array2::<f64>::zeros((n, 2 * n));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n + i]] = 1.0;
}
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[[col, col]].abs();
for row in (col + 1)..n {
let val = aug[[row, col]].abs();
if val > max_val {
max_val = val;
max_row = row;
}
}
if max_val < 1e-15 {
return Err(StatsError::ComputationError(
"gauss_jordan_inv: singular matrix".into(),
));
}
if max_row != col {
for j in 0..(2 * n) {
let tmp = aug[[col, j]];
aug[[col, j]] = aug[[max_row, j]];
aug[[max_row, j]] = tmp;
}
}
let pivot = aug[[col, col]];
for j in 0..(2 * n) {
aug[[col, j]] /= pivot;
}
for row in 0..n {
if row == col {
continue;
}
let factor = aug[[row, col]];
for j in 0..(2 * n) {
aug[[row, j]] -= factor * aug[[col, j]];
}
}
}
let mut inv = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
inv[[i, j]] = aug[[i, n + j]];
}
}
Ok(inv)
}
fn inv_symmetric_regularised(mut a: Array2<f64>, reg: f64) -> StatsResult<Array2<f64>> {
let n = a.nrows();
for i in 0..n {
a[[i, i]] += reg;
}
inv_symmetric(a)
}
fn log_det_posdef(a: &Array2<f64>) -> StatsResult<f64> {
let n = a.nrows();
let max_diag = (0..n)
.map(|i| a[[i, i]].abs())
.fold(0.0_f64, f64::max)
.max(1e-12);
let reg_levels = [0.0, 1e-10, 1e-8, 1e-6, 1e-4, 1e-2, 1e-1, 1.0, 10.0];
for &eps_scale in ®_levels {
let mut reg_a = a.clone();
let eps = eps_scale * max_diag;
if eps > 0.0 {
for i in 0..n {
reg_a[[i, i]] += eps;
}
}
match cholesky_lower(reg_a) {
Ok(l) => {
let log_det: f64 = (0..n).map(|i| 2.0 * l[[i, i]].ln()).sum();
return Ok(log_det);
}
Err(_) => continue,
}
}
Err(StatsError::ComputationError(
"log_det_posdef: Cholesky failed after regularisation".into(),
))
}
fn symmetrize(a: &mut Array2<f64>) {
let n = a.nrows();
for i in 0..n {
for j in i + 1..n {
let v = (a[[i, j]] + a[[j, i]]) * 0.5;
a[[i, j]] = v;
a[[j, i]] = v;
}
}
}
fn regularise_pd(a: &mut Array2<f64>, eps: f64) {
let n = a.nrows();
for i in 0..n {
a[[i, i]] = a[[i, i]].max(eps);
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn synthetic_data(t: usize, k: usize, r: usize) -> Array2<f64> {
let lambda: Array2<f64> =
Array2::from_shape_fn((k, r), |(i, j)| ((i + j + 1) as f64) * 0.5);
let mut factors = Array2::<f64>::zeros((t, r));
for t_idx in 1..t {
for j in 0..r {
let prev = factors[[t_idx - 1, j]];
factors[[t_idx, j]] = prev * 0.9 + (t_idx as f64 * 0.1 + j as f64).sin() * 0.5;
}
}
let mut x = Array2::<f64>::zeros((t, k));
for t_idx in 0..t {
for i in 0..k {
let mut val = 0.0_f64;
for j in 0..r {
val += factors[[t_idx, j]] * lambda[[i, j]];
}
val += (t_idx as f64 * 0.3 + i as f64).cos() * 0.1;
x[[t_idx, i]] = val;
}
}
x
}
#[test]
fn test_dfm_basic_fit() {
let data = synthetic_data(50, 4, 2);
let model = fit_dynamic_factor(&data, 2, 1, 30, None).expect("fit_dynamic_factor");
assert_eq!(model.n_factors, 2);
assert_eq!(model.n_vars, 4);
assert_eq!(model.n_lags, 1);
assert_eq!(model.factors.nrows(), 50);
assert_eq!(model.factors.ncols(), 2);
assert_eq!(model.loadings.nrows(), 4);
assert_eq!(model.loadings.ncols(), 2);
assert!(!model.log_lik_history.is_empty());
let last_ll = *model.log_lik_history.last().expect("log_lik");
assert!(last_ll.is_finite(), "log-likelihood must be finite");
}
#[test]
fn test_dfm_extract_factors() {
let data = synthetic_data(40, 5, 1);
let model = fit_dynamic_factor(&data, 1, 1, 20, None).expect("fit");
let factors = model.extract_factors();
assert_eq!(factors.nrows(), 40);
assert_eq!(factors.ncols(), 1);
}
#[test]
fn test_dfm_forecast_one_step() {
let data = synthetic_data(50, 4, 2);
let model = fit_dynamic_factor(&data, 2, 1, 20, None).expect("fit");
let forecast = model.forecast_one_step().expect("forecast");
assert_eq!(forecast.len(), 4);
assert!(forecast.iter().all(|v| v.is_finite()));
}
#[test]
fn test_dfm_with_two_lags() {
let data = synthetic_data(120, 4, 2);
let model = fit_dynamic_factor(&data, 2, 2, 20, Some(1e-4)).expect("fit p=2");
assert_eq!(model.ar_matrices.len(), 2);
assert_eq!(model.factors.nrows(), 120);
}
#[test]
fn test_dfm_invalid_inputs() {
let data = synthetic_data(30, 4, 2);
assert!(fit_dynamic_factor(&data, 5, 1, 10, None).is_err());
let tiny = synthetic_data(3, 4, 1);
assert!(fit_dynamic_factor(&tiny, 1, 3, 10, None).is_err());
}
#[test]
fn test_dynamic_factor_spec() {
let spec = DynamicFactor::new(2, 5, 1).expect("spec");
assert_eq!(spec.n_factors, 2);
assert_eq!(spec.n_vars, 5);
assert_eq!(spec.n_lags, 1);
assert!(DynamicFactor::new(6, 5, 1).is_err());
assert!(DynamicFactor::new(0, 5, 1).is_err());
}
#[test]
fn test_impute_column_means() {
let mut x = Array2::<f64>::zeros((4, 2));
x[[0, 0]] = 1.0;
x[[1, 0]] = f64::NAN;
x[[2, 0]] = 3.0;
x[[3, 0]] = 4.0;
let out = impute_column_means(&x);
assert!(out[[1, 0]].is_finite());
assert!((out[[1, 0]] - (1.0 + 3.0 + 4.0) / 3.0).abs() < 1e-10);
}
#[test]
fn test_pca_init_dimensions() {
let data = synthetic_data(30, 5, 2);
let (lambda, factors) = pca_init(&data, 2).expect("pca_init");
assert_eq!(lambda.nrows(), 5);
assert_eq!(lambda.ncols(), 2);
assert_eq!(factors.nrows(), 30);
assert_eq!(factors.ncols(), 2);
}
}