use crate::frames::GrassmannFrame;
use gam_linalg::faer_ndarray::{FaerCholesky, FaerSvd};
use ndarray::{Array2, ArrayView2, ArrayViewMut2};
pub(super) const STORED_FRAME_RESOLUTION: f64 = f32::EPSILON as f64;
pub(super) fn stored_projector_distance(
current: ArrayView2<'_, f32>,
next: ArrayView2<'_, f32>,
) -> Result<f64, String> {
if current
.iter()
.zip(next.iter())
.all(|(left, right)| left == right)
{
return Ok(0.0);
}
let u = current.t().mapv(f64::from);
let v = next.t().mapv(f64::from);
let gram_u = u.t().dot(&u);
let gram_v = v.t().dot(&v);
let scale = gram_u
.iter()
.chain(gram_v.iter())
.map(|value| value * value)
.sum::<f64>();
if scale == 0.0 {
return Ok(0.0);
}
let (left, _, right_t) = u
.t()
.dot(&v)
.svd(true, true)
.map_err(|error| format!("frame projector alignment: {error}"))?;
let left = left.ok_or("frame projector alignment omitted the left factor")?;
let right_t = right_t.ok_or("frame projector alignment omitted the right factor")?;
let aligned = v.dot(&right_t.t().dot(&left.t()));
let difference = &aligned - &u;
let midpoint = (&aligned + &u) * 0.5;
let gram_midpoint = midpoint.t().dot(&midpoint);
let gram_difference = difference.t().dot(&difference);
let mixed = midpoint.t().dot(&difference);
let mut distance_sq = 0.0;
for i in 0..mixed.nrows() {
for j in 0..mixed.ncols() {
distance_sq += 2.0
* (gram_midpoint[[i, j]] * gram_difference[[j, i]] + mixed[[i, j]] * mixed[[j, i]]);
}
}
Ok((distance_sq.max(0.0) / scale).sqrt())
}
pub(super) fn polar_tied_frame_step(
current: ArrayView2<'_, f32>,
mut action: ArrayViewMut2<'_, f64>,
code_second: ArrayView2<'_, f64>,
normal_multiplier: f64,
shift: f64,
mut proposal: ArrayViewMut2<'_, f32>,
) -> Result<f64, String> {
let (b, p) = current.dim();
if action.iter().all(|&value| value == 0.0) {
proposal.assign(¤t);
return Ok(0.0);
}
let gram = Array2::from_shape_fn((b, b), |(axis, column)| {
(0..p)
.map(|feature| current[[axis, feature]] as f64 * current[[column, feature]] as f64)
.sum::<f64>()
});
let normal_rhs = Array2::from_shape_fn((b, b), |(axis, column)| {
(0..p)
.map(|feature| current[[axis, feature]] as f64 * action[[feature, column]])
.sum::<f64>()
});
let normal = gram
.cholesky(faer::Side::Lower)
.map_err(|error| format!("tied frame projector Gram factorization: {error}"))?
.solve_mat(&normal_rhs);
let mut tangent_sq = 0.0;
let mut conditional_sq = 0.0;
for feature in 0..p {
for column in 0..b {
let mut projected = 0.0;
let mut normal_correction = 0.0;
for axis in 0..b {
let direction = current[[axis, feature]] as f64;
projected += direction * normal[[axis, column]];
normal_correction += direction * code_second[[axis, column]];
}
tangent_sq += (action[[feature, column]] - projected).powi(2);
conditional_sq +=
(action[[feature, column]] - normal_multiplier * normal_correction).powi(2);
action[[feature, column]] += shift * current[[column, feature]] as f64;
}
}
let stationarity = if conditional_sq == 0.0 {
0.0
} else {
(tangent_sq / conditional_sq).sqrt()
};
if action.iter().all(|&value| value == 0.0) {
proposal.assign(¤t);
return Ok(stationarity);
}
let frame = GrassmannFrame::polar_update(action.view())?;
let u = frame.frame();
for column in 0..b {
let alignment = (0..p)
.map(|feature| u[[feature, column]] * action[[feature, column]])
.sum::<f64>();
let orientation = if alignment < 0.0 { -1.0 } else { 1.0 };
for feature in 0..p {
proposal[[column, feature]] = (orientation * u[[feature, column]]) as f32;
}
}
Ok(stationarity)
}