1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
2
3use crate::manifold::SaeManifoldTerm;
4use gam_linalg::faer_ndarray::{FaerSvd, fast_ab, fast_abt, fast_atb};
5
6pub fn canonical_output_subspace_rows(raw: ArrayView2<'_, f64>) -> Result<Array2<f64>, String> {
15 let (r, p) = raw.dim();
16 if r == 0 || p == 0 {
17 return Err("canonical_output_subspace_rows: frame must be non-empty".to_string());
18 }
19 if r > p {
20 return Err(format!(
21 "canonical_output_subspace_rows: rank r={r} cannot exceed output dimension p={p}"
22 ));
23 }
24 if raw.iter().any(|value| !value.is_finite()) {
25 return Err("canonical_output_subspace_rows: frame must be finite".to_string());
26 }
27
28 let frame = GrassmannFrame::polar_update(raw.t())?;
29 let singular_values = frame.gauge_singular_values();
30 let rank_resolution = f64::EPSILON * r.max(p) as f64 * singular_values[0];
31 if singular_values[r - 1] <= rank_resolution {
32 return Err(format!(
33 "canonical_output_subspace_rows: requested {r}-row frame is rank deficient \
34 (sigma_min={}, numerical resolution={rank_resolution})",
35 singular_values[r - 1]
36 ));
37 }
38 Ok(frame.frame().t().to_owned())
39}
40
41#[derive(Clone, Debug)]
42pub(crate) struct FrameProjection {
43 pub(crate) p: usize,
44 pub(crate) beta_offsets: Vec<usize>,
45 pub(crate) border_offsets: Vec<usize>,
46 pub(crate) basis_sizes: Vec<usize>,
47 pub(crate) ranks: Vec<usize>,
48 frames: Vec<Option<Array2<f64>>>,
49}
50
51impl FrameProjection {
52 pub(crate) fn new(term: &SaeManifoldTerm) -> Self {
53 Self {
54 p: term.output_dim(),
55 beta_offsets: term.beta_offsets(),
56 border_offsets: term.factored_border_offsets(),
57 basis_sizes: term.atoms.iter().map(|atom| atom.basis_size()).collect(),
58 ranks: term
59 .atoms
60 .iter()
61 .map(|atom| atom.border_frame_rank())
62 .collect(),
63 frames: term
64 .atoms
65 .iter()
66 .map(|atom| {
67 atom.decoder_frame
68 .as_ref()
69 .map(|frame| frame.frame().to_owned())
70 })
71 .collect(),
72 }
73 }
74
75 pub(crate) fn beta_dim(&self) -> usize {
76 self.basis_sizes.iter().sum::<usize>() * self.p
77 }
78
79 pub(crate) fn frames_owned(&self) -> Vec<Option<Array2<f64>>> {
83 self.frames.clone()
84 }
85
86 pub(crate) fn border_dim(&self) -> usize {
87 self.basis_sizes
88 .iter()
89 .zip(&self.ranks)
90 .map(|(m, r)| m * r)
91 .sum()
92 }
93
94 pub(crate) fn lift_border_vec(&self, border: ArrayView1<'_, f64>) -> Array1<f64> {
95 let mut out = Array1::<f64>::zeros(self.beta_dim());
96 for atom in 0..self.basis_sizes.len() {
97 self.lift_atom_vec_into(atom, border, out.view_mut());
98 }
99 out
100 }
101
102 pub(crate) fn project_border_vec(&self, beta: ArrayView1<'_, f64>) -> Array1<f64> {
103 let mut out = Array1::<f64>::zeros(self.border_dim());
104 for atom in 0..self.basis_sizes.len() {
105 self.project_atom_vec_into(atom, beta, out.view_mut(), 1.0);
106 }
107 out
108 }
109
110 pub(crate) fn lift_block(&self, atom: usize, block: ArrayView2<'_, f64>) -> Array2<f64> {
111 let m = self.basis_sizes[atom];
112 let r = self.ranks[atom];
113 if self.frames[atom].is_none() {
114 return block.to_owned();
115 }
116 let uk = self.frames[atom].as_ref().expect("framed atom has a frame");
117 let mut out = Array2::<f64>::zeros((m * self.p, m * self.p));
118 for b1 in 0..m {
119 for b2 in 0..m {
120 for c1 in 0..self.p {
121 for c2 in 0..self.p {
122 let mut acc = 0.0;
123 for j1 in 0..r {
124 for j2 in 0..r {
125 acc +=
126 uk[[c1, j1]] * block[[b1 * r + j1, b2 * r + j2]] * uk[[c2, j2]];
127 }
128 }
129 out[[b1 * self.p + c1, b2 * self.p + c2]] = acc;
130 }
131 }
132 }
133 }
134 out
135 }
136
137 pub(crate) fn project_block(&self, hbb: ArrayView2<'_, f64>) -> Array2<f64> {
138 let t = self.project_rows(hbb);
139 let mut out = Array2::<f64>::zeros((self.border_dim(), self.border_dim()));
140 for atom in 0..self.basis_sizes.len() {
141 self.project_block_left_atom(atom, t.view(), out.view_mut());
142 }
143 out
144 }
145
146 pub(crate) fn project_rows(&self, block: ArrayView2<'_, f64>) -> Array2<f64> {
147 let mut out = Array2::<f64>::zeros((block.nrows(), self.border_dim()));
148 for row in 0..block.nrows() {
149 let projected = self.project_border_vec(block.row(row));
150 out.row_mut(row).assign(&projected);
151 }
152 out
153 }
154
155 pub(crate) fn atom_border_range(&self, atom: usize) -> std::ops::Range<usize> {
156 let start = self.border_offsets[atom];
157 start..start + self.basis_sizes[atom] * self.ranks[atom]
158 }
159
160 pub(crate) fn lift_axis_into(
161 &self,
162 out: &mut Array1<f64>,
163 atom: usize,
164 basis_col: usize,
165 frame_col: usize,
166 ) {
167 let base = self.beta_offsets[atom] + basis_col * self.p;
168 match &self.frames[atom] {
169 None => out[base + frame_col] = 1.0,
170 Some(uk) => {
171 for out_col in 0..self.p {
172 out[base + out_col] = uk[[out_col, frame_col]];
173 }
174 }
175 }
176 }
177
178 pub(crate) fn lift_local_axis_into(
179 &self,
180 out: &mut Array1<f64>,
181 atom: usize,
182 basis_col: usize,
183 frame_col: usize,
184 ) {
185 let base = basis_col * self.p;
186 match &self.frames[atom] {
187 None => out[base + frame_col] = 1.0,
188 Some(uk) => {
189 for out_col in 0..self.p {
190 out[base + out_col] = uk[[out_col, frame_col]];
191 }
192 }
193 }
194 }
195
196 pub(crate) fn project_atom_vec_into(
197 &self,
198 atom: usize,
199 beta: ArrayView1<'_, f64>,
200 mut out: ndarray::ArrayViewMut1<'_, f64>,
201 scale: f64,
202 ) {
203 let m = self.basis_sizes[atom];
204 let r = self.ranks[atom];
205 let ob = self.beta_offsets[atom];
206 let oc = self.border_offsets[atom];
207 for basis_col in 0..m {
208 let base_b = ob + basis_col * self.p;
209 let base_c = oc + basis_col * r;
210 match &self.frames[atom] {
211 None => {
212 for j in 0..r {
213 out[base_c + j] += scale * beta[base_b + j];
214 }
215 }
216 Some(uk) => {
217 for j in 0..r {
218 let mut acc = 0.0;
219 for i in 0..self.p {
220 acc += uk[[i, j]] * beta[base_b + i];
221 }
222 out[base_c + j] += scale * acc;
223 }
224 }
225 }
226 }
227 }
228
229 pub(crate) fn project_local_atom_vec_into(
230 &self,
231 atom: usize,
232 beta: ArrayView1<'_, f64>,
233 out: ndarray::ArrayViewMut1<'_, f64>,
234 scale: f64,
235 ) {
236 self.project_atom_vec_into_with_base(atom, beta, out, scale, 0);
237 }
238
239 pub(crate) fn project_atom_vec_into_with_base(
240 &self,
241 atom: usize,
242 beta: ArrayView1<'_, f64>,
243 mut out: ndarray::ArrayViewMut1<'_, f64>,
244 scale: f64,
245 beta_base_offset: usize,
246 ) {
247 let m = self.basis_sizes[atom];
248 let r = self.ranks[atom];
249 let oc = self.border_offsets[atom];
250 for basis_col in 0..m {
251 let base_b = beta_base_offset + basis_col * self.p;
252 let base_c = oc + basis_col * r;
253 match &self.frames[atom] {
254 None => {
255 for j in 0..r {
256 out[base_c + j] += scale * beta[base_b + j];
257 }
258 }
259 Some(uk) => {
260 for j in 0..r {
261 let mut acc = 0.0;
262 for i in 0..self.p {
263 acc += uk[[i, j]] * beta[base_b + i];
264 }
265 out[base_c + j] += scale * acc;
266 }
267 }
268 }
269 }
270 }
271
272 pub(crate) fn lift_atom_vec_into(
273 &self,
274 atom: usize,
275 border: ArrayView1<'_, f64>,
276 mut out: ndarray::ArrayViewMut1<'_, f64>,
277 ) {
278 let m = self.basis_sizes[atom];
279 let r = self.ranks[atom];
280 let ob = self.beta_offsets[atom];
281 let oc = self.border_offsets[atom];
282 for basis_col in 0..m {
283 let base_b = ob + basis_col * self.p;
284 let base_c = oc + basis_col * r;
285 match &self.frames[atom] {
286 None => {
287 for i in 0..self.p {
288 out[base_b + i] = border[base_c + i];
289 }
290 }
291 Some(uk) => {
292 for i in 0..self.p {
293 let mut acc = 0.0;
294 for j in 0..r {
295 acc += uk[[i, j]] * border[base_c + j];
296 }
297 out[base_b + i] = acc;
298 }
299 }
300 }
301 }
302 }
303
304 pub(crate) fn accumulate_output_project(
305 &self,
306 atom: usize,
307 c_base: usize,
308 output: usize,
309 value: f64,
310 out: &mut [f64],
311 ) {
312 match &self.frames[atom] {
313 None => out[c_base + output] += value,
314 Some(uk) => {
315 let rank = self.ranks[atom];
316 let frame_row = uk.row(output);
317 let frame_slice = frame_row.as_slice().expect("frame rows are contiguous");
318 let out_slice = &mut out[c_base..c_base + rank];
319 for (slot, &u) in out_slice.iter_mut().zip(frame_slice.iter()) {
320 *slot += value * u;
321 }
322 }
323 }
324 }
325
326 pub(crate) fn project_jacobian_rows(
333 &self,
334 atom: usize,
335 jac: ArrayView2<'_, f64>,
336 ) -> Option<Array2<f64>> {
337 self.frames[atom].as_ref().map(|uk| fast_ab(&jac, uk))
338 }
339
340 pub(crate) fn output_variance(
341 &self,
342 atom: usize,
343 cov_c: ArrayView2<'_, f64>,
344 basis: ArrayView1<'_, f64>,
345 output: usize,
346 ) -> f64 {
347 let Some(uk) = &self.frames[atom] else {
348 return self.full_output_variance(atom, cov_c, basis, output);
349 };
350 let m = self.basis_sizes[atom];
351 let r = self.ranks[atom];
352 let mut var = 0.0;
353 for b1 in 0..m {
354 let phi1 = basis[b1];
355 if phi1 == 0.0 {
356 continue;
357 }
358 for b2 in 0..m {
359 let phi2 = basis[b2];
360 if phi2 == 0.0 {
361 continue;
362 }
363 for j1 in 0..r {
364 for j2 in 0..r {
365 var += phi1
366 * phi2
367 * uk[[output, j1]]
368 * cov_c[[b1 * r + j1, b2 * r + j2]]
369 * uk[[output, j2]];
370 }
371 }
372 }
373 }
374 var
375 }
376
377 pub(crate) fn full_output_variance(
378 &self,
379 atom: usize,
380 cov: ArrayView2<'_, f64>,
381 basis: ArrayView1<'_, f64>,
382 output: usize,
383 ) -> f64 {
384 let m = self.basis_sizes[atom];
385 let mut var = 0.0;
386 for b1 in 0..m {
387 let phi1 = basis[b1];
388 if phi1 == 0.0 {
389 continue;
390 }
391 for b2 in 0..m {
392 var += phi1 * basis[b2] * cov[[b1 * self.p + output, b2 * self.p + output]];
393 }
394 }
395 var
396 }
397
398 pub(crate) fn project_block_left_atom(
399 &self,
400 atom: usize,
401 t: ArrayView2<'_, f64>,
402 mut out: ndarray::ArrayViewMut2<'_, f64>,
403 ) {
404 let m = self.basis_sizes[atom];
405 let r = self.ranks[atom];
406 let ob = self.beta_offsets[atom];
407 let oc = self.border_offsets[atom];
408 for basis_col in 0..m {
409 let base_b = ob + basis_col * self.p;
410 let base_c = oc + basis_col * r;
411 match &self.frames[atom] {
412 None => {
413 for j in 0..r {
414 for c in 0..out.ncols() {
415 out[[base_c + j, c]] += t[[base_b + j, c]];
416 }
417 }
418 }
419 Some(uk) => {
420 for j in 0..r {
421 for c in 0..out.ncols() {
422 let mut acc = 0.0;
423 for i in 0..self.p {
424 acc += uk[[i, j]] * t[[base_b + i, c]];
425 }
426 out[[base_c + j, c]] += acc;
427 }
428 }
429 }
430 }
431 }
432 }
433}
434
435pub(crate) struct FramedDeviceArgs<'a> {
446 pub p: usize,
447 pub border_dim: usize,
448 pub border_offsets: &'a [usize],
449 pub ranks: &'a [usize],
450 pub basis_sizes: &'a [usize],
451 pub smooth_scaled_s: &'a [Array2<f64>],
452 pub frame_blocks: Vec<gam_solve::arrow_schur::FactoredFrameGBlock>,
453 pub rows: &'a [gam_solve::arrow_schur::ArrowRowBlock],
454}
455
456pub(crate) fn build_framed_device_sae_data(
457 args: FramedDeviceArgs<'_>,
458) -> gam_solve::arrow_schur::DeviceSaePcgData {
459 use gam_solve::arrow_schur::{DeviceSaeFrameData, DeviceSaePcgData, DeviceSaeSmoothBlock};
460 let FramedDeviceArgs {
461 p,
462 border_dim,
463 border_offsets,
464 ranks,
465 basis_sizes,
466 smooth_scaled_s,
467 frame_blocks,
468 rows,
469 } = args;
470 let n_atoms = ranks.len();
471 let mut smooth_blocks = Vec::with_capacity(n_atoms);
472 let mut smooth_ranks = Vec::with_capacity(n_atoms);
473 for k in 0..n_atoms {
474 smooth_blocks.push(DeviceSaeSmoothBlock {
475 global_offset: border_offsets[k],
476 factor_a: smooth_scaled_s[k].clone(),
477 });
478 smooth_ranks.push(ranks[k]);
479 }
480 let row_htbeta: Vec<Vec<f64>> = rows
481 .iter()
482 .map(|row| {
483 let (qi, w) = row.htbeta.dim();
484 if w != border_dim {
485 return Vec::new();
486 }
487 let mut flat = vec![0.0_f64; qi * w];
488 for c in 0..qi {
489 for a in 0..w {
490 flat[c * w + a] = row.htbeta[[c, a]];
491 }
492 }
493 flat
494 })
495 .collect();
496 DeviceSaePcgData {
497 p,
498 beta_dim: border_dim,
499 a_phi: std::sync::Arc::from(Vec::new().into_boxed_slice()),
502 local_jac: std::sync::Arc::from(Vec::new().into_boxed_slice()),
503 smooth_blocks,
504 sparse_g_blocks: Vec::new(),
505 frame: Some(DeviceSaeFrameData {
506 ranks: ranks.to_vec(),
507 basis_sizes: basis_sizes.to_vec(),
508 border_offsets: border_offsets.to_vec(),
509 frame_blocks,
510 smooth_ranks,
511 row_htbeta,
512 }),
513 }
514}
515
516pub(crate) fn build_framed_device_sae_data_reusing(
523 args: FramedDeviceArgs<'_>,
524 recycled: Option<std::sync::Arc<gam_solve::arrow_schur::DeviceSaePcgData>>,
525) -> std::sync::Arc<gam_solve::arrow_schur::DeviceSaePcgData> {
526 if let Some(mut allocation) = recycled {
527 if let Some(data) = std::sync::Arc::get_mut(&mut allocation) {
528 if data.frame.is_some() {
529 refresh_framed_device_sae_data(data, args);
530 return allocation;
531 }
532 }
533 return std::sync::Arc::new(build_framed_device_sae_data(args));
534 }
535 std::sync::Arc::new(build_framed_device_sae_data(args))
536}
537
538fn refresh_framed_device_sae_data(
539 data: &mut gam_solve::arrow_schur::DeviceSaePcgData,
540 args: FramedDeviceArgs<'_>,
541) {
542 let FramedDeviceArgs {
543 p,
544 border_dim,
545 border_offsets,
546 ranks,
547 basis_sizes,
548 smooth_scaled_s,
549 frame_blocks,
550 rows,
551 } = args;
552 data.p = p;
553 data.beta_dim = border_dim;
554 if !data.a_phi.is_empty() {
555 data.a_phi = std::sync::Arc::from(Vec::new().into_boxed_slice());
556 }
557 if !data.local_jac.is_empty() {
558 data.local_jac = std::sync::Arc::from(Vec::new().into_boxed_slice());
559 }
560 if data.smooth_blocks.len() == smooth_scaled_s.len() {
561 for (atom_idx, (block, source)) in data
562 .smooth_blocks
563 .iter_mut()
564 .zip(smooth_scaled_s)
565 .enumerate()
566 {
567 block.global_offset = border_offsets[atom_idx];
568 block.factor_a.clone_from(source);
569 }
570 } else {
571 data.smooth_blocks = smooth_scaled_s
572 .iter()
573 .enumerate()
574 .map(
575 |(atom_idx, source)| gam_solve::arrow_schur::DeviceSaeSmoothBlock {
576 global_offset: border_offsets[atom_idx],
577 factor_a: source.clone(),
578 },
579 )
580 .collect();
581 }
582 data.sparse_g_blocks.clear();
583
584 let frame = data
585 .frame
586 .as_mut()
587 .expect("framed descriptor checked before refresh");
588 frame.ranks.clear();
589 frame.ranks.extend_from_slice(ranks);
590 frame.basis_sizes.clear();
591 frame.basis_sizes.extend_from_slice(basis_sizes);
592 frame.border_offsets.clear();
593 frame.border_offsets.extend_from_slice(border_offsets);
594 let stable_frame_block_layout = frame.frame_blocks.len() == frame_blocks.len()
595 && frame
596 .frame_blocks
597 .iter()
598 .zip(&frame_blocks)
599 .all(|(current, source)| {
600 current.g.dim() == source.g.dim() && current.w.dim() == source.w.dim()
601 });
602 if stable_frame_block_layout {
603 for (current, source) in frame.frame_blocks.iter_mut().zip(frame_blocks) {
604 current.atom_i = source.atom_i;
605 current.atom_j = source.atom_j;
606 current.g.assign(&source.g);
607 current.w.assign(&source.w);
608 }
609 } else {
610 frame.frame_blocks = frame_blocks;
611 }
612 frame.smooth_ranks.clear();
613 frame.smooth_ranks.extend_from_slice(ranks);
614 frame.row_htbeta.resize_with(rows.len(), Vec::new);
615 for (target, row) in frame.row_htbeta.iter_mut().zip(rows) {
616 let (qi, width) = row.htbeta.dim();
617 if width != border_dim {
618 target.clear();
619 continue;
620 }
621 target.resize(qi * width, 0.0);
622 if let Some(source) = row.htbeta.as_slice() {
623 target.copy_from_slice(source);
624 } else {
625 for c in 0..qi {
626 for a in 0..width {
627 target[c * width + a] = row.htbeta[[c, a]];
628 }
629 }
630 }
631 }
632}
633
634pub(crate) const SAE_FRAME_RANK_CUTOFF: f64 = 1.0e-7;
639
640pub(crate) const SAE_FRAME_MIN_AUTO_OUTPUT_DIM: usize = 12;
644
645pub(crate) const SAE_FRAME_ACTIVATION_MARGIN: f64 = 0.25;
654
655#[derive(Debug, Clone)]
674pub struct GrassmannFrame {
675 frame: Array2<f64>,
677 gauge_singular_values: Array1<f64>,
680}
681
682impl GrassmannFrame {
683 pub fn output_dim(&self) -> usize {
685 self.frame.nrows()
686 }
687
688 pub fn rank(&self) -> usize {
690 self.frame.ncols()
691 }
692
693 pub fn gauge_singular_values(&self) -> &Array1<f64> {
698 &self.gauge_singular_values
699 }
700
701 pub fn frame(&self) -> ArrayView2<'_, f64> {
703 self.frame.view()
704 }
705
706 pub fn from_orthonormal(
713 frame: Array2<f64>,
714 gauge_singular_values: Array1<f64>,
715 ) -> Result<Self, String> {
716 let (p, r) = frame.dim();
717 if p == 0 || r == 0 {
718 return Err("GrassmannFrame::from_orthonormal: frame must be non-empty".to_string());
719 }
720 if r > p {
721 return Err(format!(
722 "GrassmannFrame::from_orthonormal: frame rank r={r} cannot exceed output dim p={p}"
723 ));
724 }
725 if gauge_singular_values.len() != r {
726 return Err(format!(
727 "GrassmannFrame::from_orthonormal: gauge length {} must equal rank {r}",
728 gauge_singular_values.len()
729 ));
730 }
731 for i in 0..r {
732 let value = gauge_singular_values[i];
733 if !(value.is_finite() && value >= 0.0) {
734 return Err(format!(
735 "GrassmannFrame::from_orthonormal: gauge value {i} must be finite and non-negative, got {value}"
736 ));
737 }
738 if i > 0 && gauge_singular_values[i - 1] < value {
739 return Err(
740 "GrassmannFrame::from_orthonormal: gauge values must be descending".to_string(),
741 );
742 }
743 }
744 let tol = 1.0e-8_f64;
745 for a in 0..r {
746 for b in a..r {
747 let mut dot = 0.0_f64;
748 for row in 0..p {
749 dot += frame[[row, a]] * frame[[row, b]];
750 }
751 let target = if a == b { 1.0 } else { 0.0 };
752 if (dot - target).abs() > tol {
753 return Err(format!(
754 "GrassmannFrame::from_orthonormal: frame columns are not orthonormal at ({a}, {b}); dot={dot}"
755 ));
756 }
757 }
758 }
759 Ok(Self::from_oriented(frame, gauge_singular_values))
760 }
761
762 pub fn manifold_dimension(&self) -> usize {
767 let r = self.rank();
768 let p = self.output_dim();
769 r * (p - r)
770 }
771
772 pub(crate) fn from_oriented(
778 mut frame: Array2<f64>,
779 gauge_singular_values: Array1<f64>,
780 ) -> Self {
781 let (p, r) = frame.dim();
782 for col in 0..r {
783 let mut pivot_abs = 0.0_f64;
786 let mut pivot_val = 0.0_f64;
787 for row in 0..p {
788 let v = frame[[row, col]];
789 if v.abs() > pivot_abs {
790 pivot_abs = v.abs();
791 pivot_val = v;
792 }
793 }
794 if pivot_val < 0.0 {
795 for row in 0..p {
796 frame[[row, col]] = -frame[[row, col]];
797 }
798 }
799 }
800 Self {
801 frame,
802 gauge_singular_values,
803 }
804 }
805
806 pub fn polar_update(cross_moment: ArrayView2<'_, f64>) -> Result<Self, String> {
817 let (p, r) = cross_moment.dim();
818 if p == 0 || r == 0 {
819 return Err("GrassmannFrame::polar_update: cross-moment must be non-empty".into());
820 }
821 if r > p {
822 return Err(format!(
823 "GrassmannFrame::polar_update: frame rank r={r} cannot exceed output dim p={p}"
824 ));
825 }
826 let owned = cross_moment.to_owned();
827 let (u_opt, sv, vt_opt) = owned
828 .svd(true, true)
829 .map_err(|e| format!("GrassmannFrame::polar_update: SVD failed: {e}"))?;
830 let w = u_opt.ok_or_else(|| {
831 "GrassmannFrame::polar_update: thin SVD returned no left factor".to_string()
832 })?;
833 let vt = vt_opt.ok_or_else(|| {
834 "GrassmannFrame::polar_update: thin SVD returned no right factor".to_string()
835 })?;
836 let polar = fast_ab(&w, &vt);
839 Ok(Self::from_oriented(polar, sv))
840 }
841
842 pub fn reconstruct_decoder(&self, coords: ArrayView2<'_, f64>) -> Result<Array2<f64>, String> {
847 if coords.ncols() != self.rank() {
848 return Err(format!(
849 "GrassmannFrame::reconstruct_decoder: coord cols {} must equal frame rank {}",
850 coords.ncols(),
851 self.rank()
852 ));
853 }
854 Ok(fast_abt(&coords.to_owned(), &self.frame))
855 }
856
857 pub fn project_decoder(&self, decoder: ArrayView2<'_, f64>) -> Result<Array2<f64>, String> {
864 if decoder.ncols() != self.output_dim() {
865 return Err(format!(
866 "GrassmannFrame::project_decoder: decoder cols {} must equal output dim {}",
867 decoder.ncols(),
868 self.output_dim()
869 ));
870 }
871 Ok(fast_ab(&decoder.to_owned(), &self.frame))
872 }
873
874 pub fn max_principal_angle(&self, other: ArrayView2<'_, f64>) -> Result<f64, String> {
896 if other.nrows() != self.output_dim() {
897 return Err(format!(
898 "GrassmannFrame::max_principal_angle: other rows {} must equal output dim {}",
899 other.nrows(),
900 self.output_dim()
901 ));
902 }
903 if other.ncols() != self.rank() {
904 return Ok(std::f64::consts::FRAC_PI_2);
910 }
911 let other_owned = other.to_owned();
912 let overlap = fast_atb(&self.frame, &other_owned);
913 let (_u, sv_cos, _vt) = overlap
914 .svd(false, false)
915 .map_err(|e| format!("GrassmannFrame::max_principal_angle: cos-SVD failed: {e}"))?;
916 let u_overlap = fast_ab(&self.frame, &overlap);
918 let v_perp = &other_owned - &u_overlap;
919 let (_u, sv_sin, _vt) = v_perp
920 .svd(false, false)
921 .map_err(|e| format!("GrassmannFrame::max_principal_angle: sin-SVD failed: {e}"))?;
922 let min_cos = sv_cos
928 .iter()
929 .copied()
930 .fold(1.0_f64, f64::min)
931 .clamp(0.0, 1.0);
932 let max_sin = sv_sin
933 .iter()
934 .copied()
935 .fold(0.0_f64, f64::max)
936 .clamp(0.0, 1.0);
937 Ok(max_sin.atan2(min_cos))
938 }
939
940 pub fn from_decoder_row_space(decoder: ArrayView2<'_, f64>) -> Option<Self> {
957 let (m, p) = decoder.dim();
958 if m == 0 || p == 0 {
959 return None;
960 }
961 let (_u, sv, vt_opt) = decoder.to_owned().svd(false, true).ok()?;
965 let vt = vt_opt?;
966 let sigma_max = sv.iter().copied().fold(0.0_f64, f64::max);
967 if !(sigma_max.is_finite() && sigma_max > 0.0) {
968 return None;
969 }
970 let cutoff = SAE_FRAME_RANK_CUTOFF * sigma_max;
971 let r = sv.iter().take(vt.nrows()).filter(|&&s| s >= cutoff).count();
974 if r == 0 {
975 return None;
976 }
977 let mut frame = Array2::<f64>::zeros((p, r));
981 for j in 0..r {
982 for row in 0..p {
983 frame[[row, j]] = vt[[j, row]];
984 }
985 }
986 let gauge = Array1::from_iter(sv.iter().take(r).copied());
987 Some(Self::from_oriented(frame, gauge))
988 }
989}
990
991#[derive(Debug, Clone)]
997pub struct GrassmannCrossMoment {
998 moment: Array2<f64>,
999}
1000
1001impl GrassmannCrossMoment {
1002 pub fn new(output_dim: usize, rank: usize) -> Self {
1004 Self {
1005 moment: Array2::<f64>::zeros((output_dim, rank)),
1006 }
1007 }
1008
1009 pub fn accumulate(
1013 &mut self,
1014 targets: ArrayView2<'_, f64>,
1015 coords: ArrayView2<'_, f64>,
1016 ) -> Result<(), String> {
1017 if targets.ncols() != self.moment.nrows() || coords.ncols() != self.moment.ncols() {
1018 return Err(format!(
1019 "GrassmannCrossMoment::accumulate: expected targets (·,{}) and coords (·,{}); \
1020 got (·,{}) and (·,{})",
1021 self.moment.nrows(),
1022 self.moment.ncols(),
1023 targets.ncols(),
1024 coords.ncols()
1025 ));
1026 }
1027 if targets.nrows() != coords.nrows() {
1028 return Err(format!(
1029 "GrassmannCrossMoment::accumulate: targets rows {} must equal coords rows {}",
1030 targets.nrows(),
1031 coords.nrows()
1032 ));
1033 }
1034 let block = fast_atb(&targets.to_owned(), &coords.to_owned());
1035 self.moment += █
1036 Ok(())
1037 }
1038
1039 pub fn moment(&self) -> ArrayView2<'_, f64> {
1041 self.moment.view()
1042 }
1043
1044 pub fn polar_frame(&self) -> Result<GrassmannFrame, String> {
1047 GrassmannFrame::polar_update(self.moment.view())
1048 }
1049}
1050
1051pub fn grassmann_recover_planted_span_angle(
1063 targets: ArrayView2<'_, f64>,
1064 coords: ArrayView2<'_, f64>,
1065 planted: ArrayView2<'_, f64>,
1066) -> Result<f64, String> {
1067 let p = targets.ncols();
1068 let r = coords.ncols();
1069 if planted.dim() != (p, r) {
1070 return Err(format!(
1071 "grassmann_recover_planted_span_angle: planted frame must be ({p}, {r}); got {:?}",
1072 planted.dim()
1073 ));
1074 }
1075 let mut cross = GrassmannCrossMoment::new(p, r);
1076 cross.accumulate(targets, coords)?;
1077 let frame = cross.polar_frame()?;
1078 frame.max_principal_angle(planted)
1079}
1080
1081pub fn grassmann_assert_border_dim_invariant(term: &SaeManifoldTerm) -> Result<(), String> {
1086 let expected: usize = term
1087 .atoms
1088 .iter()
1089 .map(|a| a.basis_size() * a.border_frame_rank())
1090 .sum();
1091 let got = term.factored_border_dim();
1092 if got != expected {
1093 return Err(format!(
1094 "grassmann border-dim invariant violated: factored_border_dim() = {got}, \
1095 expected Σ M_k·r_k = {expected}"
1096 ));
1097 }
1098 Ok(())
1099}