1pub fn eigh(a: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
25 let mut am = a.to_vec();
26 let mut w = vec![0f64; n];
27 let _info = crate::blas::dsyevd(&mut am, &mut w, n);
28 (w, am)
29}
30
31pub fn eigh_batch(mats: &[Vec<f64>], n: usize) -> Vec<(Vec<f64>, Vec<f64>)> {
34 use rayon::prelude::*;
35 mats.par_iter().map(|a| eigh(a, n)).collect()
36}
37
38pub fn matrix_fn(a: &[f64], n: usize, f: impl Fn(f64) -> f64) -> Vec<f64> {
40 let (w, v) = eigh(a, n);
41 let fl: Vec<f64> = w.iter().map(|&l| f(l)).collect();
42 let mut out = vec![0f64; n * n];
43 for k in 0..n {
44 let fk = fl[k];
45 for i in 0..n {
46 let vik = fk * v[k * n + i];
47 for j in 0..n {
48 out[i * n + j] += vik * v[k * n + j];
49 }
50 }
51 }
52 out
53}
54
55pub fn logm(a: &[f64], n: usize) -> Vec<f64> {
57 matrix_fn(a, n, |l| l.max(1e-12).ln())
58}
59pub fn expm(a: &[f64], n: usize) -> Vec<f64> {
61 matrix_fn(a, n, |l| l.exp())
62}
63pub fn sqrtm(a: &[f64], n: usize) -> Vec<f64> {
65 matrix_fn(a, n, |l| l.max(0.0).sqrt())
66}
67pub fn invsqrtm(a: &[f64], n: usize) -> Vec<f64> {
69 matrix_fn(a, n, |l| 1.0 / l.max(1e-12).sqrt())
70}
71
72fn sqrt_invsqrt(a: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
76 let (w, v) = eigh(a, n);
77 let sh: Vec<f64> = w.iter().map(|&l| l.max(0.0).sqrt()).collect();
78 let ish: Vec<f64> = w.iter().map(|&l| 1.0 / l.max(1e-12).sqrt()).collect();
79 (reconstruct(&sh, &v, n), reconstruct(&ish, &v, n))
80}
81
82fn matrix_fn_batch(mats: &[Vec<f64>], n: usize, f: impl Fn(f64) -> f64 + Sync) -> Vec<Vec<f64>> {
88 use rayon::prelude::*;
89 mats.par_iter().map(|a| matrix_fn(a, n, &f)).collect()
90}
91
92pub fn logm_batch(covs: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
94 matrix_fn_batch(covs, n, |l| l.max(1e-12).ln())
95}
96pub fn expm_batch(mats: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
98 matrix_fn_batch(mats, n, |l| l.exp())
99}
100pub fn sqrtm_batch(covs: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
102 matrix_fn_batch(covs, n, |l| l.max(0.0).sqrt())
103}
104pub fn invsqrtm_batch(covs: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
106 matrix_fn_batch(covs, n, |l| 1.0 / l.max(1e-12).sqrt())
107}
108
109fn matmul(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
110 let mut o = vec![0f64; n * n];
111 for i in 0..n {
112 for k in 0..n {
113 let aik = a[i * n + k];
114 if aik == 0.0 {
115 continue;
116 }
117 for j in 0..n {
118 o[i * n + j] += aik * b[k * n + j];
119 }
120 }
121 }
122 o
123}
124
125pub fn airm_dist2(a: &[f64], b: &[f64], n: usize) -> f64 {
128 let w = invsqrtm(a, n);
129 let m = matmul(&matmul(&w, b, n), &w, n);
130 let (evals, _) = eigh(&m, n);
131 evals.iter().map(|&l| l.max(1e-12).ln().powi(2)).sum()
132}
133
134pub fn karcher_mean(covs: &[Vec<f64>], n: usize, iters: usize, tol: f64) -> Vec<f64> {
138 let k = covs.len().max(1) as f64;
139 let mut m = vec![0f64; n * n];
140 for c in covs {
141 for i in 0..n * n {
142 m[i] += c[i] / k;
143 }
144 }
145 for _ in 0..iters {
146 let msqrt = sqrtm(&m, n);
147 let minv = invsqrtm(&m, n);
148 let mut sbar = vec![0f64; n * n];
149 for c in covs {
150 let wcw = matmul(&matmul(&minv, c, n), &minv, n);
151 let l = logm(&wcw, n);
152 for i in 0..n * n {
153 sbar[i] += l[i] / k;
154 }
155 }
156 let e = expm(&sbar, n);
157 m = matmul(&matmul(&msqrt, &e, n), &msqrt, n);
158 let norm: f64 = sbar.iter().map(|x| x * x).sum::<f64>().sqrt();
159 if norm < tol {
160 break;
161 }
162 }
163 m
164}
165
166pub fn karcher_mean_weighted(
184 covs: &[Vec<f64>],
185 weights: &[f64],
186 n: usize,
187 iters: usize,
188 tol: f64,
189) -> Vec<f64> {
190 assert_eq!(
191 covs.len(),
192 weights.len(),
193 "karcher_mean_weighted: {} covs but {} weights",
194 covs.len(),
195 weights.len()
196 );
197 let wsum: f64 = weights.iter().sum();
199 let wbar: Vec<f64> = if wsum.abs() < 1e-300 {
200 let u = 1.0 / covs.len().max(1) as f64;
201 vec![u; covs.len()]
202 } else {
203 weights.iter().map(|&w| w / wsum).collect()
204 };
205 let mut m = vec![0f64; n * n];
207 for (c, &w) in covs.iter().zip(&wbar) {
208 for i in 0..n * n {
209 m[i] += w * c[i];
210 }
211 }
212 for _ in 0..iters {
213 let msqrt = sqrtm(&m, n);
214 let minv = invsqrtm(&m, n);
215 let mut sbar = vec![0f64; n * n];
216 for (c, &w) in covs.iter().zip(&wbar) {
217 let wcw = matmul(&matmul(&minv, c, n), &minv, n);
218 let l = logm(&wcw, n);
219 for i in 0..n * n {
220 sbar[i] += w * l[i];
221 }
222 }
223 let e = expm(&sbar, n);
224 m = matmul(&matmul(&msqrt, &e, n), &msqrt, n);
225 let norm: f64 = sbar.iter().map(|x| x * x).sum::<f64>().sqrt();
226 if norm < tol {
227 break;
228 }
229 }
230 m
231}
232
233fn transpose(a: &[f64], n: usize) -> Vec<f64> {
244 let mut t = vec![0f64; n * n];
245 for i in 0..n {
246 for j in 0..n {
247 t[j * n + i] = a[i * n + j];
248 }
249 }
250 t
251}
252
253fn symmetrize(a: &[f64], n: usize) -> Vec<f64> {
255 let mut o = vec![0f64; n * n];
256 for i in 0..n {
257 for j in 0..n {
258 o[i * n + j] = 0.5 * (a[i * n + j] + a[j * n + i]);
259 }
260 }
261 o
262}
263
264pub fn bimap(w: &[f64], x: &[f64], m: usize, n: usize) -> Vec<f64> {
270 let mut t = vec![0f64; m * n];
272 crate::blas::dgemm(w, x, &mut t, m, n, n);
273 let mut wt = vec![0f64; n * m];
275 for i in 0..m {
276 for j in 0..n {
277 wt[j * m + i] = w[i * n + j];
278 }
279 }
280 let mut y = vec![0f64; m * m];
281 crate::blas::dgemm(&t, &wt, &mut y, m, n, m);
282 y
283}
284
285pub fn reeig(x: &[f64], n: usize, eps: f64) -> Vec<f64> {
289 matrix_fn(x, n, |l| l.max(eps))
290}
291
292pub fn logeig(x: &[f64], n: usize, eps: f64) -> Vec<f64> {
297 matrix_fn(x, n, |l| l.max(eps).ln())
298}
299
300pub fn spectral_backward(
315 x: &[f64],
316 dy: &[f64],
317 n: usize,
318 f: impl Fn(f64) -> f64,
319 fprime: impl Fn(f64) -> f64,
320) -> Vec<f64> {
321 let (lam, v) = eigh(x, n);
322 spectral_backward_precomputed(&lam, &v, dy, n, f, fprime)
323}
324
325fn mm(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
327 let mut o = vec![0f64; n * n];
328 crate::blas::dgemm(a, b, &mut o, n, n, n);
329 o
330}
331
332fn reconstruct(fk: &[f64], v: &[f64], n: usize) -> Vec<f64> {
336 let mut sv = vec![0f64; n * n];
337 for k in 0..n {
338 for i in 0..n {
339 sv[k * n + i] = fk[k] * v[k * n + i];
340 }
341 }
342 mm(&transpose(&sv, n), v, n)
343}
344
345pub fn spectral_forward_packed(x: &[f64], n: usize, f: impl Fn(f64) -> f64) -> Vec<f64> {
351 let (lam, v) = eigh(x, n);
352 let fk: Vec<f64> = lam.iter().map(|&l| f(l)).collect();
353 let y = reconstruct(&fk, &v, n);
354 let mut out = vec![0f64; 2 * n * n + n];
355 out[..n * n].copy_from_slice(&y);
356 out[n * n..n * n + n].copy_from_slice(&lam);
357 out[n * n + n..].copy_from_slice(&v);
358 out
359}
360
361pub fn spectral_backward_precomputed(
366 lam: &[f64],
367 v: &[f64],
368 dy: &[f64],
369 n: usize,
370 f: impl Fn(f64) -> f64,
371 fprime: impl Fn(f64) -> f64,
372) -> Vec<f64> {
373 let g = symmetrize(dy, n);
374 let vt = transpose(v, n);
375 let c = mm(&mm(v, &g, n), &vt, n);
377 let fl: Vec<f64> = lam.iter().map(|&l| f(l)).collect();
379 let scale = lam.iter().fold(0f64, |acc, &l| acc.max(l.abs())).max(1.0);
380 let tol = scale * 1e-9;
381 let mut mmat = vec![0f64; n * n];
382 for a in 0..n {
383 for b in 0..n {
384 let d = lam[a] - lam[b];
385 let p = if d.abs() > tol {
386 (fl[a] - fl[b]) / d
387 } else {
388 fprime(lam[a])
389 };
390 mmat[a * n + b] = p * c[a * n + b];
391 }
392 }
393 let dx = mm(&mm(&vt, &mmat, n), v, n);
395 symmetrize(&dx, n)
396}
397
398pub fn reeig_backward(x: &[f64], dy: &[f64], n: usize, eps: f64) -> Vec<f64> {
402 let (lam, v) = eigh(x, n);
403 reeig_backward_precomputed(&lam, &v, dy, n, eps)
404}
405
406pub fn reeig_backward_precomputed(
408 lam: &[f64],
409 v: &[f64],
410 dy: &[f64],
411 n: usize,
412 eps: f64,
413) -> Vec<f64> {
414 spectral_backward_precomputed(
415 lam,
416 v,
417 dy,
418 n,
419 |l| l.max(eps),
420 |l| {
421 if l > eps { 1.0 } else { 0.0 }
422 },
423 )
424}
425
426pub fn logeig_backward(x: &[f64], dy: &[f64], n: usize, eps: f64) -> Vec<f64> {
428 let (lam, v) = eigh(x, n);
429 logeig_backward_precomputed(&lam, &v, dy, n, eps)
430}
431
432pub fn logeig_backward_precomputed(
434 lam: &[f64],
435 v: &[f64],
436 dy: &[f64],
437 n: usize,
438 eps: f64,
439) -> Vec<f64> {
440 spectral_backward_precomputed(lam, v, dy, n, |l| l.max(eps).ln(), |l| 1.0 / l.max(eps))
441}
442
443pub fn spd_bn_transport(
454 x: &[f64],
455 mean: &[f64],
456 g: &[f64],
457 batch: usize,
458 n: usize,
459 eps: f64,
460) -> Vec<f64> {
461 use rayon::prelude::*;
462 let ms = matrix_fn(mean, n, |l| 1.0 / l.max(eps).sqrt()); let gs = matrix_fn(g, n, |l| l.max(eps).sqrt()); let slices: Vec<Vec<f64>> = (0..batch)
468 .into_par_iter()
469 .map(|bi| {
470 let xi = &x[bi * n * n..(bi + 1) * n * n];
471 let ci = mm(&mm(&ms, xi, n), &ms, n); mm(&mm(&gs, &ci, n), &gs, n) })
474 .collect();
475 slices.concat()
476}
477
478pub fn spd_bn_backward_x(
482 mean: &[f64],
483 g: &[f64],
484 dy: &[f64],
485 batch: usize,
486 n: usize,
487 eps: f64,
488) -> Vec<f64> {
489 use rayon::prelude::*;
490 let ms = matrix_fn(mean, n, |l| 1.0 / l.max(eps).sqrt());
491 let gs = matrix_fn(g, n, |l| l.max(eps).sqrt());
492 let q = mm(&ms, &gs, n); let qt = transpose(&q, n); let slices: Vec<Vec<f64>> = (0..batch)
495 .into_par_iter()
496 .map(|bi| {
497 let gi = &dy[bi * n * n..(bi + 1) * n * n];
498 symmetrize(&mm(&mm(&q, gi, n), &qt, n), n) })
500 .collect();
501 slices.concat()
502}
503
504pub fn spd_bn_backward_g(
509 x: &[f64],
510 mean: &[f64],
511 g: &[f64],
512 dy: &[f64],
513 batch: usize,
514 n: usize,
515 eps: f64,
516) -> Vec<f64> {
517 use rayon::prelude::*;
518 let ms = matrix_fn(mean, n, |l| 1.0 / l.max(eps).sqrt());
519 let gs = matrix_fn(g, n, |l| l.max(eps).sqrt());
520 let dgs = (0..batch)
522 .into_par_iter()
523 .map(|bi| {
524 let xi = &x[bi * n * n..(bi + 1) * n * n];
525 let gi = &dy[bi * n * n..(bi + 1) * n * n];
526 let ci = mm(&mm(&ms, xi, n), &ms, n); let t1 = mm(&mm(gi, &gs, n), &ci, n); let t2 = mm(&mm(&ci, &gs, n), gi, n); let mut s = t1;
530 for k in 0..n * n {
531 s[k] += t2[k];
532 }
533 s
534 })
535 .reduce(
536 || vec![0f64; n * n],
537 |mut a, b| {
538 for k in 0..n * n {
539 a[k] += b[k];
540 }
541 a
542 },
543 );
544 spectral_backward(
546 g,
547 &dgs,
548 n,
549 |l| l.max(eps).sqrt(),
550 |l| 0.5 / l.max(eps).sqrt(),
551 )
552}
553
554pub fn geodesic_interp(a: &[f64], b: &[f64], t: f64, n: usize) -> Vec<f64> {
561 let (asqrt, ainv) = sqrt_invsqrt(a, n);
562 let m = matmul(&matmul(&ainv, b, n), &ainv, n);
563 let mt = matrix_fn(&m, n, |l| l.max(1e-12).powf(t));
564 matmul(&matmul(&asqrt, &mt, n), &asqrt, n)
565}
566
567pub fn log_map(base: &[f64], x: &[f64], n: usize) -> Vec<f64> {
579 let (phalf, pinv) = sqrt_invsqrt(base, n);
580 let m = matmul(&matmul(&pinv, x, n), &pinv, n); let lm = logm(&m, n);
582 matmul(&matmul(&phalf, &lm, n), &phalf, n)
583}
584
585pub fn exp_map(base: &[f64], v: &[f64], n: usize) -> Vec<f64> {
598 let (phalf, pinv) = sqrt_invsqrt(base, n);
599 let m = matmul(&matmul(&pinv, v, n), &pinv, n); let em = expm(&m, n);
601 matmul(&matmul(&phalf, &em, n), &phalf, n)
602}
603
604pub fn parallel_transport(from: &[f64], to: &[f64], v: &[f64], n: usize) -> Vec<f64> {
619 let (phalf, pinv) = sqrt_invsqrt(from, n);
620 let wq = matmul(&matmul(&pinv, to, n), &pinv, n); let wq_half = sqrtm(&wq, n);
622 let e = matmul(&matmul(&phalf, &wq_half, n), &pinv, n); let et = transpose(&e, n);
624 let evet = matmul(&matmul(&e, v, n), &et, n);
625 symmetrize(&evet, n) }
627
628fn add(a: &[f64], b: &[f64]) -> Vec<f64> {
640 a.iter().zip(b).map(|(x, y)| x + y).collect()
641}
642
643fn sqrt_ff(l: f64) -> f64 {
647 l.max(0.0).sqrt()
648}
649fn sqrt_fp(l: f64) -> f64 {
650 0.5 / l.max(1e-12).sqrt()
651}
652fn invsqrt_ff(l: f64) -> f64 {
653 1.0 / l.max(1e-12).sqrt()
654}
655fn invsqrt_fp(l: f64) -> f64 {
656 -0.5 * l.max(1e-12).powf(-1.5)
657}
658fn log_ff(l: f64) -> f64 {
659 l.max(1e-12).ln()
660}
661fn log_fp(l: f64) -> f64 {
662 1.0 / l.max(1e-12)
663}
664
665fn reconstruct_fn(lam: &[f64], v: &[f64], n: usize, f: impl Fn(f64) -> f64) -> Vec<f64> {
670 let fk: Vec<f64> = lam.iter().map(|&l| f(l)).collect();
671 reconstruct(&fk, v, n)
672}
673
674pub fn log_map_backward(base: &[f64], x: &[f64], dy: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
681 let (lam_b, v_b) = eigh(base, n);
682 let a = reconstruct_fn(&lam_b, &v_b, n, sqrt_ff); let w = reconstruct_fn(&lam_b, &v_b, n, invsqrt_ff); let m = matmul(&matmul(&w, x, n), &w, n); let (lam_m, v_m) = eigh(&m, n);
686 let g = symmetrize(dy, n);
687 let lbar = matmul(&matmul(&a, &g, n), &a, n); let mbar = spectral_backward_precomputed(&lam_m, &v_m, &lbar, n, log_ff, log_fp);
689 let d_x = symmetrize(&matmul(&matmul(&w, &mbar, n), &w, n), n); let l = reconstruct_fn(&lam_m, &v_m, n, log_ff); let gal = matmul(&matmul(&g, &a, n), &l, n); let abar = add(&gal, &transpose(&gal, n)); let mwx = matmul(&matmul(&mbar, &w, n), x, n); let wbar = add(&mwx, &transpose(&mwx, n)); let d_base_a = spectral_backward_precomputed(&lam_b, &v_b, &abar, n, sqrt_ff, sqrt_fp);
697 let d_base_w = spectral_backward_precomputed(&lam_b, &v_b, &wbar, n, invsqrt_ff, invsqrt_fp);
698 let d_base = symmetrize(&add(&d_base_a, &d_base_w), n);
699 (d_base, d_x)
700}
701
702pub fn exp_map_backward(base: &[f64], v: &[f64], dy: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
706 let (lam_b, v_b) = eigh(base, n);
707 let a = reconstruct_fn(&lam_b, &v_b, n, sqrt_ff);
708 let w = reconstruct_fn(&lam_b, &v_b, n, invsqrt_ff);
709 let m = matmul(&matmul(&w, v, n), &w, n); let (lam_m, v_m) = eigh(&m, n);
711 let g = symmetrize(dy, n);
712 let ebar = matmul(&matmul(&a, &g, n), &a, n); let mbar = spectral_backward_precomputed(&lam_m, &v_m, &ebar, n, f64::exp, f64::exp);
714 let d_v = symmetrize(&matmul(&matmul(&w, &mbar, n), &w, n), n); let e = reconstruct_fn(&lam_m, &v_m, n, f64::exp); let gae = matmul(&matmul(&g, &a, n), &e, n); let abar = add(&gae, &transpose(&gae, n));
718 let mwv = matmul(&matmul(&mbar, &w, n), v, n); let wbar = add(&mwv, &transpose(&mwv, n));
720 let d_base_a = spectral_backward_precomputed(&lam_b, &v_b, &abar, n, sqrt_ff, sqrt_fp);
721 let d_base_w = spectral_backward_precomputed(&lam_b, &v_b, &wbar, n, invsqrt_ff, invsqrt_fp);
722 let d_base = symmetrize(&add(&d_base_a, &d_base_w), n);
723 (d_base, d_v)
724}
725
726pub fn parallel_transport_backward(
732 from: &[f64],
733 to: &[f64],
734 v: &[f64],
735 dy: &[f64],
736 n: usize,
737) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
738 let (lam_f, v_f) = eigh(from, n);
739 let a = reconstruct_fn(&lam_f, &v_f, n, sqrt_ff); let w = reconstruct_fn(&lam_f, &v_f, n, invsqrt_ff); let mq = matmul(&matmul(&w, to, n), &w, n); let (lam_mq, v_mq) = eigh(&mq, n);
743 let s = reconstruct_fn(&lam_mq, &v_mq, n, sqrt_ff); let e = matmul(&matmul(&a, &s, n), &w, n); let et = transpose(&e, n);
746 let g = symmetrize(dy, n);
747 let d_v = symmetrize(&matmul(&matmul(&et, &g, n), &e, n), n);
749 let gev = matmul(&matmul(&g, &e, n), v, n);
751 let ebar: Vec<f64> = gev.iter().map(|x| 2.0 * x).collect();
752 let abar = matmul(&matmul(&ebar, &w, n), &s, n); let sbar = matmul(&matmul(&a, &ebar, n), &w, n); let wbar1 = matmul(&matmul(&s, &a, n), &ebar, n); let mqbar = spectral_backward_precomputed(&lam_mq, &v_mq, &sbar, n, sqrt_ff, sqrt_fp);
758 let d_to = symmetrize(&matmul(&matmul(&w, &mqbar, n), &w, n), n); let mqwq = matmul(&matmul(&mqbar, &w, n), to, n); let wbar = add(&wbar1, &add(&mqwq, &transpose(&mqwq, n))); let d_from_a = spectral_backward_precomputed(&lam_f, &v_f, &abar, n, sqrt_ff, sqrt_fp);
762 let d_from_w = spectral_backward_precomputed(&lam_f, &v_f, &wbar, n, invsqrt_ff, invsqrt_fp);
763 let d_from = symmetrize(&add(&d_from_a, &d_from_w), n);
764 (d_from, d_to, d_v)
765}
766
767pub fn matrix_fn_batch_backward(
772 x: &[f64],
773 dy: &[f64],
774 n: usize,
775 f: impl Fn(f64) -> f64 + Sync,
776 fprime: impl Fn(f64) -> f64 + Sync,
777) -> Vec<f64> {
778 use rayon::prelude::*;
779 let batch = x.len() / (n * n);
780 (0..batch)
781 .into_par_iter()
782 .flat_map(|bi| {
783 let xi = &x[bi * n * n..(bi + 1) * n * n];
784 let dyi = &dy[bi * n * n..(bi + 1) * n * n];
785 spectral_backward(xi, dyi, n, &f, &fprime)
786 })
787 .collect()
788}
789
790pub fn eigh_packed(a: &[f64], n: usize) -> Vec<f64> {
801 let (w, vrow) = eigh(a, n); let u = transpose(&vrow, n); let mut out = vec![0f64; n + n * n];
804 out[..n].copy_from_slice(&w);
805 out[n..].copy_from_slice(&u);
806 out
807}
808
809pub fn eigh_backward_packed(fwd: &[f64], bar: &[f64], n: usize) -> Vec<f64> {
820 let lam = &fwd[..n];
821 let u = &fwd[n..];
822 let lbar = &bar[..n];
823 let ubar = &bar[n..];
824 let ut = transpose(u, n);
825 let c = mm(&ut, ubar, n); let scale = lam.iter().fold(0f64, |a, &l| a.max(l.abs())).max(1.0);
827 let tol = scale * 1e-9;
828 let mut mid = vec![0f64; n * n];
829 for i in 0..n {
830 for j in 0..n {
831 mid[i * n + j] = if i == j {
832 lbar[i]
833 } else {
834 let d = lam[j] - lam[i];
835 if d.abs() > tol { c[i * n + j] / d } else { 0.0 }
836 };
837 }
838 }
839 let abar = mm(&mm(u, &mid, n), &ut, n); symmetrize(&abar, n)
841}
842
843pub fn eigh_batch_packed(mats: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
846 use rayon::prelude::*;
847 mats.par_iter().map(|a| eigh_packed(a, n)).collect()
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853
854 fn ident(n: usize) -> Vec<f64> {
855 let mut a = vec![0f64; n * n];
856 for i in 0..n {
857 a[i * n + i] = 1.0;
858 }
859 a
860 }
861
862 #[test]
863 fn logm_expm_roundtrip() {
864 let n = 4;
866 let mut a = vec![0f64; n * n];
867 for i in 0..n {
868 for j in 0..n {
869 a[i * n + j] = if i == j { 2.0 + i as f64 } else { 0.3 };
870 }
871 }
872 let back = expm(&logm(&a, n), n);
873 let err: f64 = a
874 .iter()
875 .zip(&back)
876 .map(|(x, y)| (x - y).abs())
877 .fold(0.0, f64::max);
878 assert!(err < 1e-6, "logm/expm roundtrip err {err}");
879 }
880
881 #[test]
882 fn sqrtm_squares_back() {
883 let n = 3;
884 let a = vec![4.0, 0.0, 0.0, 0.0, 9.0, 0.0, 0.0, 0.0, 16.0];
885 let s = sqrtm(&a, n);
886 assert!(
887 (s[0] - 2.0).abs() < 1e-9 && (s[4] - 3.0).abs() < 1e-9 && (s[8] - 4.0).abs() < 1e-9
888 );
889 }
890
891 #[test]
892 fn airm_dist_zero_to_self_and_invariant() {
893 let n = 3;
894 let a = vec![2.0, 0.1, 0.0, 0.1, 3.0, 0.2, 0.0, 0.2, 1.5];
895 assert!(airm_dist2(&a, &a, n) < 1e-9, "distance to self must be 0");
896 let k = 5.0;
898 let ka: Vec<f64> = a.iter().map(|x| x * k).collect();
899 let ki: Vec<f64> = ident(n).iter().map(|x| x * k).collect();
900 let d1 = airm_dist2(&a, &ident(n), n);
901 let d2 = airm_dist2(&ka, &ki, n);
902 assert!(
903 (d1 - d2).abs() < 1e-6,
904 "AIRM not affine-invariant: {d1} vs {d2}"
905 );
906 }
907
908 #[test]
909 fn karcher_mean_of_identicals_is_the_matrix() {
910 let n = 3;
911 let a = vec![2.0, 0.1, 0.0, 0.1, 3.0, 0.2, 0.0, 0.2, 1.5];
912 let m = karcher_mean(&[a.clone(), a.clone(), a.clone()], n, 20, 1e-10);
913 let err: f64 = a
914 .iter()
915 .zip(&m)
916 .map(|(x, y)| (x - y).abs())
917 .fold(0.0, f64::max);
918 assert!(err < 1e-6, "Karcher mean of identicals err {err}");
919 }
920
921 fn sym(n: usize, seed: f64) -> Vec<f64> {
925 let mut a = vec![0f64; n * n];
926 for i in 0..n {
927 for j in i..n {
928 let v = ((i as f64 * 3.0 + j as f64 * 1.7 + seed).sin()) * 0.5;
929 a[i * n + j] = v;
930 a[j * n + i] = v;
931 }
932 }
933 a
934 }
935
936 fn spd(n: usize, seed: f64) -> Vec<f64> {
938 let m = sym(n, seed);
939 let mut a = matmul(&m, &m, n); for i in 0..n {
941 a[i * n + i] += (n + 1) as f64;
942 }
943 a
944 }
945
946 fn frob(a: &[f64], b: &[f64]) -> f64 {
947 a.iter().zip(b).map(|(x, y)| x * y).sum()
948 }
949
950 #[test]
951 fn bimap_matches_manual() {
952 let (m, n) = (2, 3);
953 let w = vec![1.0, 0.5, -0.25, 2.0, -1.0, 0.75];
955 let x = spd(n, 0.3);
956 let y = bimap(&w, &x, m, n);
957 let mut wx = vec![0f64; m * n];
959 for i in 0..m {
960 for j in 0..n {
961 let mut s = 0.0;
962 for k in 0..n {
963 s += w[i * n + k] * x[k * n + j];
964 }
965 wx[i * n + j] = s;
966 }
967 }
968 let mut expect = vec![0f64; m * m];
969 for i in 0..m {
970 for j in 0..m {
971 let mut s = 0.0;
972 for k in 0..n {
973 s += wx[i * n + k] * w[j * n + k];
974 }
975 expect[i * m + j] = s;
976 }
977 }
978 let err = y
979 .iter()
980 .zip(&expect)
981 .map(|(a, b)| (a - b).abs())
982 .fold(0.0, f64::max);
983 assert!(err < 1e-10, "bimap err {err}");
984 }
985
986 #[test]
987 fn reeig_floors_spectrum() {
988 let n = 4;
989 let mut x = vec![0f64; n * n];
991 for i in 0..n {
992 x[i * n + i] = [0.01, 0.5, 2.0, 5.0][i];
993 }
994 let eps = 0.1;
995 let y = reeig(&x, n, eps);
996 let (evals, _) = eigh(&y, n);
997 for &l in &evals {
998 assert!(l >= eps - 1e-9, "reeig eigenvalue {l} below eps {eps}");
999 }
1000 assert!((y[0] - eps).abs() < 1e-9, "smallest not floored: {}", y[0]);
1002 assert!((y[n + 1] - 0.5).abs() < 1e-9);
1003 }
1004
1005 #[test]
1006 fn logeig_expm_roundtrip() {
1007 let n = 4;
1008 let x = spd(n, 1.1);
1009 let back = expm(&logeig(&x, n, 1e-12), n);
1010 let err = x
1011 .iter()
1012 .zip(&back)
1013 .map(|(a, b)| (a - b).abs())
1014 .fold(0.0, f64::max);
1015 assert!(err < 1e-8, "expm(logeig) roundtrip err {err}");
1016 }
1017
1018 fn fd_spectral_check(
1022 layer: impl Fn(&[f64], usize) -> Vec<f64>,
1023 backward: impl Fn(&[f64], &[f64], usize) -> Vec<f64>,
1024 n: usize,
1025 seed: f64,
1026 ) {
1027 let x = spd(n, seed);
1028 let c = sym(n, seed + 10.0); let v = sym(n, seed + 20.0); let grad = backward(&x, &c, n);
1031 let analytic = frob(&grad, &v);
1032 let h = 1e-6;
1033 let xp: Vec<f64> = x.iter().zip(&v).map(|(a, b)| a + h * b).collect();
1034 let xm: Vec<f64> = x.iter().zip(&v).map(|(a, b)| a - h * b).collect();
1035 let fd = (frob(&c, &layer(&xp, n)) - frob(&c, &layer(&xm, n))) / (2.0 * h);
1036 let err = (analytic - fd).abs() / (fd.abs() + 1e-6);
1037 assert!(
1038 err < 1e-4,
1039 "spectral backward FD mismatch: analytic {analytic}, fd {fd}, rel {err}"
1040 );
1041 }
1042
1043 #[test]
1044 fn logeig_backward_matches_fd() {
1045 fd_spectral_check(
1046 |x, n| logeig(x, n, 1e-12),
1047 |x, dy, n| logeig_backward(x, dy, n, 1e-12),
1048 5,
1049 0.7,
1050 );
1051 }
1052
1053 #[test]
1054 fn reeig_backward_matches_fd() {
1055 let eps = 3.0;
1059 fd_spectral_check(
1060 move |x, n| reeig(x, n, eps),
1061 move |x, dy, n| reeig_backward(x, dy, n, eps),
1062 5,
1063 0.42,
1064 );
1065 }
1066
1067 #[test]
1068 fn spd_bn_transport_normalizes() {
1069 let n = 3;
1072 let batch = 4;
1073 let mut x = vec![0f64; batch * n * n];
1074 let mut slices = Vec::new();
1075 for bi in 0..batch {
1076 let s = spd(n, bi as f64 * 0.9 + 0.2);
1077 x[bi * n * n..(bi + 1) * n * n].copy_from_slice(&s);
1078 slices.push(s);
1079 }
1080 let mean = karcher_mean(&slices, n, 50, 1e-12);
1081 let mut g = vec![0f64; n * n];
1082 for i in 0..n {
1083 g[i * n + i] = 1.0;
1084 }
1085 let y = spd_bn_transport(&x, &mean, &g, batch, n, 1e-12);
1086 let yslices: Vec<Vec<f64>> = (0..batch)
1087 .map(|bi| y[bi * n * n..(bi + 1) * n * n].to_vec())
1088 .collect();
1089 let ymean = karcher_mean(&yslices, n, 50, 1e-12);
1090 let mut ident = vec![0f64; n * n];
1092 for i in 0..n {
1093 ident[i * n + i] = 1.0;
1094 }
1095 let err = ymean
1096 .iter()
1097 .zip(&ident)
1098 .map(|(a, b)| (a - b).abs())
1099 .fold(0.0, f64::max);
1100 assert!(err < 1e-4, "SPD-BN did not normalize to I: err {err}");
1101 }
1102
1103 #[test]
1104 fn spd_bn_backward_x_matches_fd() {
1105 let n = 3;
1106 let batch = 2;
1107 let mean = spd(n, 5.0);
1108 let g = spd(n, 6.0);
1109 let mut x = vec![0f64; batch * n * n];
1110 for bi in 0..batch {
1111 let s = spd(n, bi as f64 + 0.3);
1112 x[bi * n * n..(bi + 1) * n * n].copy_from_slice(&s);
1113 }
1114 let mut c = vec![0f64; batch * n * n];
1116 for bi in 0..batch {
1117 let s = sym(n, bi as f64 + 7.0);
1118 c[bi * n * n..(bi + 1) * n * n].copy_from_slice(&s);
1119 }
1120 let eps = 1e-12;
1121 let grad = spd_bn_backward_x(&mean, &g, &c, batch, n, eps);
1122 let mut v = vec![0f64; batch * n * n];
1124 for bi in 0..batch {
1125 let s = sym(n, bi as f64 + 8.0);
1126 v[bi * n * n..(bi + 1) * n * n].copy_from_slice(&s);
1127 }
1128 let analytic = frob(&grad, &v);
1129 let h = 1e-6;
1130 let xp: Vec<f64> = x.iter().zip(&v).map(|(a, b)| a + h * b).collect();
1131 let xm: Vec<f64> = x.iter().zip(&v).map(|(a, b)| a - h * b).collect();
1132 let fd = (frob(&c, &spd_bn_transport(&xp, &mean, &g, batch, n, eps))
1133 - frob(&c, &spd_bn_transport(&xm, &mean, &g, batch, n, eps)))
1134 / (2.0 * h);
1135 let err = (analytic - fd).abs() / (fd.abs() + 1e-6);
1136 assert!(err < 1e-4, "SPD-BN dX FD mismatch: {analytic} vs {fd}");
1137 }
1138
1139 #[test]
1140 fn spd_bn_backward_g_matches_fd() {
1141 let n = 3;
1142 let batch = 2;
1143 let mean = spd(n, 5.0);
1144 let g = spd(n, 6.0);
1145 let mut x = vec![0f64; batch * n * n];
1146 for bi in 0..batch {
1147 let s = spd(n, bi as f64 + 0.3);
1148 x[bi * n * n..(bi + 1) * n * n].copy_from_slice(&s);
1149 }
1150 let mut c = vec![0f64; batch * n * n];
1151 for bi in 0..batch {
1152 let s = sym(n, bi as f64 + 7.0);
1153 c[bi * n * n..(bi + 1) * n * n].copy_from_slice(&s);
1154 }
1155 let eps = 1e-12;
1156 let grad = spd_bn_backward_g(&x, &mean, &g, &c, batch, n, eps);
1157 let vg = sym(n, 9.0); let analytic = frob(&grad, &vg);
1159 let h = 1e-6;
1160 let gp: Vec<f64> = g.iter().zip(&vg).map(|(a, b)| a + h * b).collect();
1161 let gm: Vec<f64> = g.iter().zip(&vg).map(|(a, b)| a - h * b).collect();
1162 let fd = (frob(&c, &spd_bn_transport(&x, &mean, &gp, batch, n, eps))
1163 - frob(&c, &spd_bn_transport(&x, &mean, &gm, batch, n, eps)))
1164 / (2.0 * h);
1165 let err = (analytic - fd).abs() / (fd.abs() + 1e-6);
1166 assert!(err < 1e-4, "SPD-BN dG FD mismatch: {analytic} vs {fd}");
1167 }
1168
1169 #[test]
1170 fn spectral_backward_handles_degenerate_eigenvalues() {
1171 let n = 3;
1176 let vv = [1.0, 0.5, -0.3];
1177 let mut x = vec![0f64; n * n];
1178 for i in 0..n {
1179 for j in 0..n {
1180 let d = if i == j { 2.0 } else { 0.0 };
1181 x[i * n + j] = d + vv[i] * vv[j];
1182 }
1183 }
1184 let c = sym(n, 3.0);
1185 let dir = sym(n, 4.0);
1186 let grad = logeig_backward(&x, &c, n, 1e-12);
1187 let analytic = frob(&grad, &dir);
1188 let h = 1e-6;
1189 let xp: Vec<f64> = x.iter().zip(&dir).map(|(a, b)| a + h * b).collect();
1190 let xm: Vec<f64> = x.iter().zip(&dir).map(|(a, b)| a - h * b).collect();
1191 let fd = (frob(&c, &logeig(&xp, n, 1e-12)) - frob(&c, &logeig(&xm, n, 1e-12))) / (2.0 * h);
1192 let err = (analytic - fd).abs() / (fd.abs() + 1e-6);
1193 assert!(
1194 err < 1e-4,
1195 "degenerate spectral backward FD: analytic {analytic} vs fd {fd}"
1196 );
1197 }
1198
1199 #[test]
1200 fn geodesic_interp_endpoints_and_midpoint() {
1201 let n = 3;
1202 let a = spd(n, 1.0);
1203 let b = spd(n, 2.0);
1204 let maxdiff = |p: &[f64], q: &[f64]| {
1205 p.iter()
1206 .zip(q)
1207 .map(|(x, y)| (x - y).abs())
1208 .fold(0.0, f64::max)
1209 };
1210 assert!(maxdiff(&geodesic_interp(&a, &b, 0.0, n), &a) < 1e-8);
1212 assert!(maxdiff(&geodesic_interp(&a, &b, 1.0, n), &b) < 1e-8);
1213 let mid = geodesic_interp(&a, &b, 0.5, n);
1215 let (ev, _) = eigh(&mid, n);
1216 assert!(
1217 ev.iter().all(|&l| l > 1e-9),
1218 "geodesic midpoint not SPD: {ev:?}"
1219 );
1220 }
1221
1222 fn maxerr(a: &[f64], b: &[f64]) -> f64 {
1225 a.iter()
1226 .zip(b)
1227 .map(|(x, y)| (x - y).abs())
1228 .fold(0.0, f64::max)
1229 }
1230
1231 fn asymmetry(a: &[f64], n: usize) -> f64 {
1233 let mut m = 0.0f64;
1234 for i in 0..n {
1235 for j in 0..n {
1236 m = m.max((a[i * n + j] - a[j * n + i]).abs());
1237 }
1238 }
1239 m
1240 }
1241
1242 fn airm_inner(p: &[f64], u: &[f64], v: &[f64], n: usize) -> f64 {
1244 let pinv = matrix_fn(p, n, |l| 1.0 / l.max(1e-12));
1245 let m = matmul(&matmul(&pinv, u, n), &pinv, n); let mut tr = 0.0;
1247 for i in 0..n {
1248 for j in 0..n {
1249 tr += m[i * n + j] * v[j * n + i];
1250 }
1251 }
1252 tr
1253 }
1254
1255 #[test]
1256 fn karcher_mean_weighted_uniform_matches_unweighted() {
1257 let n = 3;
1258 let covs = vec![spd(n, 0.2), spd(n, 1.3), spd(n, 2.7), spd(n, 3.1)];
1259 let unw = karcher_mean(&covs, n, 60, 1e-13);
1260 let wtd = karcher_mean_weighted(&covs, &[1.0; 4], n, 60, 1e-13);
1261 assert!(maxerr(&unw, &wtd) < 1e-9, "uniform weighted != unweighted");
1262 let scaled = karcher_mean_weighted(&covs, &[7.5; 4], n, 60, 1e-13);
1264 assert!(maxerr(&wtd, &scaled) < 1e-12, "weight scale changed result");
1265 }
1266
1267 #[test]
1268 fn karcher_mean_weighted_dominant_weight_selects_matrix() {
1269 let n = 3;
1270 let covs = vec![spd(n, 0.5), spd(n, 1.5), spd(n, 2.5)];
1271 let m = karcher_mean_weighted(&covs, &[1e-9, 1.0, 1e-9], n, 80, 1e-14);
1273 assert!(
1274 maxerr(&covs[1], &m) < 1e-6,
1275 "dominant-weight barycentre should ≈ that cov"
1276 );
1277 }
1278
1279 #[test]
1280 fn karcher_mean_weighted_is_stationary() {
1281 let n = 4;
1285 let covs = vec![
1286 spd(n, 0.3),
1287 spd(n, 1.1),
1288 spd(n, 2.2),
1289 spd(n, 3.9),
1290 spd(n, 5.0),
1291 ];
1292 let w = [0.4, 0.1, 0.25, 0.05, 0.2];
1293 let m = karcher_mean_weighted(&covs, &w, n, 200, 1e-15);
1294 let wsum: f64 = w.iter().sum();
1295 let mut g = vec![0f64; n * n];
1296 for (c, &wi) in covs.iter().zip(&w) {
1297 let l = log_map(&m, c, n);
1298 for k in 0..n * n {
1299 g[k] += (wi / wsum) * l[k];
1300 }
1301 }
1302 let gnorm: f64 = g.iter().map(|x| x * x).sum::<f64>().sqrt();
1303 assert!(
1304 gnorm < 1e-6,
1305 "weighted barycentre not stationary: |g|={gnorm}"
1306 );
1307 }
1308
1309 #[test]
1310 fn log_exp_map_roundtrip_and_identity_base() {
1311 let n = 4;
1312 let base = spd(n, 0.7);
1313 let x = spd(n, 2.4);
1314 let v = log_map(&base, &x, n);
1316 let back = exp_map(&base, &v, n);
1317 assert!(maxerr(&x, &back) < 1e-8, "exp∘log roundtrip");
1318 assert!(asymmetry(&v, n) < 1e-10, "log_map output not symmetric");
1320 let i = ident(n);
1322 assert!(
1323 maxerr(&log_map(&i, &x, n), &logm(&x, n)) < 1e-9,
1324 "log_map(I, ·) != logm"
1325 );
1326 let s = sym(n, 3.0);
1327 assert!(
1328 maxerr(&exp_map(&i, &s, n), &expm(&s, n)) < 1e-9,
1329 "exp_map(I, ·) != expm"
1330 );
1331 }
1332
1333 #[test]
1334 fn exp_log_maps_reproduce_geodesic() {
1335 let n = 3;
1337 let a = spd(n, 1.0);
1338 let b = spd(n, 2.6);
1339 let v = log_map(&a, &b, n);
1340 for &t in &[0.0, 0.25, 0.5, 0.9, 1.0] {
1341 let tv: Vec<f64> = v.iter().map(|x| x * t).collect();
1342 let via_exp = exp_map(&a, &tv, n);
1343 let gi = geodesic_interp(&a, &b, t, n);
1344 assert!(
1345 maxerr(&via_exp, &gi) < 1e-8,
1346 "exp_map(t·log) != geodesic_interp at t={t}"
1347 );
1348 }
1349 }
1350
1351 #[test]
1352 fn parallel_transport_is_airm_isometry() {
1353 let n = 4;
1354 let p = spd(n, 0.9);
1355 let q = spd(n, 3.3);
1356 let v = sym(n, 1.2);
1357 let w = sym(n, 2.4);
1358 let gv = parallel_transport(&p, &q, &v, n);
1359 let gw = parallel_transport(&p, &q, &w, n);
1360 let before = airm_inner(&p, &v, &w, n);
1362 let after = airm_inner(&q, &gv, &gw, n);
1363 let rel = (before - after).abs() / (before.abs() + 1e-9);
1364 assert!(
1365 rel < 1e-7,
1366 "transport not an isometry: ⟨v,w⟩_P={before} vs ⟨Γv,Γw⟩_Q={after}"
1367 );
1368 assert!(asymmetry(&gv, n) < 1e-9, "transported vector not symmetric");
1370 let same = parallel_transport(&p, &p, &v, n);
1371 assert!(maxerr(&same, &v) < 1e-8, "transport P→P must be identity");
1372 }
1373
1374 #[test]
1375 fn batched_matrix_fns_match_scalar() {
1376 let n = 3;
1377 let covs: Vec<Vec<f64>> = (0..6).map(|i| spd(n, i as f64 * 0.6 + 0.1)).collect();
1378 let syms: Vec<Vec<f64>> = (0..6).map(|i| sym(n, i as f64 * 0.4 + 0.2)).collect();
1379 let lb = logm_batch(&covs, n);
1380 let sb = sqrtm_batch(&covs, n);
1381 let ib = invsqrtm_batch(&covs, n);
1382 for (i, c) in covs.iter().enumerate() {
1383 assert!(maxerr(&lb[i], &logm(c, n)) < 1e-12, "logm_batch[{i}]");
1384 assert!(maxerr(&sb[i], &sqrtm(c, n)) < 1e-12, "sqrtm_batch[{i}]");
1385 assert!(
1386 maxerr(&ib[i], &invsqrtm(c, n)) < 1e-12,
1387 "invsqrtm_batch[{i}]"
1388 );
1389 }
1390 let eb = expm_batch(&syms, n);
1391 for (i, s) in syms.iter().enumerate() {
1392 assert!(maxerr(&eb[i], &expm(s, n)) < 1e-12, "expm_batch[{i}]");
1393 }
1394 assert!(logm_batch(&[], n).is_empty());
1396 }
1397
1398 fn fd_dir_check(
1402 grad: &[f64],
1403 dir: &[f64],
1404 c: &[f64],
1405 forward: impl Fn(&[f64]) -> Vec<f64>,
1406 at: &[f64],
1407 name: &str,
1408 ) {
1409 let h = 1e-6;
1410 let xp: Vec<f64> = at.iter().zip(dir).map(|(a, b)| a + h * b).collect();
1411 let xm: Vec<f64> = at.iter().zip(dir).map(|(a, b)| a - h * b).collect();
1412 let fd = (frob(c, &forward(&xp)) - frob(c, &forward(&xm))) / (2.0 * h);
1413 let an = frob(grad, dir);
1414 assert!(
1415 (an - fd).abs() / (fd.abs() + 1e-6) < 1e-4,
1416 "{name}: analytic {an} vs fd {fd}"
1417 );
1418 }
1419
1420 #[test]
1421 fn log_map_backward_matches_fd() {
1422 let n = 3;
1423 let base = spd(n, 0.7);
1424 let x = spd(n, 2.1);
1425 let c = sym(n, 3.0); let (d_base, d_x) = log_map_backward(&base, &x, &c, n);
1427 fd_dir_check(
1428 &d_x,
1429 &sym(n, 4.0),
1430 &c,
1431 |x| log_map(&base, x, n),
1432 &x,
1433 "log_map d_x",
1434 );
1435 fd_dir_check(
1436 &d_base,
1437 &sym(n, 5.0),
1438 &c,
1439 |b| log_map(b, &x, n),
1440 &base,
1441 "log_map d_base",
1442 );
1443 }
1444
1445 #[test]
1446 fn exp_map_backward_matches_fd() {
1447 let n = 3;
1448 let base = spd(n, 0.8);
1449 let v = sym(n, 1.4); let c = sym(n, 3.2);
1451 let (d_base, d_v) = exp_map_backward(&base, &v, &c, n);
1452 fd_dir_check(
1453 &d_v,
1454 &sym(n, 4.1),
1455 &c,
1456 |v| exp_map(&base, v, n),
1457 &v,
1458 "exp_map d_v",
1459 );
1460 fd_dir_check(
1461 &d_base,
1462 &sym(n, 5.1),
1463 &c,
1464 |b| exp_map(b, &v, n),
1465 &base,
1466 "exp_map d_base",
1467 );
1468 }
1469
1470 #[test]
1471 fn parallel_transport_backward_matches_fd() {
1472 let n = 3;
1473 let from = spd(n, 0.6);
1474 let to = spd(n, 1.9);
1475 let v = sym(n, 2.5);
1476 let c = sym(n, 3.3);
1477 let (d_from, d_to, d_v) = parallel_transport_backward(&from, &to, &v, &c, n);
1478 fd_dir_check(
1479 &d_from,
1480 &sym(n, 4.0),
1481 &c,
1482 |p| parallel_transport(p, &to, &v, n),
1483 &from,
1484 "transport d_from",
1485 );
1486 fd_dir_check(
1487 &d_to,
1488 &sym(n, 5.0),
1489 &c,
1490 |q| parallel_transport(&from, q, &v, n),
1491 &to,
1492 "transport d_to",
1493 );
1494 fd_dir_check(
1495 &d_v,
1496 &sym(n, 6.0),
1497 &c,
1498 |vv| parallel_transport(&from, &to, vv, n),
1499 &v,
1500 "transport d_v",
1501 );
1502 }
1503
1504 #[test]
1505 fn matrix_fn_batch_backward_matches_fd() {
1506 let n = 3;
1507 let batch = 3;
1508 let mut x = vec![0.0; batch * n * n];
1509 let mut c = vec![0.0; batch * n * n];
1510 for bi in 0..batch {
1511 x[bi * n * n..(bi + 1) * n * n].copy_from_slice(&spd(n, bi as f64 * 0.5 + 0.2));
1512 c[bi * n * n..(bi + 1) * n * n].copy_from_slice(&sym(n, bi as f64 + 3.0));
1513 }
1514 let grad =
1516 matrix_fn_batch_backward(&x, &c, n, |l| l.max(1e-12).ln(), |l| 1.0 / l.max(1e-12));
1517 let mut dir = vec![0.0; batch * n * n];
1518 for bi in 0..batch {
1519 dir[bi * n * n..(bi + 1) * n * n].copy_from_slice(&sym(n, bi as f64 + 7.0));
1520 }
1521 let logm_flat = |xx: &[f64]| {
1522 let covs: Vec<Vec<f64>> = (0..batch)
1523 .map(|bi| xx[bi * n * n..(bi + 1) * n * n].to_vec())
1524 .collect();
1525 logm_batch(&covs, n).concat()
1526 };
1527 fd_dir_check(
1528 &grad,
1529 &dir,
1530 &c,
1531 logm_flat,
1532 &x,
1533 "matrix_fn_batch(logm) backward",
1534 );
1535 }
1536
1537 fn spd_sep(n: usize) -> Vec<f64> {
1542 let mut a = vec![0f64; n * n];
1543 for i in 0..n {
1544 for j in i..n {
1545 let v = if i == j {
1546 (i as f64 + 1.0) * 2.0
1547 } else {
1548 0.05 * ((i + j) as f64).cos()
1549 };
1550 a[i * n + j] = v;
1551 a[j * n + i] = v;
1552 }
1553 }
1554 a
1555 }
1556
1557 #[test]
1558 fn eigh_packed_reconstructs() {
1559 let n = 4;
1560 let a = spd_sep(n);
1561 let fwd = eigh_packed(&a, n);
1562 let (lam, u) = (&fwd[..n], &fwd[n..]);
1563 let mut recon = vec![0f64; n * n];
1565 for i in 0..n {
1566 for j in 0..n {
1567 let mut s = 0.0;
1568 for k in 0..n {
1569 s += u[i * n + k] * lam[k] * u[j * n + k];
1570 }
1571 recon[i * n + j] = s;
1572 }
1573 }
1574 assert!(maxerr(&recon, &a) < 1e-9, "eigh_packed A = U Λ Uᵀ failed");
1575 assert!(lam.windows(2).all(|w| w[0] <= w[1] + 1e-12));
1577 }
1578
1579 #[test]
1580 fn eigh_backward_matches_fd() {
1581 let n = 4;
1582 let a = spd_sep(n);
1583 let fwd = eigh_packed(&a, n);
1584 let u_base = fwd[n..].to_vec();
1585 let lbar: Vec<f64> = (0..n).map(|i| ((i as f64 + 1.0) * 0.7).sin()).collect();
1587 let ubar = sym(n, 5.0);
1588 let mut bar = vec![0f64; n + n * n];
1589 bar[..n].copy_from_slice(&lbar);
1590 bar[n..].copy_from_slice(&ubar);
1591 let abar = eigh_backward_packed(&fwd, &bar, n);
1592 let loss = |mat: &[f64]| -> f64 {
1595 let fw = eigh_packed(mat, n);
1596 let (l, mut uu) = (fw[..n].to_vec(), fw[n..].to_vec());
1597 for j in 0..n {
1598 let mut dot = 0.0;
1599 for i in 0..n {
1600 dot += uu[i * n + j] * u_base[i * n + j];
1601 }
1602 if dot < 0.0 {
1603 for i in 0..n {
1604 uu[i * n + j] = -uu[i * n + j];
1605 }
1606 }
1607 }
1608 let mut s = 0.0;
1609 for i in 0..n {
1610 s += lbar[i] * l[i];
1611 }
1612 for k in 0..n * n {
1613 s += ubar[k] * uu[k];
1614 }
1615 s
1616 };
1617 let dir = sym(n, 4.0);
1619 let h = 1e-6;
1620 let ap: Vec<f64> = a.iter().zip(&dir).map(|(x, y)| x + h * y).collect();
1621 let am: Vec<f64> = a.iter().zip(&dir).map(|(x, y)| x - h * y).collect();
1622 let fd = (loss(&ap) - loss(&am)) / (2.0 * h);
1623 let an = frob(&abar, &dir);
1624 assert!(
1625 (an - fd).abs() / (fd.abs() + 1e-6) < 1e-4,
1626 "eigh backward FD: analytic {an} vs fd {fd}"
1627 );
1628 }
1629
1630 #[test]
1631 fn eigh_batch_packed_matches_scalar() {
1632 let n = 3;
1633 let mats: Vec<Vec<f64>> = (0..4).map(|i| spd(n, i as f64 * 0.4 + 0.2)).collect();
1634 let batched = eigh_batch_packed(&mats, n);
1635 for (i, m) in mats.iter().enumerate() {
1636 assert!(
1637 maxerr(&batched[i], &eigh_packed(m, n)) < 1e-12,
1638 "eigh_batch[{i}]"
1639 );
1640 }
1641 }
1642}