1use crate::error::{StatsError, StatsResult};
42use scirs2_core::ndarray::{Array1, Array2, Axis};
43
44#[derive(Clone, Debug)]
53pub struct DynamicFactorModel {
54 pub loadings: Array2<f64>,
56 pub ar_matrices: Vec<Array2<f64>>,
58 pub factors: Array2<f64>,
60 pub r: Array2<f64>,
62 pub q: Array2<f64>,
64 pub n_factors: usize,
66 pub n_vars: usize,
68 pub n_lags: usize,
70 pub log_lik_history: Vec<f64>,
72}
73
74impl DynamicFactorModel {
75 pub fn extract_factors(&self) -> Array2<f64> {
79 self.factors.clone()
80 }
81
82 pub fn forecast_one_step(&self) -> StatsResult<Array1<f64>> {
87 let t = self.factors.nrows();
88 if t == 0 {
89 return Err(StatsError::InsufficientData(
90 "No factor estimates available for forecasting".into(),
91 ));
92 }
93 let r = self.n_factors;
94 let p = self.n_lags;
95
96 let comp_dim = r * p;
98 let mut s_last = Array1::<f64>::zeros(comp_dim);
99 for lag in 0..p {
100 if t >= lag + 1 {
101 let f_row = self.factors.row(t - 1 - lag).to_owned();
102 for j in 0..r {
103 s_last[lag * r + j] = f_row[j];
104 }
105 }
106 }
107
108 let comp = build_companion(&self.ar_matrices, r, p)?;
110 let s_next = mat_vec_mul(&comp, &s_last)?;
111 let f_next = s_next.slice(scirs2_core::ndarray::s![0..r]).to_owned();
112
113 let x_hat = mat_vec_mul(&self.loadings, &f_next)?;
115 Ok(x_hat)
116 }
117}
118
119#[derive(Clone, Debug)]
121pub struct DynamicFactor {
122 pub n_factors: usize,
124 pub n_vars: usize,
126 pub n_lags: usize,
128}
129
130impl DynamicFactor {
131 pub fn new(n_factors: usize, n_vars: usize, n_lags: usize) -> StatsResult<Self> {
133 if n_factors == 0 {
134 return Err(StatsError::InvalidInput(
135 "DynamicFactor: n_factors must be >= 1".into(),
136 ));
137 }
138 if n_vars == 0 {
139 return Err(StatsError::InvalidInput(
140 "DynamicFactor: n_vars must be >= 1".into(),
141 ));
142 }
143 if n_lags == 0 {
144 return Err(StatsError::InvalidInput(
145 "DynamicFactor: n_lags must be >= 1".into(),
146 ));
147 }
148 if n_factors > n_vars {
149 return Err(StatsError::InvalidInput(
150 "DynamicFactor: n_factors cannot exceed n_vars".into(),
151 ));
152 }
153 Ok(Self {
154 n_factors,
155 n_vars,
156 n_lags,
157 })
158 }
159}
160
161pub fn fit_dynamic_factor(
184 x: &Array2<f64>,
185 n_factors: usize,
186 n_lags: usize,
187 max_iter: usize,
188 tol: Option<f64>,
189) -> StatsResult<DynamicFactorModel> {
190 let big_t = x.nrows();
191 let k = x.ncols();
192 let r = n_factors;
193 let p = n_lags;
194 let convergence_tol = tol.unwrap_or(1e-6);
195
196 if big_t <= p + 1 {
197 return Err(StatsError::InsufficientData(format!(
198 "Need > {} observations for n_lags={}, got {}",
199 p + 1,
200 p,
201 big_t
202 )));
203 }
204 if r == 0 || r > k {
205 return Err(StatsError::InvalidInput(format!(
206 "n_factors={} must be in 1..=n_vars={}",
207 r, k
208 )));
209 }
210 if max_iter == 0 {
211 return Err(StatsError::InvalidInput("max_iter must be >= 1".into()));
212 }
213
214 let x_clean = impute_column_means(x);
216
217 let (mut lambda, mut factors_init) = pca_init(&x_clean, r)?;
219 let mut r_mat = init_idiosyncratic_cov(&x_clean, &lambda, &factors_init, k)?;
221 let mut ar_mats = init_ar_matrices(&factors_init, r, p)?;
223 let mut q_mat = Array2::from_diag(&Array1::from_elem(r, 0.1_f64));
225
226 let mut log_lik_history: Vec<f64> = Vec::with_capacity(max_iter);
227 let mut prev_log_lik = f64::NEG_INFINITY;
228
229 let mut smoothed_factors = factors_init.clone();
230
231 for _iter in 0..max_iter {
232 let (new_factors, ptt, ptt1, log_lik) =
234 e_step(&x_clean, &lambda, &ar_mats, &r_mat, &q_mat, r, p)?;
235
236 log_lik_history.push(log_lik);
237 smoothed_factors = new_factors;
238
239 let (new_lambda, new_r) = m_step_loadings(&x_clean, &smoothed_factors, &ptt, k, r)?;
241 let (new_ar, new_q) = m_step_ar(&smoothed_factors, &ptt, &ptt1, r, p)?;
242
243 lambda = new_lambda;
244 r_mat = new_r;
245 ar_mats = new_ar;
246 q_mat = new_q;
247
248 let rel_change = if prev_log_lik.is_finite() && prev_log_lik != 0.0 {
250 (log_lik - prev_log_lik).abs() / prev_log_lik.abs().max(1.0)
251 } else {
252 f64::INFINITY
253 };
254 if rel_change < convergence_tol && _iter > 0 {
255 break;
256 }
257 prev_log_lik = log_lik;
258 }
259
260 Ok(DynamicFactorModel {
261 loadings: lambda,
262 ar_matrices: ar_mats,
263 factors: smoothed_factors,
264 r: r_mat,
265 q: q_mat,
266 n_factors: r,
267 n_vars: k,
268 n_lags: p,
269 log_lik_history,
270 })
271}
272
273fn e_step(
284 x: &Array2<f64>,
285 lambda: &Array2<f64>, ar_mats: &[Array2<f64>], r_mat: &Array2<f64>, q_mat: &Array2<f64>, r_dim: usize, p: usize, ) -> StatsResult<(
292 Array2<f64>, Vec<Array2<f64>>, Vec<Array2<f64>>, f64, )> {
297 let big_t = x.nrows();
298 let k = x.ncols();
299 let comp_dim = r_dim * p;
300
301 let comp = build_companion(ar_mats, r_dim, p)?;
303 let comp_q = build_companion_q(q_mat, r_dim, p);
304
305 let mut z_obs = Array2::<f64>::zeros((k, comp_dim));
307 for i in 0..k {
308 for j in 0..r_dim {
309 z_obs[[i, j]] = lambda[[i, j]];
310 }
311 }
312
313 let p0 = Array2::from_diag(&Array1::from_elem(comp_dim, 1e4_f64));
315 let x0 = Array1::<f64>::zeros(comp_dim);
316
317 let mut filt_mean: Vec<Array1<f64>> = Vec::with_capacity(big_t);
319 let mut filt_cov: Vec<Array2<f64>> = Vec::with_capacity(big_t);
320 let mut pred_mean: Vec<Array1<f64>> = Vec::with_capacity(big_t);
321 let mut pred_cov: Vec<Array2<f64>> = Vec::with_capacity(big_t);
322
323 let log2pi = (2.0 * std::f64::consts::PI).ln();
324 let mut log_lik = 0.0_f64;
325
326 let mut x_cur = x0;
327 let mut p_cur = p0;
328
329 for t in 0..big_t {
330 let x_pred = mat_vec_mul(&comp, &x_cur)?;
332 let cp = mat_mat_mul(&comp, &p_cur)?;
333 let mut p_pred = mat_mat_mul_bt(&cp, &comp)? + &comp_q;
334
335 symmetrize(&mut p_pred);
337 regularise_pd(&mut p_pred, 1e-10);
338
339 pred_mean.push(x_pred.clone());
341 pred_cov.push(p_pred.clone());
342
343 let y_t = x.row(t).to_owned();
345
346 let z_xp = mat_vec_mul(&z_obs, &x_pred)?;
348 let innovation = &y_t - &z_xp;
349
350 let zp = mat_mat_mul(&z_obs, &p_pred)?;
352 let mut s = mat_mat_mul_bt(&zp, &z_obs)? + r_mat;
353
354 symmetrize(&mut s);
356 regularise_pd(&mut s, 1e-6);
357
358 {
360 let s_max_diag = (0..s.nrows())
361 .map(|ii| s[[ii, ii]].abs())
362 .fold(0.0_f64, f64::max);
363 let s_min_diag = (0..s.nrows())
364 .map(|ii| s[[ii, ii]].abs())
365 .fold(f64::INFINITY, f64::min);
366 if s_min_diag < 1e-10 * s_max_diag || s_min_diag < 1e-12 {
367 let eps = (1e-6 * s_max_diag).max(1e-8);
368 for ii in 0..s.nrows() {
369 s[[ii, ii]] += eps;
370 }
371 }
372 }
373
374 let s_inv = inv_symmetric(s.clone())?;
376 let log_det_s = log_det_posdef(&s)?;
377 let sv = mat_vec_mul(&s_inv, &innovation)?;
378 let quad: f64 = innovation.iter().zip(sv.iter()).map(|(&a, &b)| a * b).sum();
379 log_lik += -0.5 * (k as f64 * log2pi + log_det_s + quad);
380
381 let pzt = mat_mat_mul_bt(&p_pred, &z_obs)?;
383 let kgain = mat_mat_mul(&pzt, &s_inv)?;
384
385 let kv = mat_vec_mul(&kgain, &innovation)?;
387 let x_upd = &x_pred + &kv;
388 let kz = mat_mat_mul(&kgain, &z_obs)?;
389 let i_kz = eye_minus(kz)?;
390 let mut p_upd = mat_mat_mul(&i_kz, &p_pred)?;
391
392 symmetrize(&mut p_upd);
394 regularise_pd(&mut p_upd, 1e-8);
395
396 filt_mean.push(x_upd.clone());
397 filt_cov.push(p_upd.clone());
398 x_cur = x_upd;
399 p_cur = p_upd;
400 }
401
402 let mut smooth_mean: Vec<Array1<f64>> = filt_mean.clone();
404 let mut smooth_cov: Vec<Array2<f64>> = filt_cov.clone();
405 let mut cross_cov: Vec<Array2<f64>> = vec![Array2::zeros((comp_dim, comp_dim)); big_t];
407
408 for t in (0..big_t - 1).rev() {
409 let p_pred_tp1 = &filt_cov[t] * 0.0 + &pred_cov[t + 1]; let p_pred_tp1_inv = inv_symmetric(p_pred_tp1.clone())?;
411
412 let pct = mat_mat_mul_bt(&filt_cov[t], &comp)?;
414 let g = mat_mat_mul(&pct, &p_pred_tp1_inv)?;
415
416 let diff = &smooth_mean[t + 1] - &pred_mean[t + 1];
418 let g_diff = mat_vec_mul(&g, &diff)?;
419 let x_smooth = &filt_mean[t] + &g_diff;
420
421 let dp = &smooth_cov[t + 1] - &p_pred_tp1;
423 let g_dp = mat_mat_mul(&g, &dp)?;
424 let correction = mat_mat_mul_bt(&g_dp, &g)?;
425 let p_smooth = &filt_cov[t] + &correction;
426
427 let cross = mat_mat_mul_bt(&smooth_cov[t + 1], &g)?;
431 cross_cov[t + 1] = cross;
432
433 smooth_mean[t] = x_smooth;
434 smooth_cov[t] = p_smooth;
435 }
436 cross_cov[0] = Array2::zeros((comp_dim, comp_dim));
438
439 let mut smoothed_factors = Array2::<f64>::zeros((big_t, r_dim));
441 for t in 0..big_t {
442 for j in 0..r_dim {
443 smoothed_factors[[t, j]] = smooth_mean[t][j];
444 }
445 }
446
447 let p_tt: Vec<Array2<f64>> = smooth_cov
449 .iter()
450 .map(|p| Array2::from_shape_fn((r_dim, r_dim), |(i, j)| p[[i, j]]))
451 .collect();
452
453 let p_tt1: Vec<Array2<f64>> = cross_cov
454 .iter()
455 .skip(1)
456 .map(|cc| Array2::from_shape_fn((r_dim, r_dim), |(i, j)| cc[[i, j]]))
457 .collect();
458
459 Ok((smoothed_factors, p_tt, p_tt1, log_lik))
460}
461
462fn m_step_loadings(
474 x: &Array2<f64>,
475 factors: &Array2<f64>, p_tt: &[Array2<f64>], k: usize,
478 r: usize,
479) -> StatsResult<(Array2<f64>, Array2<f64>)> {
480 let big_t = x.nrows();
481
482 let mut s_xf = Array2::<f64>::zeros((k, r));
484 let mut s_ff = Array2::<f64>::zeros((r, r));
486
487 for t in 0..big_t {
488 let x_t = x.row(t).to_owned();
489 let f_t = factors.row(t).to_owned();
490 for i in 0..k {
492 for j in 0..r {
493 s_xf[[i, j]] += x_t[i] * f_t[j];
494 }
495 }
496 for i in 0..r {
498 for j in 0..r {
499 s_ff[[i, j]] += f_t[i] * f_t[j] + p_tt[t][[i, j]];
500 }
501 }
502 }
503
504 let s_ff_inv = inv_symmetric(s_ff)?;
506 let lambda_new = mat_mat_mul(&s_xf, &s_ff_inv)?;
507
508 let mut r_diag = Array1::<f64>::zeros(k);
510 for t in 0..big_t {
511 let x_t = x.row(t).to_owned();
512 let f_t = factors.row(t).to_owned();
513 let lf = mat_vec_mul(&lambda_new, &f_t)?;
514 let resid = &x_t - &lf;
515 let lp = mat_mat_mul(&lambda_new, &p_tt[t])?;
517 let lpl = mat_mat_mul_bt(&lp, &lambda_new)?;
518 for i in 0..k {
519 r_diag[i] += resid[i] * resid[i] + lpl[[i, i]];
520 }
521 }
522 r_diag.mapv_inplace(|v| v / big_t as f64);
523 r_diag.mapv_inplace(|v| v.max(1e-6));
525
526 let r_new = Array2::from_diag(&r_diag);
527
528 Ok((lambda_new, r_new))
529}
530
531fn m_step_ar(
545 factors: &Array2<f64>, p_tt: &[Array2<f64>], p_tt1: &[Array2<f64>], r: usize,
549 p: usize,
550) -> StatsResult<(Vec<Array2<f64>>, Array2<f64>)> {
551 let big_t = factors.nrows();
552
553 if big_t <= p {
554 return Err(StatsError::InsufficientData(
555 "m_step_ar: not enough observations for the given number of lags".into(),
556 ));
557 }
558
559 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));
571 for t_idx in 0..n {
572 let t = t_idx + p;
573 for lag in 0..p {
574 let f_lag = factors.row(t - 1 - lag);
575 for j in 0..r {
576 x_reg[[t_idx, lag * r + j]] = f_lag[j];
577 }
578 }
579 }
580
581 let x_reg_t = x_reg.t().to_owned();
583 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)?;
587 let b_hat = mat_mat_mul(&xt_x_inv, &xt_y)?; let mut ar_mats: Vec<Array2<f64>> = Vec::with_capacity(p);
592 for lag in 0..p {
593 let a_t = b_hat
594 .slice(scirs2_core::ndarray::s![lag * r..(lag + 1) * r, ..])
595 .to_owned();
596 ar_mats.push(a_t.t().to_owned());
598 }
599
600 let mut q_new = Array2::<f64>::zeros((r, r));
603 for t_idx in 0..n {
604 let t = t_idx + p;
605 let f_t = factors.row(t).to_owned();
606 let reg_t = x_reg.row(t_idx).to_owned();
608 let f_pred: Array1<f64> = {
609 let reg_t_mat = reg_t
610 .view()
611 .into_shape_with_order((1, r * p))
612 .map_err(|_| StatsError::ComputationError("reshape failed".into()))?
613 .to_owned();
614 let pred_mat = mat_mat_mul(®_t_mat, &b_hat)?;
615 pred_mat.row(0).to_owned()
616 };
617 let resid = &f_t - &f_pred;
618 for i in 0..r {
619 for j in 0..r {
620 q_new[[i, j]] += resid[i] * resid[j];
621 }
622 }
623 if t < p_tt.len() {
625 for i in 0..r {
626 for j in 0..r {
627 q_new[[i, j]] += p_tt[t][[i, j]];
628 }
629 }
630 }
631 }
632 q_new.mapv_inplace(|v| v / n as f64);
633
634 symmetrize(&mut q_new);
636 regularise_pd(&mut q_new, 1e-8);
637
638 Ok((ar_mats, q_new))
639}
640
641fn pca_init(x: &Array2<f64>, r: usize) -> StatsResult<(Array2<f64>, Array2<f64>)> {
649 let big_t = x.nrows();
650 let k = x.ncols();
651 if r > k.min(big_t) {
652 return Err(StatsError::InvalidInput(format!(
653 "pca_init: r={} > min(T,K)={}",
654 r,
655 k.min(big_t)
656 )));
657 }
658
659 let col_means: Array1<f64> = x
661 .mean_axis(Axis(0))
662 .ok_or_else(|| StatsError::ComputationError("pca_init: mean_axis failed".into()))?;
663 let mut x_dm = x.clone();
664 for mut row in x_dm.rows_mut() {
665 for (v, &m) in row.iter_mut().zip(col_means.iter()) {
666 *v -= m;
667 }
668 }
669
670 let xt = x_dm.t().to_owned();
672 let cov = mat_mat_mul(&xt, &x_dm).map(|c| c.mapv(|v| v / big_t as f64))?;
673
674 let evecs = power_iter_top_r(&cov, r, 500)?; let factors = mat_mat_mul(&x_dm, &evecs)?; Ok((evecs, factors))
681}
682
683fn power_iter_top_r(a: &Array2<f64>, r: usize, n_iter: usize) -> StatsResult<Array2<f64>> {
687 let n = a.nrows();
688 let mut current = a.clone();
689 let mut evecs = Array2::<f64>::zeros((n, r));
690
691 for k in 0..r {
692 let mut v = Array1::<f64>::zeros(n);
694 v[k % n] = 1.0;
695 if k < n {
696 v[k] = 1.0;
697 } else {
698 v[0] = 1.0;
699 }
700 let norm = (v.iter().map(|x| x * x).sum::<f64>()).sqrt();
702 if norm > 1e-15 {
703 v.mapv_inplace(|x| x / norm);
704 }
705
706 for _ in 0..n_iter {
707 let av = mat_vec_mul(¤t, &v)?;
708 let new_norm = (av.iter().map(|x| x * x).sum::<f64>()).sqrt();
709 if new_norm < 1e-15 {
710 break;
711 }
712 v = av.mapv(|x| x / new_norm);
713 }
714
715 for i in 0..n {
717 evecs[[i, k]] = v[i];
718 }
719
720 let eigenvalue = {
722 let av = mat_vec_mul(¤t, &v)?;
723 av.iter().zip(v.iter()).map(|(&a, &b)| a * b).sum::<f64>()
724 };
725 for i in 0..n {
726 for j in 0..n {
727 current[[i, j]] -= eigenvalue * v[i] * v[j];
728 }
729 }
730 }
731
732 Ok(evecs)
733}
734
735fn init_idiosyncratic_cov(
737 x: &Array2<f64>,
738 lambda: &Array2<f64>,
739 factors: &Array2<f64>,
740 k: usize,
741) -> StatsResult<Array2<f64>> {
742 let big_t = x.nrows();
743 let lf = mat_mat_mul(factors, &lambda.t().to_owned())?; let resid = x - &lf;
745 let mut r_diag = Array1::<f64>::zeros(k);
746 for t in 0..big_t {
747 let row = resid.row(t);
748 for i in 0..k {
749 r_diag[i] += row[i] * row[i];
750 }
751 }
752 r_diag.mapv_inplace(|v| (v / big_t as f64).max(1e-6));
753 Ok(Array2::from_diag(&r_diag))
754}
755
756fn init_ar_matrices(
758 factors: &Array2<f64>, r: usize,
760 p: usize,
761) -> StatsResult<Vec<Array2<f64>>> {
762 let big_t = factors.nrows();
763 if big_t <= p {
764 return Ok(vec![Array2::zeros((r, r)); p]);
766 }
767
768 let n = big_t - p;
769 let y_reg = factors.slice(scirs2_core::ndarray::s![p.., ..]).to_owned();
770 let mut x_reg = Array2::<f64>::zeros((n, r * p));
771 for t_idx in 0..n {
772 let t = t_idx + p;
773 for lag in 0..p {
774 let f_lag = factors.row(t - 1 - lag);
775 for j in 0..r {
776 x_reg[[t_idx, lag * r + j]] = f_lag[j];
777 }
778 }
779 }
780
781 let x_reg_t = x_reg.t().to_owned();
783 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);
789 for lag in 0..p {
790 let a_t = b_hat
791 .slice(scirs2_core::ndarray::s![lag * r..(lag + 1) * r, ..])
792 .to_owned();
793 ar_mats.push(a_t.t().to_owned());
794 }
795 Ok(ar_mats)
796}
797
798fn build_companion(ar_mats: &[Array2<f64>], r: usize, p: usize) -> StatsResult<Array2<f64>> {
804 let dim = r * p;
805 let mut comp = Array2::<f64>::zeros((dim, dim));
806
807 for (lag, a) in ar_mats.iter().enumerate() {
808 if a.nrows() != r || a.ncols() != r {
809 return Err(StatsError::DimensionMismatch(format!(
810 "AR matrix {} has shape {}×{}, expected {}×{}",
811 lag,
812 a.nrows(),
813 a.ncols(),
814 r,
815 r
816 )));
817 }
818 for i in 0..r {
819 for j in 0..r {
820 comp[[i, lag * r + j]] = a[[i, j]];
821 }
822 }
823 }
824 for i in 1..p {
826 for j in 0..r {
827 comp[[i * r + j, (i - 1) * r + j]] = 1.0;
828 }
829 }
830 Ok(comp)
831}
832
833fn build_companion_q(q: &Array2<f64>, r: usize, p: usize) -> Array2<f64> {
835 let dim = r * p;
836 let mut cq = Array2::<f64>::zeros((dim, dim));
837 for i in 0..r {
838 for j in 0..r {
839 cq[[i, j]] = q[[i, j]];
840 }
841 }
842 if p > 1 {
847 let q_trace = (0..r).map(|i| q[[i, i]]).sum::<f64>() / r as f64;
848 let eps = (1e-8 * q_trace).max(1e-12);
849 for i in r..dim {
850 cq[[i, i]] = eps;
851 }
852 }
853 cq
854}
855
856fn impute_column_means(x: &Array2<f64>) -> Array2<f64> {
862 let big_t = x.nrows();
863 let k = x.ncols();
864 let mut out = x.clone();
865 for j in 0..k {
866 let col = x.column(j);
867 let (sum, cnt) = col
868 .iter()
869 .filter(|v| v.is_finite())
870 .fold((0.0_f64, 0usize), |(s, n), &v| (s + v, n + 1));
871 if cnt == 0 {
872 continue;
873 }
874 let mean = sum / cnt as f64;
875 for i in 0..big_t {
876 if !out[[i, j]].is_finite() {
877 out[[i, j]] = mean;
878 }
879 }
880 }
881 out
882}
883
884fn mat_vec_mul(a: &Array2<f64>, x: &Array1<f64>) -> StatsResult<Array1<f64>> {
889 if a.ncols() != x.len() {
890 return Err(StatsError::DimensionMismatch(format!(
891 "mat_vec_mul: A is {}×{} but x has len {}",
892 a.nrows(),
893 a.ncols(),
894 x.len()
895 )));
896 }
897 let n = a.nrows();
898 let m = a.ncols();
899 let mut y = Array1::<f64>::zeros(n);
900 for i in 0..n {
901 let mut s = 0.0_f64;
902 for k in 0..m {
903 s += a[[i, k]] * x[k];
904 }
905 y[i] = s;
906 }
907 Ok(y)
908}
909
910fn mat_mat_mul(a: &Array2<f64>, b: &Array2<f64>) -> StatsResult<Array2<f64>> {
911 if a.ncols() != b.nrows() {
912 return Err(StatsError::DimensionMismatch(format!(
913 "mat_mat_mul: A is {}×{} but B is {}×{}",
914 a.nrows(),
915 a.ncols(),
916 b.nrows(),
917 b.ncols()
918 )));
919 }
920 let n = a.nrows();
921 let kk = a.ncols();
922 let m = b.ncols();
923 let mut c = Array2::<f64>::zeros((n, m));
924 for i in 0..n {
925 for j in 0..m {
926 let mut s = 0.0_f64;
927 for l in 0..kk {
928 s += a[[i, l]] * b[[l, j]];
929 }
930 c[[i, j]] = s;
931 }
932 }
933 Ok(c)
934}
935
936fn mat_mat_mul_bt(a: &Array2<f64>, b: &Array2<f64>) -> StatsResult<Array2<f64>> {
937 if a.ncols() != b.ncols() {
938 return Err(StatsError::DimensionMismatch(format!(
939 "mat_mat_mul_bt: A is {}×{} but B^T is {}×{}",
940 a.nrows(),
941 a.ncols(),
942 b.ncols(),
943 b.nrows()
944 )));
945 }
946 let n = a.nrows();
947 let kk = a.ncols();
948 let m = b.nrows();
949 let mut c = Array2::<f64>::zeros((n, m));
950 for i in 0..n {
951 for j in 0..m {
952 let mut s = 0.0_f64;
953 for l in 0..kk {
954 s += a[[i, l]] * b[[j, l]];
955 }
956 c[[i, j]] = s;
957 }
958 }
959 Ok(c)
960}
961
962fn eye_minus(a: Array2<f64>) -> StatsResult<Array2<f64>> {
963 let n = a.nrows();
964 if a.ncols() != n {
965 return Err(StatsError::DimensionMismatch(
966 "eye_minus: not square".into(),
967 ));
968 }
969 let mut result = -a;
970 for i in 0..n {
971 result[[i, i]] += 1.0;
972 }
973 Ok(result)
974}
975
976fn cholesky_lower(a: Array2<f64>) -> StatsResult<Array2<f64>> {
977 let n = a.nrows();
978 if a.ncols() != n {
979 return Err(StatsError::DimensionMismatch(
980 "cholesky_lower: not square".into(),
981 ));
982 }
983 let mut l = Array2::<f64>::zeros((n, n));
984 for i in 0..n {
985 for j in 0..=i {
986 let mut s = a[[i, j]];
987 for kk in 0..j {
988 s -= l[[i, kk]] * l[[j, kk]];
989 }
990 if i == j {
991 if s <= 0.0 {
992 let eps = (1e-10_f64).max(s.abs() * 1e-8);
993 let s_reg = s + eps;
994 if s_reg <= 0.0 {
995 return Err(StatsError::ComputationError(format!(
996 "Cholesky failed at ({},{}): s={}",
997 i, j, s
998 )));
999 }
1000 l[[i, j]] = s_reg.sqrt();
1001 } else {
1002 l[[i, j]] = s.sqrt();
1003 }
1004 } else {
1005 let ljj = l[[j, j]];
1006 if ljj.abs() < 1e-15 {
1007 return Err(StatsError::ComputationError(
1008 "Cholesky: near-zero diagonal".into(),
1009 ));
1010 }
1011 l[[i, j]] = s / ljj;
1012 }
1013 }
1014 }
1015 Ok(l)
1016}
1017
1018fn lower_tri_inv(l: &Array2<f64>) -> StatsResult<Array2<f64>> {
1019 let n = l.nrows();
1020 let mut inv = Array2::<f64>::zeros((n, n));
1021 for j in 0..n {
1022 inv[[j, j]] = 1.0 / l[[j, j]];
1023 for i in j + 1..n {
1024 let mut s = 0.0_f64;
1025 for kk in j..i {
1026 s += l[[i, kk]] * inv[[kk, j]];
1027 }
1028 inv[[i, j]] = -s / l[[i, i]];
1029 }
1030 }
1031 Ok(inv)
1032}
1033
1034fn inv_symmetric(a: Array2<f64>) -> StatsResult<Array2<f64>> {
1035 let n = a.nrows();
1036 if a.ncols() != n {
1037 return Err(StatsError::DimensionMismatch(
1038 "inv_symmetric: not square".into(),
1039 ));
1040 }
1041 if n == 1 {
1042 let val = a[[0, 0]];
1043 if val.abs() < 1e-15 {
1044 return Err(StatsError::ComputationError(
1045 "inv_symmetric: singular 1×1".into(),
1046 ));
1047 }
1048 let mut inv = Array2::<f64>::zeros((1, 1));
1049 inv[[0, 0]] = 1.0 / val;
1050 return Ok(inv);
1051 }
1052
1053 let max_diag = (0..n)
1055 .map(|i| a[[i, i]].abs())
1056 .fold(0.0_f64, f64::max)
1057 .max(1e-12);
1058
1059 let regularisation_levels = [0.0, 1e-10, 1e-8, 1e-6, 1e-4, 1e-2, 1e-1, 1.0, 10.0];
1061 for &eps_scale in ®ularisation_levels {
1062 let mut reg_a = a.clone();
1063 let eps = eps_scale * max_diag;
1064 if eps > 0.0 {
1065 for i in 0..n {
1066 reg_a[[i, i]] += eps;
1067 }
1068 }
1069 match cholesky_lower(reg_a) {
1070 Ok(l) => {
1071 let l_inv = lower_tri_inv(&l)?;
1072 let l_inv_t = l_inv.t().to_owned();
1073 return mat_mat_mul(&l_inv_t, &l_inv);
1074 }
1075 Err(_) => continue,
1076 }
1077 }
1078 gauss_jordan_inv(&a)
1081}
1082
1083fn gauss_jordan_inv(a: &Array2<f64>) -> StatsResult<Array2<f64>> {
1086 let n = a.nrows();
1087 let mut aug = Array2::<f64>::zeros((n, 2 * n));
1089 for i in 0..n {
1090 for j in 0..n {
1091 aug[[i, j]] = a[[i, j]];
1092 }
1093 aug[[i, n + i]] = 1.0;
1094 }
1095
1096 for col in 0..n {
1097 let mut max_row = col;
1099 let mut max_val = aug[[col, col]].abs();
1100 for row in (col + 1)..n {
1101 let val = aug[[row, col]].abs();
1102 if val > max_val {
1103 max_val = val;
1104 max_row = row;
1105 }
1106 }
1107 if max_val < 1e-15 {
1108 return Err(StatsError::ComputationError(
1109 "gauss_jordan_inv: singular matrix".into(),
1110 ));
1111 }
1112 if max_row != col {
1114 for j in 0..(2 * n) {
1115 let tmp = aug[[col, j]];
1116 aug[[col, j]] = aug[[max_row, j]];
1117 aug[[max_row, j]] = tmp;
1118 }
1119 }
1120 let pivot = aug[[col, col]];
1122 for j in 0..(2 * n) {
1123 aug[[col, j]] /= pivot;
1124 }
1125 for row in 0..n {
1127 if row == col {
1128 continue;
1129 }
1130 let factor = aug[[row, col]];
1131 for j in 0..(2 * n) {
1132 aug[[row, j]] -= factor * aug[[col, j]];
1133 }
1134 }
1135 }
1136
1137 let mut inv = Array2::<f64>::zeros((n, n));
1139 for i in 0..n {
1140 for j in 0..n {
1141 inv[[i, j]] = aug[[i, n + j]];
1142 }
1143 }
1144 Ok(inv)
1145}
1146
1147fn inv_symmetric_regularised(mut a: Array2<f64>, reg: f64) -> StatsResult<Array2<f64>> {
1149 let n = a.nrows();
1150 for i in 0..n {
1151 a[[i, i]] += reg;
1152 }
1153 inv_symmetric(a)
1154}
1155
1156fn log_det_posdef(a: &Array2<f64>) -> StatsResult<f64> {
1157 let n = a.nrows();
1158 let max_diag = (0..n)
1160 .map(|i| a[[i, i]].abs())
1161 .fold(0.0_f64, f64::max)
1162 .max(1e-12);
1163
1164 let reg_levels = [0.0, 1e-10, 1e-8, 1e-6, 1e-4, 1e-2, 1e-1, 1.0, 10.0];
1165 for &eps_scale in ®_levels {
1166 let mut reg_a = a.clone();
1167 let eps = eps_scale * max_diag;
1168 if eps > 0.0 {
1169 for i in 0..n {
1170 reg_a[[i, i]] += eps;
1171 }
1172 }
1173 match cholesky_lower(reg_a) {
1174 Ok(l) => {
1175 let log_det: f64 = (0..n).map(|i| 2.0 * l[[i, i]].ln()).sum();
1176 return Ok(log_det);
1177 }
1178 Err(_) => continue,
1179 }
1180 }
1181 Err(StatsError::ComputationError(
1182 "log_det_posdef: Cholesky failed after regularisation".into(),
1183 ))
1184}
1185
1186fn symmetrize(a: &mut Array2<f64>) {
1187 let n = a.nrows();
1188 for i in 0..n {
1189 for j in i + 1..n {
1190 let v = (a[[i, j]] + a[[j, i]]) * 0.5;
1191 a[[i, j]] = v;
1192 a[[j, i]] = v;
1193 }
1194 }
1195}
1196
1197fn regularise_pd(a: &mut Array2<f64>, eps: f64) {
1198 let n = a.nrows();
1199 for i in 0..n {
1200 a[[i, i]] = a[[i, i]].max(eps);
1201 }
1202}
1203
1204#[cfg(test)]
1209mod tests {
1210 use super::*;
1211 use scirs2_core::ndarray::Array2;
1212
1213 fn synthetic_data(t: usize, k: usize, r: usize) -> Array2<f64> {
1214 let lambda: Array2<f64> =
1217 Array2::from_shape_fn((k, r), |(i, j)| ((i + j + 1) as f64) * 0.5);
1218 let mut factors = Array2::<f64>::zeros((t, r));
1219 for t_idx in 1..t {
1220 for j in 0..r {
1221 let prev = factors[[t_idx - 1, j]];
1222 factors[[t_idx, j]] = prev * 0.9 + (t_idx as f64 * 0.1 + j as f64).sin() * 0.5;
1224 }
1225 }
1226 let mut x = Array2::<f64>::zeros((t, k));
1227 for t_idx in 0..t {
1228 for i in 0..k {
1229 let mut val = 0.0_f64;
1230 for j in 0..r {
1231 val += factors[[t_idx, j]] * lambda[[i, j]];
1232 }
1233 val += (t_idx as f64 * 0.3 + i as f64).cos() * 0.1;
1235 x[[t_idx, i]] = val;
1236 }
1237 }
1238 x
1239 }
1240
1241 #[test]
1242 fn test_dfm_basic_fit() {
1243 let data = synthetic_data(50, 4, 2);
1244 let model = fit_dynamic_factor(&data, 2, 1, 30, None).expect("fit_dynamic_factor");
1245 assert_eq!(model.n_factors, 2);
1246 assert_eq!(model.n_vars, 4);
1247 assert_eq!(model.n_lags, 1);
1248 assert_eq!(model.factors.nrows(), 50);
1249 assert_eq!(model.factors.ncols(), 2);
1250 assert_eq!(model.loadings.nrows(), 4);
1251 assert_eq!(model.loadings.ncols(), 2);
1252 assert!(!model.log_lik_history.is_empty());
1253 let last_ll = *model.log_lik_history.last().expect("log_lik");
1254 assert!(last_ll.is_finite(), "log-likelihood must be finite");
1255 }
1256
1257 #[test]
1258 fn test_dfm_extract_factors() {
1259 let data = synthetic_data(40, 5, 1);
1260 let model = fit_dynamic_factor(&data, 1, 1, 20, None).expect("fit");
1261 let factors = model.extract_factors();
1262 assert_eq!(factors.nrows(), 40);
1263 assert_eq!(factors.ncols(), 1);
1264 }
1265
1266 #[test]
1267 fn test_dfm_forecast_one_step() {
1268 let data = synthetic_data(50, 4, 2);
1269 let model = fit_dynamic_factor(&data, 2, 1, 20, None).expect("fit");
1270 let forecast = model.forecast_one_step().expect("forecast");
1271 assert_eq!(forecast.len(), 4);
1272 assert!(forecast.iter().all(|v| v.is_finite()));
1273 }
1274
1275 #[test]
1276 fn test_dfm_with_two_lags() {
1277 let data = synthetic_data(120, 4, 2);
1280 let model = fit_dynamic_factor(&data, 2, 2, 20, Some(1e-4)).expect("fit p=2");
1281 assert_eq!(model.ar_matrices.len(), 2);
1282 assert_eq!(model.factors.nrows(), 120);
1283 }
1284
1285 #[test]
1286 fn test_dfm_invalid_inputs() {
1287 let data = synthetic_data(30, 4, 2);
1288 assert!(fit_dynamic_factor(&data, 5, 1, 10, None).is_err());
1290 let tiny = synthetic_data(3, 4, 1);
1292 assert!(fit_dynamic_factor(&tiny, 1, 3, 10, None).is_err());
1293 }
1294
1295 #[test]
1296 fn test_dynamic_factor_spec() {
1297 let spec = DynamicFactor::new(2, 5, 1).expect("spec");
1298 assert_eq!(spec.n_factors, 2);
1299 assert_eq!(spec.n_vars, 5);
1300 assert_eq!(spec.n_lags, 1);
1301 assert!(DynamicFactor::new(6, 5, 1).is_err());
1303 assert!(DynamicFactor::new(0, 5, 1).is_err());
1305 }
1306
1307 #[test]
1308 fn test_impute_column_means() {
1309 let mut x = Array2::<f64>::zeros((4, 2));
1310 x[[0, 0]] = 1.0;
1311 x[[1, 0]] = f64::NAN;
1312 x[[2, 0]] = 3.0;
1313 x[[3, 0]] = 4.0;
1314 let out = impute_column_means(&x);
1315 assert!(out[[1, 0]].is_finite());
1317 assert!((out[[1, 0]] - (1.0 + 3.0 + 4.0) / 3.0).abs() < 1e-10);
1318 }
1319
1320 #[test]
1321 fn test_pca_init_dimensions() {
1322 let data = synthetic_data(30, 5, 2);
1323 let (lambda, factors) = pca_init(&data, 2).expect("pca_init");
1324 assert_eq!(lambda.nrows(), 5);
1325 assert_eq!(lambda.ncols(), 2);
1326 assert_eq!(factors.nrows(), 30);
1327 assert_eq!(factors.ncols(), 2);
1328 }
1329}