use ndarray::{Array2, Array3, Array4, Array5, ArrayView2};
use std::sync::Arc;
pub trait SaeBasisEvaluator: Send + Sync + std::fmt::Debug {
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String>;
fn evaluate_into(
&self,
phi: &mut Array2<f64>,
jet: &mut Array3<f64>,
coords: ArrayView2<'_, f64>,
) -> Result<(), String> {
let (new_phi, new_jet) = self.evaluate(coords)?;
if new_phi.dim() != phi.dim() {
return Err(format!(
"SaeBasisEvaluator::evaluate_into: evaluator returned Φ {:?}, target buffer {:?}",
new_phi.dim(),
phi.dim()
));
}
if new_jet.dim() != jet.dim() {
return Err(format!(
"SaeBasisEvaluator::evaluate_into: evaluator returned jet {:?}, target buffer {:?}",
new_jet.dim(),
jet.dim()
));
}
phi.assign(&new_phi);
jet.assign(&new_jet);
Ok(())
}
fn affine_transformed_evaluator(
&self,
shift: &[f64],
scale: &[f64],
n_basis: usize,
) -> Result<Option<Arc<dyn SaeBasisSecondJet>>, String> {
if shift.len() == usize::MAX || scale.len() == usize::MAX || n_basis == usize::MAX {
return Err("SaeBasisEvaluator::affine_transformed_evaluator: unreachable affine metadata width".to_string());
}
Ok(None)
}
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
Ok(PhiEtaSplit::all_base(n_basis))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
None
}
fn evaluate_phi_eta(
&self,
coords: ArrayView2<'_, f64>,
eta: f64,
) -> Result<PhiEtaEvaluation, String> {
if !(eta.is_finite() && (0.0..=1.0).contains(&eta)) {
return Err(format!(
"SaeBasisEvaluator::evaluate_phi_eta: eta must be finite in [0, 1]; got {eta}"
));
}
let (mut phi, mut jet) = self.evaluate(coords)?;
let split = self.phi_eta_split(phi.ncols())?;
let mut dphi_deta = Array2::<f64>::zeros(phi.dim());
let mut djet_deta = Array3::<f64>::zeros(jet.dim());
for &col in &split.curved_cols {
if col >= phi.ncols() {
return Err(format!(
"SaeBasisEvaluator::evaluate_phi_eta: curved column {col} exceeds basis width {}",
phi.ncols()
));
}
for row in 0..phi.nrows() {
dphi_deta[[row, col]] = phi[[row, col]];
if eta != 1.0 {
phi[[row, col]] *= eta;
}
for axis in 0..jet.shape()[2] {
djet_deta[[row, col, axis]] = jet[[row, col, axis]];
if eta != 1.0 {
jet[[row, col, axis]] *= eta;
}
}
}
}
Ok(PhiEtaEvaluation {
phi,
jet,
dphi_deta,
djet_deta,
split,
})
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>>;
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhiEtaSplit {
pub base_cols: Vec<usize>,
pub curved_cols: Vec<usize>,
}
impl PhiEtaSplit {
pub fn all_base(n_basis: usize) -> Self {
Self {
base_cols: (0..n_basis).collect(),
curved_cols: Vec::new(),
}
}
fn from_curved_mask(mask: Vec<bool>) -> Self {
let mut base_cols = Vec::new();
let mut curved_cols = Vec::new();
for (col, curved) in mask.into_iter().enumerate() {
if curved {
curved_cols.push(col);
} else {
base_cols.push(col);
}
}
Self {
base_cols,
curved_cols,
}
}
}
#[derive(Debug, Clone)]
pub struct PhiEtaEvaluation {
pub phi: Array2<f64>,
pub jet: Array3<f64>,
pub dphi_deta: Array2<f64>,
pub djet_deta: Array3<f64>,
pub split: PhiEtaSplit,
}
fn monomial_linear_mask(dimension: usize, max_total_degree: usize) -> Vec<bool> {
gam_terms::basis::monomial_exponents(dimension, max_total_degree)
.iter()
.map(|alpha| alpha.iter().sum::<usize>() <= 1)
.collect()
}
fn duchon_effective_order_for_eta(
centers: ArrayView2<'_, f64>,
order: gam_terms::basis::DuchonNullspaceOrder,
) -> gam_terms::basis::DuchonNullspaceOrder {
let mut effective = order;
while effective != gam_terms::basis::DuchonNullspaceOrder::Zero
&& centers.nrows() <= duchon_polynomial_column_count(centers.ncols(), effective)
{
effective = match effective {
gam_terms::basis::DuchonNullspaceOrder::Zero => {
gam_terms::basis::DuchonNullspaceOrder::Zero
}
gam_terms::basis::DuchonNullspaceOrder::Linear => {
gam_terms::basis::DuchonNullspaceOrder::Zero
}
gam_terms::basis::DuchonNullspaceOrder::Degree(2) => {
gam_terms::basis::DuchonNullspaceOrder::Linear
}
gam_terms::basis::DuchonNullspaceOrder::Degree(k) => {
gam_terms::basis::DuchonNullspaceOrder::Degree(k - 1)
}
};
}
effective
}
fn duchon_polynomial_column_count(
dimension: usize,
order: gam_terms::basis::DuchonNullspaceOrder,
) -> usize {
match order {
gam_terms::basis::DuchonNullspaceOrder::Zero => 1,
gam_terms::basis::DuchonNullspaceOrder::Linear => dimension + 1,
gam_terms::basis::DuchonNullspaceOrder::Degree(degree) => {
gam_terms::basis::monomial_exponents(dimension, degree).len()
}
}
}
pub trait SaeBasisSecondJet: SaeBasisEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String>;
}
pub trait SaeBasisThirdJet: SaeBasisSecondJet {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String>;
}
#[derive(Debug, Clone)]
pub struct PeriodicHarmonicEvaluator {
pub num_basis: usize,
}
impl PeriodicHarmonicEvaluator {
pub fn new(num_basis: usize) -> Result<Self, String> {
if num_basis == 0 || num_basis % 2 == 0 {
return Err(format!(
"PeriodicHarmonicEvaluator requires odd num_basis >= 1; got {num_basis}"
));
}
Ok(Self { num_basis })
}
}
impl SaeBasisEvaluator for PeriodicHarmonicEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
if n_basis != self.num_basis {
return Err(format!(
"PeriodicHarmonicEvaluator::phi_eta_split: n_basis {n_basis} != evaluator width {}",
self.num_basis
));
}
let mut curved = vec![false; n_basis];
for h in 2..=(n_basis - 1) / 2 {
curved[2 * h - 1] = true;
curved[2 * h] = true;
}
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
let n = coords.nrows();
let m = self.num_basis;
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, 1));
self.evaluate_into(&mut phi, &mut jet, coords)?;
Ok((phi, jet))
}
fn evaluate_into(
&self,
phi: &mut Array2<f64>,
jet: &mut Array3<f64>,
coords: ArrayView2<'_, f64>,
) -> Result<(), String> {
let n = coords.nrows();
let d = coords.ncols();
if d != 1 {
return Err(format!(
"PeriodicHarmonicEvaluator: expected latent_dim == 1, got {d}"
));
}
let m = self.num_basis;
if phi.dim() != (n, m) {
return Err(format!(
"PeriodicHarmonicEvaluator::evaluate_into: Φ buffer {:?} != ({n}, {m})",
phi.dim()
));
}
if jet.dim() != (n, m, 1) {
return Err(format!(
"PeriodicHarmonicEvaluator::evaluate_into: jet buffer {:?} != ({n}, {m}, 1)",
jet.dim()
));
}
let num_harmonics = (m - 1) / 2;
let two_pi = 2.0 * std::f64::consts::PI;
phi.fill(0.0);
jet.fill(0.0);
for row in 0..n {
let t = coords[[row, 0]];
phi[[row, 0]] = 1.0;
for h in 1..=num_harmonics {
let angle = two_pi * (h as f64) * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * h - 1;
let c_idx = 2 * h;
phi[[row, s_idx]] = s;
phi[[row, c_idx]] = c;
jet[[row, s_idx, 0]] = two_pi * (h as f64) * c;
jet[[row, c_idx, 0]] = -two_pi * (h as f64) * s;
}
}
Ok(())
}
}
impl SaeBasisSecondJet for PeriodicHarmonicEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
let n = coords.nrows();
let d = coords.ncols();
if d != 1 {
return Err(format!(
"PeriodicHarmonicEvaluator::second_jet: expected latent_dim == 1, got {d}"
));
}
let m = self.num_basis;
let num_harmonics = (m - 1) / 2;
let two_pi = 2.0 * std::f64::consts::PI;
let mut h = Array4::<f64>::zeros((n, m, 1, 1));
for row in 0..n {
let t = coords[[row, 0]];
for k in 1..=num_harmonics {
let freq = two_pi * (k as f64);
let freq2 = freq * freq;
let angle = freq * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * k - 1;
let c_idx = 2 * k;
h[[row, s_idx, 0, 0]] = -freq2 * s;
h[[row, c_idx, 0, 0]] = -freq2 * c;
}
}
Ok(h)
}
}
impl SaeBasisThirdJet for PeriodicHarmonicEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
let n = coords.nrows();
let d = coords.ncols();
if d != 1 {
return Err(format!(
"PeriodicHarmonicEvaluator::third_jet: expected latent_dim == 1, got {d}"
));
}
let m = self.num_basis;
let num_harmonics = (m - 1) / 2;
let two_pi = 2.0 * std::f64::consts::PI;
let mut t3 = Array5::<f64>::zeros((n, m, 1, 1, 1));
for row in 0..n {
let t = coords[[row, 0]];
for k in 1..=num_harmonics {
let freq = two_pi * (k as f64);
let freq3 = freq * freq * freq;
let angle = freq * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * k - 1;
let c_idx = 2 * k;
t3[[row, s_idx, 0, 0, 0]] = -freq3 * c;
t3[[row, c_idx, 0, 0, 0]] = freq3 * s;
}
}
Ok(t3)
}
}
#[derive(Debug, Clone)]
pub struct RawPeriodicCircleEvaluator {
pub latent_dim: usize,
}
impl RawPeriodicCircleEvaluator {
pub fn new(latent_dim: usize) -> Result<Self, String> {
if latent_dim == 0 {
return Err("RawPeriodicCircleEvaluator requires latent_dim >= 1".to_string());
}
Ok(Self { latent_dim })
}
}
impl SaeBasisEvaluator for RawPeriodicCircleEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
if n_basis != 2 {
return Err(format!(
"RawPeriodicCircleEvaluator::phi_eta_split: n_basis {n_basis} != 2"
));
}
Ok(PhiEtaSplit::all_base(n_basis))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
if coords.ncols() != self.latent_dim {
return Some(Err(format!(
"RawPeriodicCircleEvaluator::second_jet_dyn: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
)));
}
None
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
if coords.ncols() != self.latent_dim {
return Some(Err(format!(
"RawPeriodicCircleEvaluator::third_jet_dyn: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
)));
}
None
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
if coords.ncols() != self.latent_dim {
return Err(format!(
"RawPeriodicCircleEvaluator: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let n = coords.nrows();
let mut phi = Array2::<f64>::zeros((n, 2));
let mut jet = Array3::<f64>::zeros((n, 2, self.latent_dim));
for row in 0..n {
let t = coords[[row, 0]];
phi[[row, 0]] = t.cos();
phi[[row, 1]] = t.sin();
jet[[row, 0, 0]] = -t.sin();
jet[[row, 1, 0]] = t.cos();
}
Ok((phi, jet))
}
}
#[derive(Debug, Clone)]
struct SphHarmonicColumn {
degree: usize,
m: i64,
am: usize,
norm: f64,
assoc: Vec<f64>,
curved: bool,
}
#[derive(Debug, Clone)]
pub struct SphericalHarmonicEvaluator {
degree: usize,
columns: Vec<SphHarmonicColumn>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SphericalHarmonicMode {
pub degree: usize,
pub order: i64,
pub laplace_eigenvalue: f64,
pub l2_gram_weight: f64,
}
fn legendre_polynomials(degree: usize) -> Vec<Vec<f64>> {
let mut polys: Vec<Vec<f64>> = Vec::with_capacity(degree + 1);
polys.push(vec![1.0]);
if degree >= 1 {
polys.push(vec![0.0, 1.0]);
}
for l in 2..=degree {
let prev = &polys[l - 1];
let prev2 = &polys[l - 2];
let mut next = vec![0.0_f64; l + 1];
for (k, &coeff) in prev.iter().enumerate() {
next[k + 1] += (2 * l - 1) as f64 * coeff;
}
for (k, &coeff) in prev2.iter().enumerate() {
next[k] -= (l - 1) as f64 * coeff;
}
let inv = 1.0 / l as f64;
for c in next.iter_mut() {
*c *= inv;
}
polys.push(next);
}
polys
}
fn polynomial_derivative(coeffs: &[f64], order: usize) -> Vec<f64> {
let mut c = coeffs.to_vec();
for _ in 0..order {
if c.len() <= 1 {
return vec![0.0];
}
c = (1..c.len()).map(|k| k as f64 * c[k]).collect();
}
c
}
fn spherical_harmonic_norm(l: usize, m: i64) -> f64 {
let am = m.unsigned_abs() as usize;
let mut ratio = 1.0_f64; for k in (l - am + 1)..=(l + am) {
ratio /= k as f64;
}
let base = ((2 * l + 1) as f64 / (4.0 * std::f64::consts::PI) * ratio).sqrt();
if m == 0 {
base
} else {
base * std::f64::consts::SQRT_2
}
}
#[inline]
fn sph_jet_mul(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
[
a[0] * b[0],
a[1] * b[0] + a[0] * b[1],
a[2] * b[0] + 2.0 * a[1] * b[1] + a[0] * b[2],
a[3] * b[0] + 3.0 * a[2] * b[1] + 3.0 * a[1] * b[2] + a[0] * b[3],
]
}
impl SphericalHarmonicEvaluator {
pub fn new(degree: usize) -> Result<Self, String> {
let side = degree.checked_add(1).ok_or_else(|| {
"SphericalHarmonicEvaluator: basis width overflowed usize".to_string()
})?;
let basis_size = side.checked_mul(side).ok_or_else(|| {
"SphericalHarmonicEvaluator: basis width overflowed usize".to_string()
})?;
let polys = legendre_polynomials(degree);
let mut columns = Vec::with_capacity(basis_size);
for l in 0..=degree {
for m in -(l as i64)..=(l as i64) {
let am = m.unsigned_abs() as usize;
columns.push(SphHarmonicColumn {
degree: l,
m,
am,
norm: spherical_harmonic_norm(l, m),
assoc: polynomial_derivative(&polys[l], am),
curved: l >= 2,
});
}
}
Ok(Self { degree, columns })
}
pub fn degree(&self) -> usize {
self.degree
}
pub fn basis_size(&self) -> usize {
self.columns.len()
}
pub fn spectral_modes(&self) -> Vec<SphericalHarmonicMode> {
self.columns
.iter()
.map(|column| {
let degree = column.degree as f64;
SphericalHarmonicMode {
degree: column.degree,
order: column.m,
laplace_eigenvalue: degree * (degree + 1.0),
l2_gram_weight: 1.0,
}
})
.collect()
}
fn lat_table(&self, col: &SphHarmonicColumn, lat: f64) -> [f64; 4] {
let (s, c) = lat.sin_cos();
let slat = [s, c, -s, -c];
let clat = [c, -s, -c, s];
let mut pow = [1.0, 0.0, 0.0, 0.0];
for _ in 0..col.am {
pow = sph_jet_mul(pow, clat);
}
let mut acc = [0.0, 0.0, 0.0, 0.0];
for &coeff in col.assoc.iter().rev() {
acc = sph_jet_mul(acc, slat);
acc[0] += coeff;
}
let mut r = sph_jet_mul(pow, acc);
for value in r.iter_mut() {
*value *= col.norm;
}
r
}
fn lon_table(m: i64, lon: f64) -> [f64; 4] {
if m == 0 {
return [1.0, 0.0, 0.0, 0.0];
}
let mf = m.unsigned_abs() as f64;
let (s, c) = (mf * lon).sin_cos();
if m > 0 {
[c, -mf * s, -mf * mf * c, mf * mf * mf * s]
} else {
[s, mf * c, -mf * mf * s, -mf * mf * mf * c]
}
}
fn check_coords(&self, coords: ArrayView2<'_, f64>, what: &str) -> Result<(), String> {
if coords.ncols() != 2 {
return Err(format!(
"SphericalHarmonicEvaluator::{what}: expected latent_dim == 2 (lat, lon), got {}",
coords.ncols()
));
}
Ok(())
}
}
impl SaeBasisEvaluator for SphericalHarmonicEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let expected = self.basis_size();
if n_basis != expected {
return Err(format!(
"SphericalHarmonicEvaluator::phi_eta_split: n_basis {n_basis} != evaluator width {expected}"
));
}
let curved = self
.columns
.iter()
.map(|col| col.curved)
.collect::<Vec<_>>();
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
None
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
self.check_coords(coords, "evaluate")?;
let n = coords.nrows();
let m = self.basis_size();
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, 2));
for row in 0..n {
let lat = coords[[row, 0]];
let lon = coords[[row, 1]];
for (col_idx, col) in self.columns.iter().enumerate() {
let r = self.lat_table(col, lat);
let t = Self::lon_table(col.m, lon);
phi[[row, col_idx]] = r[0] * t[0];
jet[[row, col_idx, 0]] = r[1] * t[0];
jet[[row, col_idx, 1]] = r[0] * t[1];
}
}
Ok((phi, jet))
}
}
impl SaeBasisSecondJet for SphericalHarmonicEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
self.check_coords(coords, "second_jet")?;
let n = coords.nrows();
let m = self.basis_size();
let mut h = Array4::<f64>::zeros((n, m, 2, 2));
for row in 0..n {
let lat = coords[[row, 0]];
let lon = coords[[row, 1]];
for (col_idx, col) in self.columns.iter().enumerate() {
let r = self.lat_table(col, lat);
let t = Self::lon_table(col.m, lon);
h[[row, col_idx, 0, 0]] = r[2] * t[0];
h[[row, col_idx, 0, 1]] = r[1] * t[1];
h[[row, col_idx, 1, 0]] = r[1] * t[1];
h[[row, col_idx, 1, 1]] = r[0] * t[2];
}
}
Ok(h)
}
}
impl SaeBasisThirdJet for SphericalHarmonicEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
self.check_coords(coords, "third_jet")?;
let n = coords.nrows();
let m = self.basis_size();
let mut t3 = Array5::<f64>::zeros((n, m, 2, 2, 2));
for row in 0..n {
let lat = coords[[row, 0]];
let lon = coords[[row, 1]];
for (col_idx, col) in self.columns.iter().enumerate() {
let r = self.lat_table(col, lat);
let t = Self::lon_table(col.m, lon);
for axis_a in 0..2 {
for axis_b in 0..2 {
for axis_c in 0..2 {
let n_lat = (axis_a == 0) as usize
+ (axis_b == 0) as usize
+ (axis_c == 0) as usize;
t3[[row, col_idx, axis_a, axis_b, axis_c]] = r[n_lat] * t[3 - n_lat];
}
}
}
}
}
Ok(t3)
}
}
#[derive(Debug, Clone)]
struct AmbientSphereColumn {
degree: usize,
m: i64,
am: usize,
norm: f64,
assoc_derivatives: [Vec<f64>; 4],
curved: bool,
}
#[inline]
fn ambient_complex_power(x: f64, y: f64, k: usize) -> (f64, f64) {
let (mut re, mut im) = (1.0_f64, 0.0_f64);
for _ in 0..k {
let next_re = re * x - im * y;
let next_im = re * y + im * x;
re = next_re;
im = next_im;
}
(re, im)
}
#[inline]
fn ambient_poly_eval(coefficients: &[f64], z: f64) -> f64 {
let mut acc = 0.0_f64;
for &coefficient in coefficients.iter().rev() {
acc = acc * z + coefficient;
}
acc
}
#[inline]
fn ambient_angular_partial(am: usize, sine_phase: bool, x: f64, y: f64, p: usize, q: usize) -> f64 {
let order = p + q;
if order > am {
return 0.0;
}
let mut falling = 1.0_f64;
for step in 0..order {
falling *= (am - step) as f64;
}
let (re, im) = ambient_complex_power(x, y, am - order);
let (re, im) = match q % 4 {
0 => (re, im),
1 => (-im, re),
2 => (-re, -im),
_ => (im, -re),
};
falling * if sine_phase { im } else { re }
}
#[derive(Debug, Clone)]
pub struct AmbientSphereHarmonicEvaluator {
degree: usize,
columns: Vec<AmbientSphereColumn>,
}
impl AmbientSphereHarmonicEvaluator {
pub fn new(degree: usize) -> Result<Self, String> {
let side = degree.checked_add(1).ok_or_else(|| {
"AmbientSphereHarmonicEvaluator: basis width overflowed usize".to_string()
})?;
let basis_size = side.checked_mul(side).ok_or_else(|| {
"AmbientSphereHarmonicEvaluator: basis width overflowed usize".to_string()
})?;
let polys = legendre_polynomials(degree);
let mut columns = Vec::with_capacity(basis_size);
for l in 0..=degree {
for m in -(l as i64)..=(l as i64) {
let am = m.unsigned_abs() as usize;
let assoc = polynomial_derivative(&polys[l], am);
columns.push(AmbientSphereColumn {
degree: l,
m,
am,
norm: spherical_harmonic_norm(l, m),
assoc_derivatives: [
polynomial_derivative(&assoc, 0),
polynomial_derivative(&assoc, 1),
polynomial_derivative(&assoc, 2),
polynomial_derivative(&assoc, 3),
],
curved: l >= 2,
});
}
}
Ok(Self { degree, columns })
}
pub fn degree(&self) -> usize {
self.degree
}
pub fn basis_size(&self) -> usize {
self.columns.len()
}
pub fn column_jet_bound(&self) -> f64 {
self.columns
.iter()
.map(|column| {
let coefficient_norm: f64 = column.assoc_derivatives[0]
.iter()
.map(|coefficient| coefficient.abs())
.sum();
column.norm * coefficient_norm
})
.fold(0.0_f64, f64::max)
}
pub fn spectral_modes(&self) -> Vec<SphericalHarmonicMode> {
self.columns
.iter()
.map(|column| {
let degree = column.degree as f64;
SphericalHarmonicMode {
degree: column.degree,
order: column.m,
laplace_eigenvalue: degree * (degree + 1.0),
l2_gram_weight: 1.0,
}
})
.collect()
}
#[inline]
fn partial(
&self,
column: &AmbientSphereColumn,
x: f64,
y: f64,
z: f64,
p: usize,
q: usize,
r: usize,
) -> f64 {
let radial = ambient_poly_eval(&column.assoc_derivatives[r], z);
if radial == 0.0 {
return 0.0;
}
let angular = ambient_angular_partial(column.am, column.m < 0, x, y, p, q);
column.norm * radial * angular
}
#[inline]
fn axis_counts(axes: &[usize]) -> (usize, usize, usize) {
let mut counts = (0_usize, 0_usize, 0_usize);
for &axis in axes {
match axis {
0 => counts.0 += 1,
1 => counts.1 += 1,
_ => counts.2 += 1,
}
}
counts
}
fn check_coords(&self, coords: ArrayView2<'_, f64>, what: &str) -> Result<(), String> {
if coords.ncols() != 3 {
return Err(format!(
"AmbientSphereHarmonicEvaluator::{what}: expected ambient dim == 3 (x, y, z), got {}",
coords.ncols()
));
}
Ok(())
}
}
impl SaeBasisEvaluator for AmbientSphereHarmonicEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let expected = self.basis_size();
if n_basis != expected {
return Err(format!(
"AmbientSphereHarmonicEvaluator::phi_eta_split: n_basis {n_basis} != evaluator width {expected}"
));
}
let curved = self
.columns
.iter()
.map(|column| column.curved)
.collect::<Vec<_>>();
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
None
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
self.check_coords(coords, "evaluate")?;
let n = coords.nrows();
let m = self.basis_size();
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, 3));
for row in 0..n {
let (x, y, z) = (coords[[row, 0]], coords[[row, 1]], coords[[row, 2]]);
for (col_idx, column) in self.columns.iter().enumerate() {
phi[[row, col_idx]] = self.partial(column, x, y, z, 0, 0, 0);
jet[[row, col_idx, 0]] = self.partial(column, x, y, z, 1, 0, 0);
jet[[row, col_idx, 1]] = self.partial(column, x, y, z, 0, 1, 0);
jet[[row, col_idx, 2]] = self.partial(column, x, y, z, 0, 0, 1);
}
}
Ok((phi, jet))
}
}
impl SaeBasisSecondJet for AmbientSphereHarmonicEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
self.check_coords(coords, "second_jet")?;
let n = coords.nrows();
let m = self.basis_size();
let mut h = Array4::<f64>::zeros((n, m, 3, 3));
for row in 0..n {
let (x, y, z) = (coords[[row, 0]], coords[[row, 1]], coords[[row, 2]]);
for (col_idx, column) in self.columns.iter().enumerate() {
for axis_a in 0..3 {
for axis_b in 0..3 {
let (p, q, r) = Self::axis_counts(&[axis_a, axis_b]);
h[[row, col_idx, axis_a, axis_b]] =
self.partial(column, x, y, z, p, q, r);
}
}
}
}
Ok(h)
}
}
impl SaeBasisThirdJet for AmbientSphereHarmonicEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
self.check_coords(coords, "third_jet")?;
let n = coords.nrows();
let m = self.basis_size();
let mut t3 = Array5::<f64>::zeros((n, m, 3, 3, 3));
for row in 0..n {
let (x, y, z) = (coords[[row, 0]], coords[[row, 1]], coords[[row, 2]]);
for (col_idx, column) in self.columns.iter().enumerate() {
for axis_a in 0..3 {
for axis_b in 0..3 {
for axis_c in 0..3 {
let (p, q, r) = Self::axis_counts(&[axis_a, axis_b, axis_c]);
t3[[row, col_idx, axis_a, axis_b, axis_c]] =
self.partial(column, x, y, z, p, q, r);
}
}
}
}
}
Ok(t3)
}
}
pub fn select_spherical_harmonic_degree(
coords: ArrayView2<'_, f64>,
target: ArrayView2<'_, f64>,
max_degree: usize,
) -> Result<usize, String> {
use faer::Side;
use gam_linalg::faer_ndarray::{FaerCholesky, fast_ata, fast_atb};
if coords.nrows() != target.nrows() {
return Err(format!(
"select_spherical_harmonic_degree: coords rows {} != target rows {}",
coords.nrows(),
target.nrows()
));
}
if max_degree == 0 {
return Ok(0);
}
let evaluator = SphericalHarmonicEvaluator::new(max_degree)?;
let (phi, _) = evaluator.evaluate(coords)?;
let mut gram = fast_ata(&phi);
let scale = gram.diag().iter().copied().fold(0.0_f64, f64::max);
let ridge = if scale > 0.0 { scale } else { 1.0 } * 1e-9;
for d in gram.diag_mut().iter_mut() {
*d += ridge;
}
let rhs = fast_atb(&phi, &target.to_owned());
let decoder = gram
.cholesky(Side::Lower)
.map_err(|err| {
format!("select_spherical_harmonic_degree: gram factorization failed: {err:?}")
})?
.solve_mat(&rhs);
let mut energy = vec![0.0_f64; max_degree + 1];
let mut col = 0usize;
for (l, e) in energy.iter_mut().enumerate() {
let width = 2 * l + 1;
let mut acc = 0.0_f64;
for _ in 0..width {
for out in 0..decoder.ncols() {
acc += decoder[[col, out]] * decoder[[col, out]];
}
col += 1;
}
*e = acc / width as f64;
}
let mut cut = max_degree;
let mut best_ratio = 1.0_f64;
for l in 0..max_degree {
let hi = energy[l];
let lo = energy[l + 1].max(f64::MIN_POSITIVE);
let ratio = hi / lo;
if ratio > best_ratio {
best_ratio = ratio;
cut = l;
}
}
if best_ratio < 100.0 {
cut = max_degree;
}
Ok(cut)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RealHarmonicComponent {
Constant,
Sine { harmonic: usize },
Cosine { harmonic: usize },
}
impl RealHarmonicComponent {
pub fn harmonic(self) -> usize {
match self {
Self::Constant => 0,
Self::Sine { harmonic } | Self::Cosine { harmonic } => harmonic,
}
}
pub fn reflection_sign(self) -> i8 {
match self {
Self::Sine { .. } => -1,
Self::Constant | Self::Cosine { .. } => 1,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TorusHarmonicMode {
pub components: Vec<RealHarmonicComponent>,
pub laplace_eigenvalue: f64,
pub l2_gram_weight: f64,
}
#[derive(Debug, Clone)]
pub struct TorusHarmonicEvaluator {
latent_dim: usize,
num_harmonics: usize,
axis_basis_size: usize,
basis_size: usize,
}
impl TorusHarmonicEvaluator {
pub fn new(latent_dim: usize, num_harmonics: usize) -> Result<Self, String> {
if latent_dim == 0 {
return Err("TorusHarmonicEvaluator requires latent_dim >= 1".to_string());
}
if num_harmonics == 0 {
return Err("TorusHarmonicEvaluator requires num_harmonics >= 1".to_string());
}
let axis_width = num_harmonics
.checked_mul(2)
.and_then(|twice| twice.checked_add(1))
.ok_or_else(|| {
"TorusHarmonicEvaluator: per-axis basis width overflowed usize".to_string()
})?;
let basis_size = (0..latent_dim)
.try_fold(1usize, |width, _| width.checked_mul(axis_width))
.ok_or_else(|| {
"TorusHarmonicEvaluator: tensor basis width overflowed usize".to_string()
})?;
Ok(Self {
latent_dim,
num_harmonics,
axis_basis_size: axis_width,
basis_size,
})
}
pub fn latent_dim(&self) -> usize {
self.latent_dim
}
pub fn num_harmonics(&self) -> usize {
self.num_harmonics
}
pub fn axis_basis_size(&self) -> usize {
self.axis_basis_size
}
pub fn basis_size(&self) -> usize {
self.basis_size
}
pub fn axis_component(axis_column: usize) -> RealHarmonicComponent {
if axis_column == 0 {
RealHarmonicComponent::Constant
} else {
let harmonic = axis_column.div_ceil(2);
if axis_column % 2 == 1 {
RealHarmonicComponent::Sine { harmonic }
} else {
RealHarmonicComponent::Cosine { harmonic }
}
}
}
pub fn spectral_modes(&self) -> Vec<TorusHarmonicMode> {
let axis_m = self.axis_basis_size();
let mut index = vec![0usize; self.latent_dim];
let mut modes = Vec::with_capacity(self.basis_size());
for _ in 0..self.basis_size() {
let components: Vec<RealHarmonicComponent> =
index.iter().copied().map(Self::axis_component).collect();
let laplace_eigenvalue = components
.iter()
.map(|component| {
let harmonic = component.harmonic() as f64;
harmonic * harmonic
})
.sum();
let l2_gram_weight = components.iter().fold(1.0, |weight, component| {
if *component == RealHarmonicComponent::Constant {
weight
} else {
0.5 * weight
}
});
modes.push(TorusHarmonicMode {
components,
laplace_eigenvalue,
l2_gram_weight,
});
for axis in (0..self.latent_dim).rev() {
index[axis] += 1;
if index[axis] < axis_m {
break;
}
index[axis] = 0;
}
}
modes
}
}
impl SaeBasisEvaluator for TorusHarmonicEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let expected = self.basis_size();
if n_basis != expected {
return Err(format!(
"TorusHarmonicEvaluator::phi_eta_split: n_basis {n_basis} != evaluator width {expected}"
));
}
let d = self.latent_dim;
let axis_m = self.axis_basis_size();
let mut curved = Vec::with_capacity(n_basis);
let mut idx = vec![0usize; d];
for _flat in 0..n_basis {
let mut nonconstant_axes = 0usize;
let mut has_higher_harmonic = false;
for &axis_col in &idx {
if axis_col > 0 {
nonconstant_axes += 1;
if axis_col > 2 {
has_higher_harmonic = true;
}
}
}
curved.push(has_higher_harmonic || nonconstant_axes > 1);
for axis in (0..d).rev() {
idx[axis] += 1;
if idx[axis] < axis_m {
break;
}
idx[axis] = 0;
}
}
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
if self.latent_dim == 2 {
let m = self.axis_basis_size();
Some((m, m))
} else {
None
}
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
let n = coords.nrows();
let m = self.basis_size();
let d = self.latent_dim;
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, d));
self.evaluate_into(&mut phi, &mut jet, coords)?;
Ok((phi, jet))
}
fn evaluate_into(
&self,
phi: &mut Array2<f64>,
jet: &mut Array3<f64>,
coords: ArrayView2<'_, f64>,
) -> Result<(), String> {
let d = self.latent_dim;
if coords.ncols() != d {
return Err(format!(
"TorusHarmonicEvaluator: expected latent_dim {d}, got {}",
coords.ncols()
));
}
let n = coords.nrows();
let axis_m = self.axis_basis_size();
let m = self.basis_size();
if phi.dim() != (n, m) {
return Err(format!(
"TorusHarmonicEvaluator::evaluate_into: Φ buffer {:?} != ({n}, {m})",
phi.dim()
));
}
if jet.dim() != (n, m, d) {
return Err(format!(
"TorusHarmonicEvaluator::evaluate_into: jet buffer {:?} != ({n}, {m}, {d})",
jet.dim()
));
}
let h_max = self.num_harmonics;
let two_pi = 2.0 * std::f64::consts::PI;
let mut phi_axis = vec![vec![0.0_f64; axis_m]; d];
let mut dphi_axis = vec![vec![0.0_f64; axis_m]; d];
for row in 0..n {
for axis in 0..d {
let t = coords[[row, axis]];
phi_axis[axis][0] = 1.0;
dphi_axis[axis][0] = 0.0;
for h in 1..=h_max {
let freq = two_pi * (h as f64);
let angle = freq * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * h - 1;
let c_idx = 2 * h;
phi_axis[axis][s_idx] = s;
phi_axis[axis][c_idx] = c;
dphi_axis[axis][s_idx] = freq * c;
dphi_axis[axis][c_idx] = -freq * s;
}
}
let mut idx = vec![0usize; d];
for flat in 0..m {
let mut val = 1.0_f64;
for axis in 0..d {
val *= phi_axis[axis][idx[axis]];
}
phi[[row, flat]] = val;
for axis_target in 0..d {
let mut deriv = 1.0_f64;
for axis in 0..d {
deriv *= if axis == axis_target {
dphi_axis[axis][idx[axis]]
} else {
phi_axis[axis][idx[axis]]
};
}
jet[[row, flat, axis_target]] = deriv;
}
for axis in (0..d).rev() {
idx[axis] += 1;
if idx[axis] < axis_m {
break;
}
idx[axis] = 0;
}
}
}
Ok(())
}
}
impl SaeBasisSecondJet for TorusHarmonicEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
let d = self.latent_dim;
if coords.ncols() != d {
return Err(format!(
"TorusHarmonicEvaluator::second_jet expects latent_dim == {d}, got {}",
coords.ncols()
));
}
let n = coords.nrows();
let axis_m = self.axis_basis_size();
let m = self.basis_size();
let h_max = self.num_harmonics;
let two_pi = 2.0 * std::f64::consts::PI;
let mut hess = Array4::<f64>::zeros((n, m, d, d));
let mut phi_axis = vec![vec![0.0_f64; axis_m]; d];
let mut dphi_axis = vec![vec![0.0_f64; axis_m]; d];
let mut d2phi_axis = vec![vec![0.0_f64; axis_m]; d];
for row in 0..n {
for axis in 0..d {
let t = coords[[row, axis]];
phi_axis[axis][0] = 1.0;
dphi_axis[axis][0] = 0.0;
d2phi_axis[axis][0] = 0.0;
for k in 1..=h_max {
let freq = two_pi * (k as f64);
let freq2 = freq * freq;
let angle = freq * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * k - 1;
let c_idx = 2 * k;
phi_axis[axis][s_idx] = s;
phi_axis[axis][c_idx] = c;
dphi_axis[axis][s_idx] = freq * c;
dphi_axis[axis][c_idx] = -freq * s;
d2phi_axis[axis][s_idx] = -freq2 * s;
d2phi_axis[axis][c_idx] = -freq2 * c;
}
}
let mut idx = vec![0usize; d];
for flat in 0..m {
for axis_a in 0..d {
for axis_b in 0..d {
let mut prod = 1.0_f64;
for axis in 0..d {
let factor = if axis == axis_a && axis == axis_b {
d2phi_axis[axis][idx[axis]]
} else if axis == axis_a || axis == axis_b {
dphi_axis[axis][idx[axis]]
} else {
phi_axis[axis][idx[axis]]
};
prod *= factor;
}
hess[[row, flat, axis_a, axis_b]] = prod;
}
}
for axis in (0..d).rev() {
idx[axis] += 1;
if idx[axis] < axis_m {
break;
}
idx[axis] = 0;
}
}
}
Ok(hess)
}
}
impl SaeBasisThirdJet for TorusHarmonicEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
let d = self.latent_dim;
if coords.ncols() != d {
return Err(format!(
"TorusHarmonicEvaluator::third_jet expects latent_dim == {d}, got {}",
coords.ncols()
));
}
let n = coords.nrows();
let axis_m = self.axis_basis_size();
let m = self.basis_size();
let h_max = self.num_harmonics;
let two_pi = 2.0 * std::f64::consts::PI;
let mut t3 = Array5::<f64>::zeros((n, m, d, d, d));
let mut deriv_axis = vec![vec![vec![0.0_f64; axis_m]; 4]; d];
for row in 0..n {
for axis in 0..d {
let t = coords[[row, axis]];
for order in 0..4 {
deriv_axis[axis][order][0] = 0.0;
}
deriv_axis[axis][0][0] = 1.0;
for k in 1..=h_max {
let freq = two_pi * (k as f64);
let freq2 = freq * freq;
let freq3 = freq2 * freq;
let angle = freq * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * k - 1;
let c_idx = 2 * k;
deriv_axis[axis][0][s_idx] = s;
deriv_axis[axis][0][c_idx] = c;
deriv_axis[axis][1][s_idx] = freq * c;
deriv_axis[axis][1][c_idx] = -freq * s;
deriv_axis[axis][2][s_idx] = -freq2 * s;
deriv_axis[axis][2][c_idx] = -freq2 * c;
deriv_axis[axis][3][s_idx] = -freq3 * c;
deriv_axis[axis][3][c_idx] = freq3 * s;
}
}
let mut idx = vec![0usize; d];
for flat in 0..m {
for axis_a in 0..d {
for axis_b in 0..d {
for axis_c in 0..d {
let mut prod = 1.0_f64;
for axis in 0..d {
let order = (axis == axis_a) as usize
+ (axis == axis_b) as usize
+ (axis == axis_c) as usize;
prod *= deriv_axis[axis][order][idx[axis]];
}
t3[[row, flat, axis_a, axis_b, axis_c]] = prod;
}
}
}
for axis in (0..d).rev() {
idx[axis] += 1;
if idx[axis] < axis_m {
break;
}
idx[axis] = 0;
}
}
}
Ok(t3)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeckInvolutionCharacter {
Trivial,
Sign,
}
impl DeckInvolutionCharacter {
fn reynolds_projector_eigenvalue(self) -> u8 {
match self {
Self::Trivial => 1,
Self::Sign => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct CoverSpectralCharacter {
deck_character: DeckInvolutionCharacter,
laplace_eigenvalue: f64,
l2_gram_weight: f64,
curved_if_retained: bool,
}
#[derive(Debug, Clone)]
pub struct QuotientSpectralEvaluator {
quotient_name: String,
cover: Arc<dyn SaeBasisThirdJet>,
cover_width: usize,
cover_columns: Vec<usize>,
laplace_eigenvalues: Vec<f64>,
l2_gram_weights: Vec<f64>,
curved_columns: Vec<bool>,
}
pub fn projective_plane_basis_size(harmonic_order: usize) -> Result<usize, String> {
if harmonic_order == 0 {
return Err("projective_plane_basis_size requires harmonic_order >= 1".to_string());
}
harmonic_order
.checked_add(1)
.and_then(|left| {
harmonic_order
.checked_mul(2)
.and_then(|twice| twice.checked_add(1))
.and_then(|right| left.checked_mul(right))
})
.ok_or_else(|| "projective_plane_basis_size overflowed usize".to_string())
}
pub fn klein_bottle_basis_size(num_harmonics: usize) -> Result<usize, String> {
if num_harmonics == 0 {
return Err("klein_bottle_basis_size requires num_harmonics >= 1".to_string());
}
let cross_width = num_harmonics
.checked_mul(num_harmonics)
.and_then(|square| square.checked_mul(2))
.ok_or_else(|| "klein_bottle_basis_size overflowed usize".to_string())?;
1usize
.checked_add(2 * (num_harmonics / 2))
.and_then(|width| width.checked_add(num_harmonics))
.and_then(|width| width.checked_add(cross_width))
.ok_or_else(|| "klein_bottle_basis_size overflowed usize".to_string())
}
impl QuotientSpectralEvaluator {
fn from_diagonal_involution_characters(
quotient_name: impl Into<String>,
cover: Arc<dyn SaeBasisThirdJet>,
cover_width: usize,
characters: Vec<CoverSpectralCharacter>,
) -> Result<Self, String> {
let quotient_name = quotient_name.into();
if quotient_name.trim().is_empty() {
return Err("QuotientSpectralEvaluator requires a non-empty quotient name".to_string());
}
if cover_width == 0 {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: cover width must be positive"
));
}
if characters.len() != cover_width {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: diagonal character table width {} != cover width {cover_width}",
characters.len()
));
}
let mut cover_columns = Vec::with_capacity(cover_width);
let mut laplace_eigenvalues = Vec::with_capacity(cover_width);
let mut l2_gram_weights = Vec::with_capacity(cover_width);
let mut curved_columns = Vec::with_capacity(cover_width);
let mut nullity = 0usize;
for (cover_column, mode) in characters.into_iter().enumerate() {
if !(mode.laplace_eigenvalue.is_finite() && mode.laplace_eigenvalue >= 0.0) {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: cover column {cover_column} has invalid Laplace eigenvalue {}",
mode.laplace_eigenvalue
));
}
if !(mode.l2_gram_weight.is_finite() && mode.l2_gram_weight > 0.0) {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: cover column {cover_column} has invalid L2 Gram weight {}",
mode.l2_gram_weight
));
}
if mode.laplace_eigenvalue == 0.0 {
if mode.deck_character != DeckInvolutionCharacter::Trivial {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: the constant cover mode at column {cover_column} must have trivial deck character"
));
}
nullity += 1;
if mode.curved_if_retained {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: the constant null mode cannot be curvature-scaled"
));
}
}
if mode.deck_character.reynolds_projector_eigenvalue() == 1 {
cover_columns.push(cover_column);
laplace_eigenvalues.push(mode.laplace_eigenvalue);
l2_gram_weights.push(mode.l2_gram_weight);
curved_columns.push(mode.curved_if_retained);
}
}
if nullity != 1 {
return Err(format!(
"QuotientSpectralEvaluator[{quotient_name}]: connected closed quotient requires exactly one constant null mode; found {nullity}"
));
}
Ok(Self {
quotient_name,
cover,
cover_width,
cover_columns,
laplace_eigenvalues,
l2_gram_weights,
curved_columns,
})
}
pub fn projective_plane(harmonic_order: usize) -> Result<Self, String> {
if harmonic_order == 0 {
return Err(
"QuotientSpectralEvaluator::projective_plane requires harmonic_order >= 1"
.to_string(),
);
}
let max_degree = harmonic_order.checked_mul(2).ok_or_else(|| {
"QuotientSpectralEvaluator::projective_plane: maximum cover degree overflowed usize"
.to_string()
})?;
let cover_side = max_degree.checked_add(1).ok_or_else(|| {
"QuotientSpectralEvaluator::projective_plane: cover width overflowed usize".to_string()
})?;
cover_side.checked_mul(cover_side).ok_or_else(|| {
"QuotientSpectralEvaluator::projective_plane: cover width overflowed usize".to_string()
})?;
let expected_width = projective_plane_basis_size(harmonic_order)?;
let cover = SphericalHarmonicEvaluator::new(max_degree)?;
let cover_width = cover.basis_size();
let modes = cover.spectral_modes();
Self::projective_plane_from_cover(
"projective-plane",
Arc::new(cover),
cover_width,
modes,
expected_width,
)
}
pub fn projective_plane_ambient(harmonic_order: usize) -> Result<Self, String> {
if harmonic_order == 0 {
return Err(
"QuotientSpectralEvaluator::projective_plane_ambient requires harmonic_order >= 1"
.to_string(),
);
}
let max_degree = harmonic_order.checked_mul(2).ok_or_else(|| {
"QuotientSpectralEvaluator::projective_plane_ambient: maximum cover degree overflowed usize"
.to_string()
})?;
let expected_width = projective_plane_basis_size(harmonic_order)?;
let cover = AmbientSphereHarmonicEvaluator::new(max_degree)?;
let cover_width = cover.basis_size();
let modes = cover.spectral_modes();
Self::projective_plane_from_cover(
"projective-plane-ambient",
Arc::new(cover),
cover_width,
modes,
expected_width,
)
}
fn projective_plane_from_cover(
quotient_name: &str,
cover: Arc<dyn SaeBasisThirdJet>,
cover_width: usize,
modes: Vec<SphericalHarmonicMode>,
expected_width: usize,
) -> Result<Self, String> {
let characters = modes
.into_iter()
.map(|mode| CoverSpectralCharacter {
deck_character: if mode.degree % 2 == 0 {
DeckInvolutionCharacter::Trivial
} else {
DeckInvolutionCharacter::Sign
},
laplace_eigenvalue: mode.laplace_eigenvalue,
l2_gram_weight: mode.l2_gram_weight,
curved_if_retained: mode.degree > 2,
})
.collect::<Vec<_>>();
let evaluator =
Self::from_diagonal_involution_characters(quotient_name, cover, cover_width, characters)?;
if evaluator.basis_size() != expected_width {
return Err(format!(
"QuotientSpectralEvaluator::{quotient_name}: group average produced width {}, expected {expected_width}",
evaluator.basis_size()
));
}
Ok(evaluator)
}
pub fn klein_bottle(num_harmonics: usize) -> Result<Self, String> {
if num_harmonics < 2 {
return Err(
"QuotientSpectralEvaluator::klein_bottle requires num_harmonics >= 2 for the standard R4 embedding"
.to_string(),
);
}
let expected_width = klein_bottle_basis_size(num_harmonics)?;
let cover = TorusHarmonicEvaluator::new(2, num_harmonics)?;
let cover_width = cover.basis_size();
let mut characters = Vec::with_capacity(cover_width);
for (cover_column, mode) in cover.spectral_modes().into_iter().enumerate() {
let [theta_component, phi_component] = mode.components.as_slice() else {
return Err(format!(
"QuotientSpectralEvaluator::klein_bottle: torus cover mode {cover_column} did not have two factors"
));
};
let theta_harmonic = theta_component.harmonic();
let phi_harmonic = phi_component.harmonic();
let half_turn_sign = if theta_harmonic % 2 == 0 { 1 } else { -1 };
let deck_character = if half_turn_sign * phi_component.reflection_sign() == 1 {
DeckInvolutionCharacter::Trivial
} else {
DeckInvolutionCharacter::Sign
};
let embedding_mode = (theta_harmonic == 0 && phi_harmonic == 0)
|| (theta_harmonic == 2 && phi_harmonic == 0)
|| (theta_harmonic == 2
&& phi_harmonic == 1
&& phi_component.reflection_sign() == 1)
|| (theta_harmonic == 1
&& phi_harmonic == 1
&& phi_component.reflection_sign() == -1);
characters.push(CoverSpectralCharacter {
deck_character,
laplace_eigenvalue: mode.laplace_eigenvalue,
l2_gram_weight: mode.l2_gram_weight,
curved_if_retained: !embedding_mode,
});
}
let evaluator = Self::from_diagonal_involution_characters(
"klein-bottle",
Arc::new(cover),
cover_width,
characters,
)?;
if evaluator.basis_size() != expected_width {
return Err(format!(
"QuotientSpectralEvaluator::klein_bottle: group average produced width {}, expected {expected_width}",
evaluator.basis_size()
));
}
Ok(evaluator)
}
pub fn quotient_name(&self) -> &str {
&self.quotient_name
}
pub fn basis_size(&self) -> usize {
self.cover_columns.len()
}
pub fn cover_width(&self) -> usize {
self.cover_width
}
pub fn cover_columns(&self) -> &[usize] {
&self.cover_columns
}
pub fn laplace_eigenvalues(&self) -> &[f64] {
&self.laplace_eigenvalues
}
pub fn l2_gram_weights(&self) -> &[f64] {
&self.l2_gram_weights
}
pub fn function_space_gram(&self) -> Array2<f64> {
let mut gram = Array2::<f64>::zeros((self.basis_size(), self.basis_size()));
for (column, &weight) in self.l2_gram_weights.iter().enumerate() {
gram[[column, column]] = weight;
}
gram
}
pub fn spectral_penalty(&self, power: u32) -> Result<Array2<f64>, String> {
if power == 0 {
return Err(format!(
"QuotientSpectralEvaluator[{}]::spectral_penalty requires power >= 1",
self.quotient_name
));
}
let exponent = i32::try_from(power).map_err(|_| {
format!(
"QuotientSpectralEvaluator[{}]::spectral_penalty power {power} exceeds i32::MAX",
self.quotient_name
)
})?;
let mut penalty = Array2::<f64>::zeros((self.basis_size(), self.basis_size()));
for column in 0..self.basis_size() {
let value =
self.l2_gram_weights[column] * self.laplace_eigenvalues[column].powi(exponent);
if !value.is_finite() {
return Err(format!(
"QuotientSpectralEvaluator[{}]::spectral_penalty overflowed at quotient column {column}",
self.quotient_name
));
}
penalty[[column, column]] = value;
}
Ok(penalty)
}
pub fn nullspace_dimension(&self) -> usize {
self.laplace_eigenvalues
.iter()
.filter(|&&eigenvalue| eigenvalue == 0.0)
.count()
}
fn validate_cover_value_jet(
&self,
phi: &Array2<f64>,
jet: &Array3<f64>,
n_rows: usize,
latent_dim: usize,
) -> Result<(), String> {
if phi.dim() != (n_rows, self.cover_width) {
return Err(format!(
"QuotientSpectralEvaluator[{}]: cover Phi shape {:?} != ({n_rows}, {})",
self.quotient_name,
phi.dim(),
self.cover_width
));
}
if jet.dim() != (n_rows, self.cover_width, latent_dim) {
return Err(format!(
"QuotientSpectralEvaluator[{}]: cover jet shape {:?} != ({n_rows}, {}, {latent_dim})",
self.quotient_name,
jet.dim(),
self.cover_width
));
}
Ok(())
}
}
impl SaeBasisEvaluator for QuotientSpectralEvaluator {
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
let n_rows = coords.nrows();
let latent_dim = coords.ncols();
let mut phi = Array2::<f64>::zeros((n_rows, self.basis_size()));
let mut jet = Array3::<f64>::zeros((n_rows, self.basis_size(), latent_dim));
self.evaluate_into(&mut phi, &mut jet, coords)?;
Ok((phi, jet))
}
fn evaluate_into(
&self,
phi: &mut Array2<f64>,
jet: &mut Array3<f64>,
coords: ArrayView2<'_, f64>,
) -> Result<(), String> {
let n_rows = coords.nrows();
let latent_dim = coords.ncols();
let quotient_width = self.basis_size();
if phi.dim() != (n_rows, quotient_width) {
return Err(format!(
"QuotientSpectralEvaluator[{}]::evaluate_into: Phi buffer {:?} != ({n_rows}, {quotient_width})",
self.quotient_name,
phi.dim()
));
}
if jet.dim() != (n_rows, quotient_width, latent_dim) {
return Err(format!(
"QuotientSpectralEvaluator[{}]::evaluate_into: jet buffer {:?} != ({n_rows}, {quotient_width}, {latent_dim})",
self.quotient_name,
jet.dim()
));
}
let (cover_phi, cover_jet) = self.cover.evaluate(coords)?;
self.validate_cover_value_jet(&cover_phi, &cover_jet, n_rows, latent_dim)?;
for (quotient_column, &cover_column) in self.cover_columns.iter().enumerate() {
for row in 0..n_rows {
phi[[row, quotient_column]] = cover_phi[[row, cover_column]];
for axis in 0..latent_dim {
jet[[row, quotient_column, axis]] = cover_jet[[row, cover_column, axis]];
}
}
}
Ok(())
}
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
if n_basis != self.basis_size() {
return Err(format!(
"QuotientSpectralEvaluator[{}]::phi_eta_split: n_basis {n_basis} != evaluator width {}",
self.quotient_name,
self.basis_size()
));
}
Ok(PhiEtaSplit::from_curved_mask(self.curved_columns.clone()))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
None
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
}
impl SaeBasisSecondJet for QuotientSpectralEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
let n_rows = coords.nrows();
let latent_dim = coords.ncols();
let cover_hessian = self.cover.second_jet(coords)?;
if cover_hessian.dim() != (n_rows, self.cover_width, latent_dim, latent_dim) {
return Err(format!(
"QuotientSpectralEvaluator[{}]: cover second-jet shape {:?} != ({n_rows}, {}, {latent_dim}, {latent_dim})",
self.quotient_name,
cover_hessian.dim(),
self.cover_width
));
}
let mut hessian = Array4::<f64>::zeros((n_rows, self.basis_size(), latent_dim, latent_dim));
for (quotient_column, &cover_column) in self.cover_columns.iter().enumerate() {
for row in 0..n_rows {
for axis_a in 0..latent_dim {
for axis_b in 0..latent_dim {
hessian[[row, quotient_column, axis_a, axis_b]] =
cover_hessian[[row, cover_column, axis_a, axis_b]];
}
}
}
}
Ok(hessian)
}
}
impl SaeBasisThirdJet for QuotientSpectralEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
let n_rows = coords.nrows();
let latent_dim = coords.ncols();
let cover_third = self.cover.third_jet(coords)?;
if cover_third.dim() != (n_rows, self.cover_width, latent_dim, latent_dim, latent_dim) {
return Err(format!(
"QuotientSpectralEvaluator[{}]: cover third-jet shape {:?} != ({n_rows}, {}, {latent_dim}, {latent_dim}, {latent_dim})",
self.quotient_name,
cover_third.dim(),
self.cover_width
));
}
let mut third = Array5::<f64>::zeros((
n_rows,
self.basis_size(),
latent_dim,
latent_dim,
latent_dim,
));
for (quotient_column, &cover_column) in self.cover_columns.iter().enumerate() {
for row in 0..n_rows {
for axis_a in 0..latent_dim {
for axis_b in 0..latent_dim {
for axis_c in 0..latent_dim {
third[[row, quotient_column, axis_a, axis_b, axis_c]] =
cover_third[[row, cover_column, axis_a, axis_b, axis_c]];
}
}
}
}
}
Ok(third)
}
}
#[derive(Debug, Clone)]
pub struct AffineCoordinateEvaluator {
pub latent_dim: usize,
}
impl AffineCoordinateEvaluator {
pub fn new(latent_dim: usize) -> Self {
Self { latent_dim }
}
}
impl SaeBasisEvaluator for AffineCoordinateEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let expected = self.latent_dim + 1;
if n_basis != expected {
return Err(format!(
"AffineCoordinateEvaluator::phi_eta_split: n_basis {n_basis} != {expected}"
));
}
Ok(PhiEtaSplit::all_base(n_basis))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
if coords.ncols() != self.latent_dim {
return Err(format!(
"AffineCoordinateEvaluator: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let n = coords.nrows();
let m = self.latent_dim + 1;
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, self.latent_dim));
phi.column_mut(0).fill(1.0);
for row in 0..n {
for axis in 0..self.latent_dim {
phi[[row, axis + 1]] = coords[[row, axis]];
jet[[row, axis + 1, axis]] = 1.0;
}
}
Ok((phi, jet))
}
}
impl SaeBasisSecondJet for AffineCoordinateEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
if coords.ncols() != self.latent_dim {
return Err(format!(
"AffineCoordinateEvaluator::second_jet: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let n = coords.nrows();
let m = self.latent_dim + 1;
let d = self.latent_dim;
Ok(Array4::<f64>::zeros((n, m, d, d)))
}
}
impl SaeBasisThirdJet for AffineCoordinateEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
if coords.ncols() != self.latent_dim {
return Err(format!(
"AffineCoordinateEvaluator::third_jet: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let n = coords.nrows();
let m = self.latent_dim + 1;
let d = self.latent_dim;
Ok(Array5::<f64>::zeros((n, m, d, d, d)))
}
}
#[derive(Debug, Clone)]
pub struct DuchonCoordinateEvaluator {
pub centers: Array2<f64>,
pub order: gam_terms::basis::DuchonNullspaceOrder,
}
impl DuchonCoordinateEvaluator {
pub fn new(centers: Array2<f64>, m: usize) -> Result<Self, String> {
if centers.ncols() == 0 {
return Err("DuchonCoordinateEvaluator: centers must have at least one column".into());
}
if m == 0 {
return Err("DuchonCoordinateEvaluator: Duchon m must be at least 1".into());
}
let order = match m {
1 => gam_terms::basis::DuchonNullspaceOrder::Zero,
2 => gam_terms::basis::DuchonNullspaceOrder::Linear,
other => gam_terms::basis::DuchonNullspaceOrder::Degree(other - 1),
};
Ok(Self { centers, order })
}
}
impl SaeBasisEvaluator for DuchonCoordinateEvaluator {
fn affine_transformed_evaluator(
&self,
shift: &[f64],
scale: &[f64],
n_basis: usize,
) -> Result<Option<Arc<dyn SaeBasisSecondJet>>, String> {
let dim = self.centers.ncols();
if shift.len() != dim || scale.len() != dim {
return Err(format!(
"DuchonCoordinateEvaluator::affine_transformed_evaluator: affine vectors must have length {dim}; got shift={} scale={}",
shift.len(),
scale.len()
));
}
if n_basis == usize::MAX {
return Err(
"DuchonCoordinateEvaluator::affine_transformed_evaluator: unreachable basis width"
.to_string(),
);
}
if dim != 1 {
return Ok(None);
}
if !(scale[0].is_finite() && scale[0] > 0.0 && shift[0].is_finite()) {
return Ok(None);
}
let mut centers = self.centers.clone();
for row in 0..centers.nrows() {
centers[[row, 0]] = (centers[[row, 0]] - shift[0]) / scale[0];
}
Ok(Some(Arc::new(Self {
centers,
order: self.order,
})))
}
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let dim = self.centers.ncols();
let effective = duchon_effective_order_for_eta(self.centers.view(), self.order);
let n_poly = duchon_polynomial_column_count(dim, effective);
if n_basis < n_poly {
return Err(format!(
"DuchonCoordinateEvaluator::phi_eta_split: n_basis {n_basis} smaller than polynomial block {n_poly}"
));
}
let n_kernel = n_basis - n_poly;
let mut curved = vec![false; n_basis];
for col in 0..n_kernel {
curved[col] = true;
}
if let gam_terms::basis::DuchonNullspaceOrder::Degree(degree) = effective {
let linear_mask = monomial_linear_mask(dim, degree);
if linear_mask.len() != n_poly {
return Err(format!(
"DuchonCoordinateEvaluator::phi_eta_split: polynomial mask width {} != {n_poly}",
linear_mask.len()
));
}
for (local_col, linear) in linear_mask.into_iter().enumerate() {
if !linear {
curved[n_kernel + local_col] = true;
}
}
}
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
if coords.ncols() != self.centers.ncols() {
return Err(format!(
"DuchonCoordinateEvaluator: expected latent_dim {}, got {}",
self.centers.ncols(),
coords.ncols()
));
}
gam_terms::basis::duchon_sae_atom_basis_with_jet(coords, self.centers.view(), self.order)
.map_err(|err| err.to_string())
}
}
impl SaeBasisSecondJet for DuchonCoordinateEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
if coords.ncols() != self.centers.ncols() {
return Err(format!(
"DuchonCoordinateEvaluator::second_jet: expected latent_dim {}, got {}",
self.centers.ncols(),
coords.ncols()
));
}
gam_terms::basis::duchon_sae_atom_second_jet(coords, self.centers.view(), self.order)
.map_err(|err| err.to_string())
}
}
impl SaeBasisThirdJet for DuchonCoordinateEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
if coords.ncols() != self.centers.ncols() {
return Err(format!(
"DuchonCoordinateEvaluator::third_jet: expected latent_dim {}, got {}",
self.centers.ncols(),
coords.ncols()
));
}
gam_terms::basis::duchon_sae_atom_third_jet(coords, self.centers.view(), self.order)
.map_err(|err| err.to_string())
}
}
#[derive(Debug, Clone)]
pub struct EuclideanPatchEvaluator {
pub latent_dim: usize,
pub max_degree: usize,
}
impl EuclideanPatchEvaluator {
pub fn new(latent_dim: usize, max_degree: usize) -> Result<Self, String> {
if latent_dim == 0 {
return Err("EuclideanPatchEvaluator: latent_dim must be positive".into());
}
Ok(Self {
latent_dim,
max_degree,
})
}
pub fn basis_size(&self) -> usize {
gam_terms::basis::monomial_exponents(self.latent_dim, self.max_degree).len()
}
}
impl SaeBasisEvaluator for EuclideanPatchEvaluator {
fn affine_transformed_evaluator(
&self,
shift: &[f64],
scale: &[f64],
n_basis: usize,
) -> Result<Option<Arc<dyn SaeBasisSecondJet>>, String> {
if shift.len() != self.latent_dim || scale.len() != self.latent_dim {
return Err(format!(
"EuclideanPatchEvaluator::affine_transformed_evaluator: affine vectors must have length {}; got shift={} scale={}",
self.latent_dim,
shift.len(),
scale.len()
));
}
if n_basis != self.basis_size() {
return Err(format!(
"EuclideanPatchEvaluator::affine_transformed_evaluator: n_basis {n_basis} != evaluator width {}",
self.basis_size()
));
}
if shift.iter().chain(scale.iter()).any(|v| !v.is_finite())
|| scale.iter().any(|&v| v <= 0.0)
{
return Ok(None);
}
Ok(Some(Arc::new(Self {
latent_dim: self.latent_dim,
max_degree: self.max_degree,
})))
}
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let linear_mask = monomial_linear_mask(self.latent_dim, self.max_degree);
if linear_mask.len() != n_basis {
return Err(format!(
"EuclideanPatchEvaluator::phi_eta_split: polynomial mask width {} != n_basis {n_basis}",
linear_mask.len()
));
}
Ok(PhiEtaSplit::from_curved_mask(
linear_mask.into_iter().map(|linear| !linear).collect(),
))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
let n = coords.nrows();
let m = self.basis_size();
let d = self.latent_dim;
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, d));
self.evaluate_into(&mut phi, &mut jet, coords)?;
Ok((phi, jet))
}
fn evaluate_into(
&self,
phi: &mut Array2<f64>,
jet: &mut Array3<f64>,
coords: ArrayView2<'_, f64>,
) -> Result<(), String> {
let d = self.latent_dim;
if coords.ncols() != d {
return Err(format!(
"EuclideanPatchEvaluator: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let exponents = gam_terms::basis::monomial_exponents(self.latent_dim, self.max_degree);
let n = coords.nrows();
let m = exponents.len();
if phi.dim() != (n, m) {
return Err(format!(
"EuclideanPatchEvaluator::evaluate_into: Φ buffer {:?} != ({n}, {m})",
phi.dim()
));
}
if jet.dim() != (n, m, d) {
return Err(format!(
"EuclideanPatchEvaluator::evaluate_into: jet buffer {:?} != ({n}, {m}, {d})",
jet.dim()
));
}
phi.fill(0.0);
jet.fill(0.0);
for (col, alpha) in exponents.iter().enumerate() {
for row in 0..n {
let mut value = 1.0_f64;
for (axis, &exp) in alpha.iter().enumerate() {
if exp != 0 {
value *= coords[[row, axis]].powi(exp as i32);
}
}
phi[[row, col]] = value;
}
for axis in 0..d {
let a_axis = alpha[axis];
if a_axis == 0 {
continue;
}
for row in 0..n {
let mut value = a_axis as f64;
for a in 0..d {
let exp_a = if a == axis { a_axis - 1 } else { alpha[a] };
if exp_a != 0 {
value *= coords[[row, a]].powi(exp_a as i32);
}
}
jet[[row, col, axis]] = value;
}
}
}
Ok(())
}
}
impl SaeBasisSecondJet for EuclideanPatchEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
if coords.ncols() != self.latent_dim {
return Err(format!(
"EuclideanPatchEvaluator::second_jet: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let exponents = gam_terms::basis::monomial_exponents(self.latent_dim, self.max_degree);
let n = coords.nrows();
let m = exponents.len();
let d = self.latent_dim;
let mut hess = Array4::<f64>::zeros((n, m, d, d));
for (col, alpha) in exponents.iter().enumerate() {
for a in 0..d {
if alpha[a] == 0 {
continue;
}
for c in 0..d {
if a != c && alpha[c] == 0 {
continue;
}
let lead = if a == c {
(alpha[a] as f64) * (alpha[a].saturating_sub(1) as f64)
} else {
(alpha[a] as f64) * (alpha[c] as f64)
};
if lead == 0.0 {
continue;
}
for row in 0..n {
let mut value = lead;
for axis in 0..d {
let mut exp = alpha[axis];
if axis == a {
exp = exp.saturating_sub(1);
}
if axis == c {
exp = exp.saturating_sub(1);
}
if exp != 0 {
value *= coords[[row, axis]].powi(exp as i32);
}
}
hess[[row, col, a, c]] = value;
}
}
}
}
Ok(hess)
}
}
impl SaeBasisThirdJet for EuclideanPatchEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
if coords.ncols() != self.latent_dim {
return Err(format!(
"EuclideanPatchEvaluator::third_jet: expected latent_dim {}, got {}",
self.latent_dim,
coords.ncols()
));
}
let exponents = gam_terms::basis::monomial_exponents(self.latent_dim, self.max_degree);
let n = coords.nrows();
let m = exponents.len();
let d = self.latent_dim;
let mut t3 = Array5::<f64>::zeros((n, m, d, d, d));
let falling = |alpha: usize, k: usize| -> f64 {
let mut acc = 1.0_f64;
for j in 0..k {
acc *= (alpha as f64) - (j as f64);
}
acc
};
for (col, alpha) in exponents.iter().enumerate() {
for a in 0..d {
if alpha[a] == 0 {
continue;
}
for b in 0..d {
for c in 0..d {
let mut order = vec![0usize; d];
order[a] += 1;
order[b] += 1;
order[c] += 1;
if (0..d).any(|axis| order[axis] > alpha[axis]) {
continue;
}
let mut lead = 1.0_f64;
for axis in 0..d {
lead *= falling(alpha[axis], order[axis]);
}
if lead == 0.0 {
continue;
}
for row in 0..n {
let mut value = lead;
for axis in 0..d {
let exp = alpha[axis] - order[axis];
if exp != 0 {
value *= coords[[row, axis]].powi(exp as i32);
}
}
t3[[row, col, a, b, c]] = value;
}
}
}
}
}
Ok(t3)
}
}
#[derive(Debug, Clone)]
pub struct CylinderHarmonicEvaluator {
pub circle_harmonics: usize,
pub line_degree: usize,
}
impl CylinderHarmonicEvaluator {
pub fn new(circle_harmonics: usize, line_degree: usize) -> Result<Self, String> {
if circle_harmonics == 0 {
return Err(
"CylinderHarmonicEvaluator requires circle_harmonics >= 1 (S¹ needs at least one \
harmonic pair)"
.to_string(),
);
}
Ok(Self {
circle_harmonics,
line_degree,
})
}
pub fn circle_basis_size(&self) -> usize {
2 * self.circle_harmonics + 1
}
pub fn line_basis_size(&self) -> usize {
self.line_degree + 1
}
pub fn basis_size(&self) -> usize {
self.circle_basis_size() * self.line_basis_size()
}
fn circle_tables(&self, t: f64) -> [Vec<f64>; 4] {
let mc = self.circle_basis_size();
let two_pi = 2.0 * std::f64::consts::PI;
let mut table = [
vec![0.0_f64; mc],
vec![0.0_f64; mc],
vec![0.0_f64; mc],
vec![0.0_f64; mc],
];
table[0][0] = 1.0;
for h in 1..=self.circle_harmonics {
let omega = two_pi * (h as f64);
let w2 = omega * omega;
let w3 = w2 * omega;
let angle = omega * t;
let s = angle.sin();
let c = angle.cos();
let s_idx = 2 * h - 1;
let c_idx = 2 * h;
table[0][s_idx] = s;
table[1][s_idx] = omega * c;
table[2][s_idx] = -w2 * s;
table[3][s_idx] = -w3 * c;
table[0][c_idx] = c;
table[1][c_idx] = -omega * s;
table[2][c_idx] = -w2 * c;
table[3][c_idx] = w3 * s;
}
table
}
fn line_tables(&self, t: f64) -> [Vec<f64>; 4] {
let ml = self.line_basis_size();
let mut table = [
vec![0.0_f64; ml],
vec![0.0_f64; ml],
vec![0.0_f64; ml],
vec![0.0_f64; ml],
];
for j in 0..ml {
for k in 0..4 {
if k > j {
table[k][j] = 0.0;
continue;
}
let mut coeff = 1.0_f64;
for q in 0..k {
coeff *= (j - q) as f64;
}
let residual = j - k;
let pow = if residual == 0 {
1.0
} else {
t.powi(residual as i32)
};
table[k][j] = coeff * pow;
}
}
table
}
pub fn roughness_gram(&self) -> Array2<f64> {
let mc = self.circle_basis_size();
let ml = self.line_basis_size();
let two_pi = 2.0 * std::f64::consts::PI;
let mut gc = Array2::<f64>::zeros((mc, mc));
let mut sc = Array2::<f64>::zeros((mc, mc));
gc[[0, 0]] = 1.0; for h in 1..=self.circle_harmonics {
let omega = two_pi * (h as f64);
let w4 = omega.powi(4);
let s_idx = 2 * h - 1;
let c_idx = 2 * h;
gc[[s_idx, s_idx]] = 0.5;
gc[[c_idx, c_idx]] = 0.5;
sc[[s_idx, s_idx]] = w4 * 0.5;
sc[[c_idx, c_idx]] = w4 * 0.5;
}
let mut gl = Array2::<f64>::zeros((ml, ml));
let mut sl = Array2::<f64>::zeros((ml, ml));
for i in 0..ml {
for j in 0..ml {
gl[[i, j]] = 1.0 / ((i + j + 1) as f64);
if i >= 2 && j >= 2 {
let ci = (i * (i - 1)) as f64;
let cj = (j * (j - 1)) as f64;
let exp = (i - 2) + (j - 2);
sl[[i, j]] = ci * cj / ((exp + 1) as f64);
}
}
}
let m = mc * ml;
let mut s = Array2::<f64>::zeros((m, m));
for ca in 0..mc {
for la in 0..ml {
let row = ca * ml + la;
for cb in 0..mc {
for lb in 0..ml {
let col = cb * ml + lb;
s[[row, col]] = sc[[ca, cb]] * gl[[la, lb]] + gc[[ca, cb]] * sl[[la, lb]];
}
}
}
}
s
}
fn check_coords(&self, coords: ArrayView2<'_, f64>, what: &str) -> Result<(), String> {
if coords.ncols() != 2 {
return Err(format!(
"CylinderHarmonicEvaluator::{what}: expected latent_dim == 2 (S¹ × ℝ), got {}",
coords.ncols()
));
}
Ok(())
}
}
impl SaeBasisEvaluator for CylinderHarmonicEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let expected = self.basis_size();
if n_basis != expected {
return Err(format!(
"CylinderHarmonicEvaluator::phi_eta_split: n_basis {n_basis} != evaluator width {expected}"
));
}
let ml = self.line_basis_size();
let mut curved = vec![false; expected];
for c in 0..self.circle_basis_size() {
for l in 0..ml {
let circle_curved = c > 2;
let line_curved = l > 1;
curved[c * ml + l] = circle_curved || line_curved;
}
}
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
Some((self.circle_basis_size(), self.line_basis_size()))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
self.check_coords(coords, "evaluate")?;
let n = coords.nrows();
let mc = self.circle_basis_size();
let ml = self.line_basis_size();
let m = mc * ml;
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, 2));
for row in 0..n {
let t0 = coords[[row, 0]];
let t1 = coords[[row, 1]];
let circ = self.circle_tables(t0);
let line = self.line_tables(t1);
for c in 0..mc {
for l in 0..ml {
let col = c * ml + l;
phi[[row, col]] = circ[0][c] * line[0][l];
jet[[row, col, 0]] = circ[1][c] * line[0][l];
jet[[row, col, 1]] = circ[0][c] * line[1][l];
}
}
}
Ok((phi, jet))
}
}
impl SaeBasisSecondJet for CylinderHarmonicEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
self.check_coords(coords, "second_jet")?;
let n = coords.nrows();
let mc = self.circle_basis_size();
let ml = self.line_basis_size();
let m = mc * ml;
let mut h = Array4::<f64>::zeros((n, m, 2, 2));
for row in 0..n {
let t0 = coords[[row, 0]];
let t1 = coords[[row, 1]];
let circ = self.circle_tables(t0);
let line = self.line_tables(t1);
for c in 0..mc {
for l in 0..ml {
let col = c * ml + l;
h[[row, col, 0, 0]] = circ[2][c] * line[0][l];
h[[row, col, 1, 1]] = circ[0][c] * line[2][l];
let mixed = circ[1][c] * line[1][l];
h[[row, col, 0, 1]] = mixed;
h[[row, col, 1, 0]] = mixed;
}
}
}
Ok(h)
}
}
impl SaeBasisThirdJet for CylinderHarmonicEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
self.check_coords(coords, "third_jet")?;
let n = coords.nrows();
let mc = self.circle_basis_size();
let ml = self.line_basis_size();
let m = mc * ml;
let mut t3 = Array5::<f64>::zeros((n, m, 2, 2, 2));
for row in 0..n {
let t0 = coords[[row, 0]];
let t1 = coords[[row, 1]];
let circ = self.circle_tables(t0);
let line = self.line_tables(t1);
for c in 0..mc {
for l in 0..ml {
let col = c * ml + l;
for a in 0..2 {
for b in 0..2 {
for e in 0..2 {
let k0 = (a == 0) as usize + (b == 0) as usize + (e == 0) as usize;
let k1 = 3 - k0;
t3[[row, col, a, b, e]] = circ[k0][c] * line[k1][l];
}
}
}
}
}
}
Ok(t3)
}
}
#[derive(Debug, Clone)]
pub struct MobiusHarmonicEvaluator {
pub circle_harmonics: usize,
pub width_degree: usize,
columns: Vec<(usize, usize)>,
}
impl MobiusHarmonicEvaluator {
pub fn new(circle_harmonics: usize, width_degree: usize) -> Result<Self, String> {
if circle_harmonics == 0 {
return Err(
"MobiusHarmonicEvaluator requires circle_harmonics >= 1 (the band core needs \
at least the half-period harmonic pair)"
.to_string(),
);
}
if width_degree == 0 {
return Err(
"MobiusHarmonicEvaluator requires width_degree >= 1: with no width-odd \
columns the deck-invariant basis degenerates to a plain circle"
.to_string(),
);
}
let mc = 2 * circle_harmonics + 1;
let mut columns = Vec::new();
for c in 0..mc {
let k = Self::circle_mode(c);
for m in 0..=width_degree {
if (k + m) % 2 == 0 {
columns.push((c, m));
}
}
}
Ok(Self {
circle_harmonics,
width_degree,
columns,
})
}
fn circle_mode(c: usize) -> usize {
c.div_ceil(2)
}
pub fn basis_size(&self) -> usize {
self.columns.len()
}
fn circle_tables(&self, s: f64) -> [Vec<f64>; 4] {
let mc = 2 * self.circle_harmonics + 1;
let pi = std::f64::consts::PI;
let mut table = [
vec![0.0_f64; mc],
vec![0.0_f64; mc],
vec![0.0_f64; mc],
vec![0.0_f64; mc],
];
table[0][0] = 1.0;
for h in 1..=self.circle_harmonics {
let omega = pi * (h as f64);
let w2 = omega * omega;
let w3 = w2 * omega;
let angle = omega * s;
let sv = angle.sin();
let cv = angle.cos();
let s_idx = 2 * h - 1;
let c_idx = 2 * h;
table[0][s_idx] = sv;
table[1][s_idx] = omega * cv;
table[2][s_idx] = -w2 * sv;
table[3][s_idx] = -w3 * cv;
table[0][c_idx] = cv;
table[1][c_idx] = -omega * sv;
table[2][c_idx] = -w2 * cv;
table[3][c_idx] = w3 * sv;
}
table
}
fn width_tables(&self, w: f64) -> [Vec<f64>; 4] {
let mw = self.width_degree + 1;
let mut table = [
vec![0.0_f64; mw],
vec![0.0_f64; mw],
vec![0.0_f64; mw],
vec![0.0_f64; mw],
];
for j in 0..mw {
for k in 0..4 {
if k > j {
table[k][j] = 0.0;
continue;
}
let mut coeff = 1.0_f64;
for q in 0..k {
coeff *= (j - q) as f64;
}
let residual = j - k;
let pow = if residual == 0 {
1.0
} else {
w.powi(residual as i32)
};
table[k][j] = coeff * pow;
}
}
table
}
pub fn roughness_gram(&self) -> Array2<f64> {
let pi = std::f64::consts::PI;
let m = self.columns.len();
let moment = |exp: usize| -> f64 {
if exp % 2 == 0 {
2.0 / ((exp + 1) as f64)
} else {
0.0
}
};
let mut s = Array2::<f64>::zeros((m, m));
for (row, &(c_a, m_a)) in self.columns.iter().enumerate() {
for (col, &(c_b, m_b)) in self.columns.iter().enumerate() {
if c_a != c_b {
continue;
}
let k = Self::circle_mode(c_a) as f64;
let gc = if c_a == 0 { 2.0 } else { 1.0 };
let sc = if c_a == 0 {
0.0
} else {
(pi * k).powi(4) * 1.0
};
let gw = moment(m_a + m_b);
let sw = if m_a >= 2 && m_b >= 2 {
((m_a * (m_a - 1)) as f64) * ((m_b * (m_b - 1)) as f64) * moment(m_a + m_b - 4)
} else {
0.0
};
s[[row, col]] = sc * gw + gc * sw;
}
}
s
}
fn check_coords(&self, coords: ArrayView2<'_, f64>, what: &str) -> Result<(), String> {
if coords.ncols() != 2 {
return Err(format!(
"MobiusHarmonicEvaluator::{what}: expected latent_dim == 2 (double-cover \
angle × width), got {}",
coords.ncols()
));
}
Ok(())
}
}
impl SaeBasisEvaluator for MobiusHarmonicEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
let expected = self.basis_size();
if n_basis != expected {
return Err(format!(
"MobiusHarmonicEvaluator::phi_eta_split: n_basis {n_basis} != evaluator width {expected}"
));
}
let curved = self
.columns
.iter()
.map(|&(c, m)| Self::circle_mode(c) >= 2 || m >= 2)
.collect::<Vec<_>>();
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn factor_basis_sizes(&self) -> Option<(usize, usize)> {
None
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
Some(<Self as SaeBasisThirdJet>::third_jet(self, coords))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
self.check_coords(coords, "evaluate")?;
let n = coords.nrows();
let m = self.basis_size();
let mut phi = Array2::<f64>::zeros((n, m));
let mut jet = Array3::<f64>::zeros((n, m, 2));
for row in 0..n {
let circ = self.circle_tables(coords[[row, 0]]);
let width = self.width_tables(coords[[row, 1]]);
for (col, &(c, wm)) in self.columns.iter().enumerate() {
phi[[row, col]] = circ[0][c] * width[0][wm];
jet[[row, col, 0]] = circ[1][c] * width[0][wm];
jet[[row, col, 1]] = circ[0][c] * width[1][wm];
}
}
Ok((phi, jet))
}
}
impl SaeBasisSecondJet for MobiusHarmonicEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
self.check_coords(coords, "second_jet")?;
let n = coords.nrows();
let m = self.basis_size();
let mut h = Array4::<f64>::zeros((n, m, 2, 2));
for row in 0..n {
let circ = self.circle_tables(coords[[row, 0]]);
let width = self.width_tables(coords[[row, 1]]);
for (col, &(c, wm)) in self.columns.iter().enumerate() {
h[[row, col, 0, 0]] = circ[2][c] * width[0][wm];
h[[row, col, 1, 1]] = circ[0][c] * width[2][wm];
let mixed = circ[1][c] * width[1][wm];
h[[row, col, 0, 1]] = mixed;
h[[row, col, 1, 0]] = mixed;
}
}
Ok(h)
}
}
impl SaeBasisThirdJet for MobiusHarmonicEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
self.check_coords(coords, "third_jet")?;
let n = coords.nrows();
let m = self.basis_size();
let mut t3 = Array5::<f64>::zeros((n, m, 2, 2, 2));
for row in 0..n {
let circ = self.circle_tables(coords[[row, 0]]);
let width = self.width_tables(coords[[row, 1]]);
for (col, &(c, wm)) in self.columns.iter().enumerate() {
for a in 0..2 {
for b in 0..2 {
for e in 0..2 {
let k0 = (a == 0) as usize + (b == 0) as usize + (e == 0) as usize;
let k1 = 3 - k0;
t3[[row, col, a, b, e]] = circ[k0][c] * width[k1][wm];
}
}
}
}
}
Ok(t3)
}
}
#[derive(Debug, Clone)]
pub struct SubspaceReducedEvaluator {
inner: Arc<dyn SaeBasisSecondJet>,
q: Array2<f64>,
}
impl SubspaceReducedEvaluator {
pub fn new(inner: Arc<dyn SaeBasisSecondJet>, q: Array2<f64>) -> Result<Self, String> {
if q.nrows() == 0 || q.ncols() == 0 {
return Err(format!(
"SubspaceReducedEvaluator: column map must be non-empty; got {:?}",
q.dim()
));
}
if q.ncols() > q.nrows() {
return Err(format!(
"SubspaceReducedEvaluator: retained rank {} exceeds inner basis width {}",
q.ncols(),
q.nrows()
));
}
Ok(Self { inner, q })
}
pub fn inner_width(&self) -> usize {
self.q.nrows()
}
pub fn reduced_width(&self) -> usize {
self.q.ncols()
}
fn check_inner_width(&self, got: usize, what: &str) -> Result<(), String> {
if got != self.q.nrows() {
return Err(format!(
"SubspaceReducedEvaluator::{what}: inner evaluator returned width {got}, \
column map expects {}",
self.q.nrows()
));
}
Ok(())
}
}
fn remix_cols_2(phi: &Array2<f64>, q: &Array2<f64>) -> Array2<f64> {
phi.dot(q)
}
fn remix_cols_along_basis(
jet: ndarray::ArrayViewD<'_, f64>,
q: &Array2<f64>,
) -> Result<ndarray::ArrayD<f64>, String> {
let shape = jet.shape().to_vec();
if shape.len() < 2 {
return Err(format!(
"SubspaceReducedEvaluator: jet must have at least (n, M) axes; got {shape:?}"
));
}
let n = shape[0];
let m = shape[1];
if m != q.nrows() {
return Err(format!(
"SubspaceReducedEvaluator: jet basis axis {m} != column-map rows {}",
q.nrows()
));
}
let r = q.ncols();
let trailing: usize = shape[2..].iter().product::<usize>().max(1);
let mut out_shape = shape.clone();
out_shape[1] = r;
let jet_std = jet.to_owned();
let jet_flat = jet_std
.to_shape((n, m, trailing))
.map_err(|err| format!("SubspaceReducedEvaluator: jet reshape failed: {err}"))?;
let mut out_flat = Array3::<f64>::zeros((n, r, trailing));
for row in 0..n {
for t in 0..trailing {
for rc in 0..r {
let mut acc = 0.0_f64;
for mc in 0..m {
acc += jet_flat[[row, mc, t]] * q[[mc, rc]];
}
out_flat[[row, rc, t]] = acc;
}
}
}
let out = out_flat
.into_shape_with_order(ndarray::IxDyn(&out_shape))
.map_err(|err| format!("SubspaceReducedEvaluator: out reshape failed: {err}"))?;
Ok(out)
}
impl SaeBasisEvaluator for SubspaceReducedEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
if n_basis != self.q.ncols() {
return Err(format!(
"SubspaceReducedEvaluator::phi_eta_split: n_basis {n_basis} != reduced width {}",
self.q.ncols()
));
}
let inner_split = self.inner.phi_eta_split(self.q.nrows())?;
let mut inner_curved = vec![false; self.q.nrows()];
for &col in &inner_split.curved_cols {
if col < inner_curved.len() {
inner_curved[col] = true;
}
}
let mut curved = vec![false; self.q.ncols()];
for rc in 0..self.q.ncols() {
for mc in 0..self.q.nrows() {
if inner_curved[mc] && self.q[[mc, rc]] != 0.0 {
curved[rc] = true;
break;
}
}
}
Ok(PhiEtaSplit::from_curved_mask(curved))
}
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
match self.inner.third_jet_dyn(coords) {
Some(Ok(t3)) => {
if let Err(err) = self.check_inner_width(t3.shape()[1], "third_jet_dyn") {
return Some(Err(err));
}
Some(
remix_cols_along_basis(t3.view().into_dyn(), &self.q).and_then(|out| {
out.into_dimensionality::<ndarray::Ix5>().map_err(|err| {
format!("SubspaceReducedEvaluator: third jet dim: {err}")
})
}),
)
}
Some(Err(err)) => Some(Err(err)),
None => None,
}
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
let (phi, jet) = self.inner.evaluate(coords)?;
self.check_inner_width(phi.ncols(), "evaluate")?;
let phi_red = remix_cols_2(&phi, &self.q);
let jet_red = remix_cols_along_basis(jet.view().into_dyn(), &self.q)?
.into_dimensionality::<ndarray::Ix3>()
.map_err(|err| format!("SubspaceReducedEvaluator: jet dim: {err}"))?;
Ok((phi_red, jet_red))
}
}
impl SaeBasisSecondJet for SubspaceReducedEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
let h = self.inner.second_jet(coords)?;
self.check_inner_width(h.shape()[1], "second_jet")?;
remix_cols_along_basis(h.view().into_dyn(), &self.q)?
.into_dimensionality::<ndarray::Ix4>()
.map_err(|err| format!("SubspaceReducedEvaluator: second jet dim: {err}"))
}
}
#[derive(Debug, Clone)]
pub struct AnchorIndicatorEvaluator {
pub anchors: usize,
}
impl AnchorIndicatorEvaluator {
pub fn new(anchors: usize) -> Result<Self, String> {
if anchors < 2 {
return Err(format!(
"AnchorIndicatorEvaluator requires anchors >= 2 (a finite set of at \
least two points); got {anchors}"
));
}
Ok(Self { anchors })
}
}
impl SaeBasisEvaluator for AnchorIndicatorEvaluator {
fn second_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array4<f64>, String>> {
Some(<Self as SaeBasisSecondJet>::second_jet(self, coords))
}
fn third_jet_dyn(&self, coords: ArrayView2<'_, f64>) -> Option<Result<Array5<f64>, String>> {
let n = coords.nrows();
Some(Ok(Array5::<f64>::zeros((n, self.anchors, 1, 1, 1))))
}
fn evaluate(&self, coords: ArrayView2<'_, f64>) -> Result<(Array2<f64>, Array3<f64>), String> {
let n = coords.nrows();
let d = coords.ncols();
if d != 1 {
return Err(format!(
"AnchorIndicatorEvaluator: expected latent_dim == 1 (a single \
categorical axis), got {d}"
));
}
let m = self.anchors;
let mut phi = Array2::<f64>::zeros((n, m));
let jet = Array3::<f64>::zeros((n, m, 1));
for row in 0..n {
let t = coords[[row, 0]];
if !t.is_finite() {
return Err("AnchorIndicatorEvaluator: non-finite coordinate".to_string());
}
let idx = t.round().clamp(0.0, (m - 1) as f64) as usize;
phi[[row, idx]] = 1.0;
}
Ok((phi, jet))
}
}
impl SaeBasisSecondJet for AnchorIndicatorEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
let n = coords.nrows();
Ok(Array4::<f64>::zeros((n, self.anchors, 1, 1)))
}
}
impl SaeBasisThirdJet for AnchorIndicatorEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
let n = coords.nrows();
Ok(Array5::<f64>::zeros((n, self.anchors, 1, 1, 1)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array2, Array3};
fn uniform_stream(seed: u64) -> impl FnMut() -> f64 {
let mut state = seed;
move || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^= z >> 31;
(z >> 11) as f64 / (1u64 << 53) as f64
}
}
fn heldout_r2(
phi: &Array2<f64>,
target: &Array2<f64>,
train_rows: &[usize],
test_rows: &[usize],
) -> f64 {
use faer::Side;
use gam_linalg::faer_ndarray::FaerCholesky;
let p = phi.ncols();
let q = target.ncols();
let mut gram = Array2::<f64>::zeros((p, p));
let mut rhs = Array2::<f64>::zeros((p, q));
for &row in train_rows {
for a in 0..p {
for b in 0..p {
gram[[a, b]] += phi[[row, a]] * phi[[row, b]];
}
for c in 0..q {
rhs[[a, c]] += phi[[row, a]] * target[[row, c]];
}
}
}
let scale = gram.diag().iter().copied().fold(0.0_f64, f64::max);
for d in gram.diag_mut().iter_mut() {
*d += scale * 64.0 * f64::EPSILON;
}
let decoder = gram.cholesky(Side::Lower).unwrap().solve_mat(&rhs);
let mut mean = vec![0.0_f64; q];
for &row in test_rows {
for c in 0..q {
mean[c] += target[[row, c]];
}
}
for v in mean.iter_mut() {
*v /= test_rows.len() as f64;
}
let (mut residual, mut total) = (0.0_f64, 0.0_f64);
for &row in test_rows {
for c in 0..q {
let mut fit = 0.0_f64;
for a in 0..p {
fit += phi[[row, a]] * decoder[[a, c]];
}
residual += (target[[row, c]] - fit).powi(2);
total += (target[[row, c]] - mean[c]).powi(2);
}
}
1.0 - residual / total
}
fn rotate_vector(v: [f64; 3], axis: [f64; 3], theta: f64) -> [f64; 3] {
let norm = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
let k = [axis[0] / norm, axis[1] / norm, axis[2] / norm];
let (s, c) = theta.sin_cos();
let kv = k[0] * v[0] + k[1] * v[1] + k[2] * v[2];
let cross = [
k[1] * v[2] - k[2] * v[1],
k[2] * v[0] - k[0] * v[2],
k[0] * v[1] - k[1] * v[0],
];
[
v[0] * c + cross[0] * s + k[0] * kv * (1.0 - c),
v[1] * c + cross[1] * s + k[1] * kv * (1.0 - c),
v[2] * c + cross[2] * s + k[2] * kv * (1.0 - c),
]
}
fn span_residual(phi: &Array2<f64>, target: &Array2<f64>) -> f64 {
use faer::Side;
use gam_linalg::faer_ndarray::FaerCholesky;
let p = phi.ncols();
let q = target.ncols();
let mut gram = Array2::<f64>::zeros((p, p));
let mut rhs = Array2::<f64>::zeros((p, q));
for row in 0..phi.nrows() {
for a in 0..p {
for b in 0..p {
gram[[a, b]] += phi[[row, a]] * phi[[row, b]];
}
for c in 0..q {
rhs[[a, c]] += phi[[row, a]] * target[[row, c]];
}
}
}
let scale = gram.diag().iter().copied().fold(0.0_f64, f64::max);
for d in gram.diag_mut().iter_mut() {
*d += scale * 64.0 * f64::EPSILON;
}
let coefficients = gram.cholesky(Side::Lower).unwrap().solve_mat(&rhs);
let fitted = phi.dot(&coefficients);
let (mut residual, mut total) = (0.0_f64, 0.0_f64);
for row in 0..target.nrows() {
for c in 0..q {
residual += (target[[row, c]] - fitted[[row, c]]).powi(2);
total += target[[row, c]].powi(2);
}
}
(residual / total.max(f64::MIN_POSITIVE)).sqrt()
}
fn sphere_sample(seed: u64, n: usize) -> (Array2<f64>, Array2<f64>) {
let mut rng = uniform_stream(seed);
let mut chart = Array2::<f64>::zeros((n, 2));
let mut ambient = Array2::<f64>::zeros((n, 3));
for row in 0..n {
let lat = (2.0 * rng() - 1.0).asin();
let lon = 2.0 * std::f64::consts::PI * rng();
chart[[row, 0]] = lat;
chart[[row, 1]] = lon;
ambient[[row, 0]] = lat.cos() * lon.cos();
ambient[[row, 1]] = lat.cos() * lon.sin();
ambient[[row, 2]] = lat.sin();
}
(chart, ambient)
}
#[test]
fn ambient_sphere_matches_chart_on_the_sphere() {
let (chart_coords, ambient_coords) = sphere_sample(0x5B4E, 512);
let chart = SphericalHarmonicEvaluator::new(3).unwrap();
let ambient = AmbientSphereHarmonicEvaluator::new(3).unwrap();
assert_eq!(chart.basis_size(), ambient.basis_size());
let (phi_chart, _) = chart.evaluate(chart_coords.view()).unwrap();
let (phi_ambient, _) = ambient.evaluate(ambient_coords.view()).unwrap();
let mut worst = 0.0_f64;
for row in 0..phi_chart.nrows() {
for col in 0..phi_chart.ncols() {
worst = worst.max((phi_chart[[row, col]] - phi_ambient[[row, col]]).abs());
}
}
assert!(
worst <= 1.0e-12,
"ambient and chart spherical harmonics disagree on S²: max |Δ| = {worst:.3e}"
);
}
#[test]
fn ambient_sphere_jets_match_finite_differences() {
let (_, ambient_coords) = sphere_sample(0xDEF7, 24);
let ambient = AmbientSphereHarmonicEvaluator::new(3).unwrap();
let (_, jet) = ambient.evaluate(ambient_coords.view()).unwrap();
let hessian = ambient.second_jet(ambient_coords.view()).unwrap();
let h = 1.0e-5_f64;
let (mut worst_first, mut worst_second) = (0.0_f64, 0.0_f64);
for row in 0..ambient_coords.nrows() {
let base = [
ambient_coords[[row, 0]],
ambient_coords[[row, 1]],
ambient_coords[[row, 2]],
];
for axis_a in 0..3 {
let (mut plus, mut minus) = (base, base);
plus[axis_a] += h;
minus[axis_a] -= h;
let (phi_plus, jet_plus) =
ambient.evaluate(at_coords(plus).view()).unwrap();
let (phi_minus, jet_minus) =
ambient.evaluate(at_coords(minus).view()).unwrap();
for col in 0..ambient.basis_size() {
let fd = (phi_plus[[0, col]] - phi_minus[[0, col]]) / (2.0 * h);
worst_first = worst_first.max((fd - jet[[row, col, axis_a]]).abs());
}
for col in 0..ambient.basis_size() {
for axis_b in 0..3 {
let fd = (jet_plus[[0, col, axis_b]] - jet_minus[[0, col, axis_b]])
/ (2.0 * h);
worst_second =
worst_second.max((fd - hessian[[row, col, axis_b, axis_a]]).abs());
}
}
}
}
assert!(
worst_first <= 1.0e-6,
"ambient first jet disagrees with finite differences: {worst_first:.3e}"
);
assert!(
worst_second <= 1.0e-5,
"ambient second jet disagrees with finite differences: {worst_second:.3e}"
);
}
fn at_coords(point: [f64; 3]) -> Array2<f64> {
let mut single = Array2::<f64>::zeros((1, 3));
for axis in 0..3 {
single[[0, axis]] = point[axis];
}
single
}
#[test]
fn ambient_sphere_has_no_pole_degeneracy() {
let ambient = AmbientSphereHarmonicEvaluator::new(2).unwrap();
let chart = SphericalHarmonicEvaluator::new(2).unwrap();
for &pole_z in &[1.0_f64, -1.0_f64] {
let (_, jet) = ambient.evaluate(at_coords([0.0, 0.0, pole_z]).view()).unwrap();
let mut chart_coords = Array2::<f64>::zeros((1, 2));
chart_coords[[0, 0]] = pole_z * std::f64::consts::FRAC_PI_2;
chart_coords[[0, 1]] = 0.9;
let (_, chart_jet) = chart.evaluate(chart_coords.view()).unwrap();
let mut chart_longitude = 0.0_f64;
for col in 0..chart.basis_size() {
chart_longitude = chart_longitude.max(chart_jet[[0, col, 1]].abs());
}
assert!(
chart_longitude <= 1.0e-12,
"chart longitude jet should collapse at the pole (that is the defect); got {chart_longitude:.3e}"
);
let mut ambient_tangential = 0.0_f64;
for col in 0..ambient.basis_size() {
for axis in 0..2 {
ambient_tangential = ambient_tangential.max(jet[[0, col, axis]].abs());
assert!(
jet[[0, col, axis]].is_finite(),
"ambient jet must stay finite at the pole"
);
}
}
assert!(
ambient_tangential >= 0.1,
"ambient basis must carry tangential signal AT the pole; got {ambient_tangential:.3e}"
);
}
}
#[test]
fn ambient_sphere_span_is_rotation_closed_and_a_truncated_block_is_not() {
let (chart_coords, ambient_coords) = sphere_sample(0x120F, 900);
let axis = [1.0, 1.0, 1.0];
let theta = 0.7_f64;
let mut rotated_ambient = Array2::<f64>::zeros(ambient_coords.dim());
let mut rotated_chart = Array2::<f64>::zeros(chart_coords.dim());
for row in 0..ambient_coords.nrows() {
let turned = rotate_vector(
[
ambient_coords[[row, 0]],
ambient_coords[[row, 1]],
ambient_coords[[row, 2]],
],
axis,
theta,
);
for a in 0..3 {
rotated_ambient[[row, a]] = turned[a];
}
rotated_chart[[row, 0]] = turned[2].clamp(-1.0, 1.0).asin();
rotated_chart[[row, 1]] = turned[1].atan2(turned[0]);
}
let ambient = AmbientSphereHarmonicEvaluator::new(2).unwrap();
let (phi, _) = ambient.evaluate(ambient_coords.view()).unwrap();
let (phi_rotated, _) = ambient.evaluate(rotated_ambient.view()).unwrap();
let ambient_residual = span_residual(&phi, &phi_rotated);
assert!(
ambient_residual <= 1.0e-8,
"ambient harmonic span must be closed under SO(3); residual {ambient_residual:.3e}"
);
let keep: Vec<usize> = (0..phi.ncols()).filter(|&c| c != 7 && c != 8).collect();
let truncate = |full: &Array2<f64>| -> Array2<f64> {
let mut out = Array2::<f64>::zeros((full.nrows(), keep.len()));
for (target, &source) in keep.iter().enumerate() {
for row in 0..full.nrows() {
out[[row, target]] = full[[row, source]];
}
}
out
};
let partial_residual = span_residual(&truncate(&phi), &truncate(&phi_rotated));
assert!(
partial_residual >= 1.0e-3,
"a degree-2 block missing two of its five harmonics must NOT be \
rotation-closed; if this passes the control is vacuous and the \
positive assertion above proves nothing. residual {partial_residual:.3e}"
);
}
#[test]
fn ambient_sphere_basis_is_closed_under_its_own_killing_fields() {
let (_, ambient) = sphere_sample(0x50F3, 700);
let evaluator = AmbientSphereHarmonicEvaluator::new(3).unwrap();
let (phi, jet) = evaluator.evaluate(ambient.view()).unwrap();
let width = evaluator.basis_size();
let n = ambient.nrows();
for axis in 0..3 {
let mut derivative = Array2::<f64>::zeros((n, width));
for row in 0..n {
let u = [ambient[[row, 0]], ambient[[row, 1]], ambient[[row, 2]]];
let k = match axis {
0 => [0.0, -u[2], u[1]],
1 => [u[2], 0.0, -u[0]],
_ => [-u[1], u[0], 0.0],
};
for col in 0..width {
let mut acc = 0.0_f64;
for a in 0..3 {
acc += jet[[row, col, a]] * k[a];
}
derivative[[row, col]] = acc;
}
}
let residual = span_residual(&phi, &derivative);
assert!(
residual <= 1.0e-9,
"Killing generator {axis} carries the ambient sphere basis OUT of its own \
span (relative residual {residual:.3e}). The atom's representable function \
space would then depend on where the coordinate origin was placed, and the \
atom could not be certified on the exact-orbit path."
);
}
}
fn derivative_along_field(
jet: &Array3<f64>,
field: &Array2<f64>,
) -> Array2<f64> {
let (n, width, d) = jet.dim();
let mut out = Array2::<f64>::zeros((n, width));
for row in 0..n {
for col in 0..width {
let mut acc = 0.0_f64;
for axis in 0..d {
acc += jet[[row, col, axis]] * field[[row, axis]];
}
out[[row, col]] = acc;
}
}
out
}
fn constant_field(n: usize, d: usize, axis: usize) -> Array2<f64> {
let mut field = Array2::<f64>::zeros((n, d));
for row in 0..n {
field[[row, axis]] = 1.0;
}
field
}
#[test]
fn every_topology_basis_is_closed_under_its_declared_killing_fields() {
let n = 400usize;
let mut rng = uniform_stream(0xC0FFEE);
struct FlatCase {
name: &'static str,
evaluator: Box<dyn SaeBasisEvaluator>,
dim: usize,
symmetric_axes: &'static [usize],
asymmetric_axes: &'static [usize],
span: fn(usize, f64) -> f64,
}
fn unit_span(axis: usize, u: f64) -> f64 {
std::hint::black_box(axis);
u
}
fn mobius_span(axis: usize, u: f64) -> f64 {
if axis == 0 { 2.0 * u } else { 2.0 * u - 1.0 }
}
let cases: Vec<FlatCase> = vec![
FlatCase {
name: "periodic S1",
evaluator: Box::new(PeriodicHarmonicEvaluator::new(7).unwrap()),
dim: 1,
symmetric_axes: &[0],
asymmetric_axes: &[],
span: unit_span,
},
FlatCase {
name: "flat torus T2",
evaluator: Box::new(TorusHarmonicEvaluator::new(2, 3).unwrap()),
dim: 2,
symmetric_axes: &[0, 1],
asymmetric_axes: &[],
span: unit_span,
},
FlatCase {
name: "cylinder S1 x R",
evaluator: Box::new(CylinderHarmonicEvaluator::new(3, 2).unwrap()),
dim: 2,
symmetric_axes: &[0, 1],
asymmetric_axes: &[],
span: unit_span,
},
FlatCase {
name: "mobius band",
evaluator: Box::new(MobiusHarmonicEvaluator::new(3, 2).unwrap()),
dim: 2,
symmetric_axes: &[0],
asymmetric_axes: &[1],
span: mobius_span,
},
FlatCase {
name: "klein bottle",
evaluator: Box::new(QuotientSpectralEvaluator::klein_bottle(3).unwrap()),
dim: 2,
symmetric_axes: &[0],
asymmetric_axes: &[1],
span: unit_span,
},
FlatCase {
name: "euclidean patch",
evaluator: Box::new(EuclideanPatchEvaluator::new(2, 2).unwrap()),
dim: 2,
symmetric_axes: &[0, 1],
asymmetric_axes: &[],
span: |_, u| 4.0 * u - 2.0,
},
];
for case in &cases {
let mut coords = Array2::<f64>::zeros((n, case.dim));
for row in 0..n {
for axis in 0..case.dim {
coords[[row, axis]] = (case.span)(axis, rng());
}
}
let (phi, jet) = case.evaluator.evaluate(coords.view()).unwrap();
for &axis in case.symmetric_axes {
let field = constant_field(n, case.dim, axis);
let derivative = derivative_along_field(&jet, &field);
let residual = span_residual(&phi, &derivative);
assert!(
residual <= 1.0e-8,
"{}: translation along axis {axis} is a declared isometry, but it \
carries the basis OUT of its own span (residual {residual:.3e})",
case.name
);
}
for &axis in case.asymmetric_axes {
let field = constant_field(n, case.dim, axis);
let derivative = derivative_along_field(&jet, &field);
let residual = span_residual(&phi, &derivative);
assert!(
residual >= 1.0e-6,
"{}: axis {axis} is NOT a declared isometry, so closure here would \
mean the basis carries a symmetry the manifold does not have \
(residual {residual:.3e})",
case.name
);
}
}
let (_, ambient) = sphere_sample(0x511E, n);
let spherical: Vec<(&str, Box<dyn SaeBasisEvaluator>)> = vec![
(
"ambient sphere",
Box::new(AmbientSphereHarmonicEvaluator::new(2).unwrap()),
),
(
"ambient RP2",
Box::new(QuotientSpectralEvaluator::projective_plane_ambient(1).unwrap()),
),
];
for (name, evaluator) in &spherical {
let (phi, jet) = evaluator.evaluate(ambient.view()).unwrap();
for generator in 0..3 {
let mut field = Array2::<f64>::zeros((n, 3));
for row in 0..n {
let u = [
ambient[[row, 0]],
ambient[[row, 1]],
ambient[[row, 2]],
];
let k = match generator {
0 => [0.0, -u[2], u[1]],
1 => [u[2], 0.0, -u[0]],
_ => [-u[1], u[0], 0.0],
};
for axis in 0..3 {
field[[row, axis]] = k[axis];
}
}
let derivative = derivative_along_field(&jet, &field);
let residual = span_residual(&phi, &derivative);
assert!(
residual <= 1.0e-8,
"{name}: SO(3) generator {generator} carries the basis out of its own \
span (residual {residual:.3e})"
);
}
}
}
#[test]
fn spherical_harmonic_recovers_bandlimited_field_and_beats_fixed_chart() {
let mut rng = uniform_stream(0xACE1);
let n = 3000usize;
let mut coords = Array2::<f64>::zeros((n, 2));
for row in 0..n {
coords[[row, 0]] = (2.0 * rng() - 1.0).asin();
coords[[row, 1]] = std::f64::consts::TAU * rng();
}
let planted = SphericalHarmonicEvaluator::new(3).unwrap();
let (basis3, _) = planted.evaluate(coords.view()).unwrap();
let p_out = 6usize;
let mut decoder = Array2::<f64>::zeros((basis3.ncols(), p_out));
for a in 0..basis3.ncols() {
for c in 0..p_out {
decoder[[a, c]] = 2.0 * rng() - 1.0;
}
}
let target = basis3.dot(&decoder);
let test_rows: Vec<usize> = (0..n).filter(|r| r % 4 == 0).collect();
let train_rows: Vec<usize> = (0..n).filter(|r| r % 4 != 0).collect();
let sh_r2 = heldout_r2(&basis3, &target, &train_rows, &test_rows);
let low_band = SphericalHarmonicEvaluator::new(1).unwrap();
let (fixed_phi, _) = low_band.evaluate(coords.view()).unwrap();
let fixed_r2 = heldout_r2(&fixed_phi, &target, &train_rows, &test_rows);
assert!(
sh_r2 > 0.999,
"the spherical-harmonic basis must reconstruct a band-limited sphere \
field to near-exactness; SH held-out R²={sh_r2}, degree-1 R²={fixed_r2}"
);
assert!(
fixed_r2 < 0.9,
"the fixed degree-2 chart must NOT reach the higher-degree field \
(its span omits two quadrupoles and every l≥3 mode); \
SH held-out R²={sh_r2}, degree-1 R²={fixed_r2}"
);
assert!(
sh_r2 > fixed_r2,
"the spherical-harmonic basis must beat the fixed chart on held-out \
reconstruction; SH held-out R²={sh_r2}, degree-1 R²={fixed_r2}"
);
let mut noisy = target.clone();
for row in 0..n {
for c in 0..p_out {
noisy[[row, c]] += 0.01 * (2.0 * rng() - 1.0);
}
}
let selected = select_spherical_harmonic_degree(coords.view(), noisy.view(), 6).unwrap();
assert_eq!(
selected, 3,
"spectral-noise-floor bandwidth selection must recover the planted degree 3"
);
}
#[test]
fn spherical_harmonic_jets_match_finite_differences() {
let evaluator = SphericalHarmonicEvaluator::new(4).unwrap();
let coords =
Array2::from_shape_vec((4, 2), vec![0.3, 0.7, -0.9, 2.1, 1.2, -1.3, -0.1, 4.0])
.unwrap();
let h = 1e-5;
let m = evaluator.basis_size();
let hess = evaluator.second_jet(coords.view()).unwrap();
let third = evaluator.third_jet(coords.view()).unwrap();
let shifted = |axis: usize, step: f64| -> Array2<f64> {
let mut c = coords.clone();
for row in 0..c.nrows() {
c[[row, axis]] += step;
}
c
};
let mut max_h_err = 0.0_f64;
for axis in 0..2 {
let (_, jp) = evaluator.evaluate(shifted(axis, h).view()).unwrap();
let (_, jm) = evaluator.evaluate(shifted(axis, -h).view()).unwrap();
for row in 0..coords.nrows() {
for col in 0..m {
for other in 0..2 {
let fd = (jp[[row, col, other]] - jm[[row, col, other]]) / (2.0 * h);
max_h_err = max_h_err.max((hess[[row, col, other, axis]] - fd).abs());
}
}
}
}
assert!(
max_h_err < 1e-4,
"spherical-harmonic Hessian must match FD of the first jet; max err {max_h_err}"
);
let mut max_t_err = 0.0_f64;
for axis in 0..2 {
let hp = evaluator.second_jet(shifted(axis, h).view()).unwrap();
let hm = evaluator.second_jet(shifted(axis, -h).view()).unwrap();
for row in 0..coords.nrows() {
for col in 0..m {
for a in 0..2 {
for b in 0..2 {
let fd = (hp[[row, col, a, b]] - hm[[row, col, a, b]]) / (2.0 * h);
max_t_err = max_t_err.max((third[[row, col, a, b, axis]] - fd).abs());
}
}
}
}
}
assert!(
max_t_err < 1e-4,
"spherical-harmonic third jet must match FD of the Hessian; max err {max_t_err}"
);
}
fn assert_machine_close(actual: f64, expected: f64, label: &str) {
let tolerance = 4096.0 * f64::EPSILON * (1.0 + actual.abs().max(expected.abs()));
assert!(
(actual - expected).abs() <= tolerance,
"{label}: actual={actual:.17e}, expected={expected:.17e}, tolerance={tolerance:.3e}"
);
}
fn assert_spectral_deck_close(
actual: f64,
expected: f64,
laplace_eigenvalue: f64,
derivative_order: i32,
label: &str,
) {
let angular_frequency = std::f64::consts::TAU * laplace_eigenvalue.sqrt();
let derivative_envelope = if derivative_order == 0 {
1.0
} else {
angular_frequency.powi(derivative_order)
};
let tolerance =
64.0 * f64::EPSILON * (1.0 + derivative_envelope + actual.abs().max(expected.abs()));
assert!(
(actual - expected).abs() <= tolerance,
"{label}: actual={actual:.17e}, expected={expected:.17e}, spectral envelope={derivative_envelope:.6e}, tolerance={tolerance:.3e}"
);
}
fn assert_group_average_equals_diagonal_restriction(
evaluator: &QuotientSpectralEvaluator,
cover: &dyn SaeBasisEvaluator,
cover_eigenvalues: &[f64],
coords: &Array2<f64>,
twins: &Array2<f64>,
) {
let (cover_phi, _) = cover.evaluate(coords.view()).unwrap();
let (twin_cover_phi, _) = cover.evaluate(twins.view()).unwrap();
let (quotient_phi, _) = evaluator.evaluate(coords.view()).unwrap();
assert_eq!(cover_phi.dim(), twin_cover_phi.dim());
assert_eq!(cover_phi.ncols(), evaluator.cover_width());
assert_eq!(cover_eigenvalues.len(), evaluator.cover_width());
assert_eq!(quotient_phi.ncols(), evaluator.basis_size());
let mut quotient_column = 0usize;
for cover_column in 0..cover_phi.ncols() {
let retained =
evaluator.cover_columns().get(quotient_column).copied() == Some(cover_column);
for row in 0..cover_phi.nrows() {
let group_average =
0.5 * (cover_phi[[row, cover_column]] + twin_cover_phi[[row, cover_column]]);
if retained {
assert_spectral_deck_close(
group_average,
cover_phi[[row, cover_column]],
cover_eigenvalues[cover_column],
0,
"trivial-character column fixed by exact group average",
);
assert_spectral_deck_close(
quotient_phi[[row, quotient_column]],
group_average,
cover_eigenvalues[cover_column],
0,
"quotient restriction equals exact group average",
);
} else {
assert_spectral_deck_close(
group_average,
0.0,
cover_eigenvalues[cover_column],
0,
"sign-character column annihilated by exact group average",
);
}
}
if retained {
quotient_column += 1;
}
}
assert_eq!(quotient_column, evaluator.basis_size());
}
fn assert_quotient_deck_covariance(
evaluator: &QuotientSpectralEvaluator,
coords: &Array2<f64>,
twins: &Array2<f64>,
deck_jacobian_diagonal: [f64; 2],
) {
let (phi, jet) = evaluator.evaluate(coords.view()).unwrap();
let (twin_phi, twin_jet) = evaluator.evaluate(twins.view()).unwrap();
let hessian = evaluator.second_jet(coords.view()).unwrap();
let twin_hessian = evaluator.second_jet(twins.view()).unwrap();
let third = evaluator.third_jet(coords.view()).unwrap();
let twin_third = evaluator.third_jet(twins.view()).unwrap();
for row in 0..coords.nrows() {
for column in 0..evaluator.basis_size() {
assert_spectral_deck_close(
twin_phi[[row, column]],
phi[[row, column]],
evaluator.laplace_eigenvalues()[column],
0,
"deck-invariant quotient value",
);
for axis_a in 0..2 {
assert_spectral_deck_close(
twin_jet[[row, column, axis_a]] * deck_jacobian_diagonal[axis_a],
jet[[row, column, axis_a]],
evaluator.laplace_eigenvalues()[column],
1,
"deck-covariant quotient first jet",
);
for axis_b in 0..2 {
assert_spectral_deck_close(
twin_hessian[[row, column, axis_a, axis_b]]
* deck_jacobian_diagonal[axis_a]
* deck_jacobian_diagonal[axis_b],
hessian[[row, column, axis_a, axis_b]],
evaluator.laplace_eigenvalues()[column],
2,
"deck-covariant quotient second jet",
);
for axis_c in 0..2 {
assert_spectral_deck_close(
twin_third[[row, column, axis_a, axis_b, axis_c]]
* deck_jacobian_diagonal[axis_a]
* deck_jacobian_diagonal[axis_b]
* deck_jacobian_diagonal[axis_c],
third[[row, column, axis_a, axis_b, axis_c]],
evaluator.laplace_eigenvalues()[column],
3,
"deck-covariant quotient third jet",
);
}
}
}
}
}
}
fn assert_exact_quotient_spectral_contract(evaluator: &QuotientSpectralEvaluator) {
let gram = evaluator.function_space_gram();
let penalty = evaluator.spectral_penalty(2).unwrap();
assert_eq!(gram.dim(), (evaluator.basis_size(), evaluator.basis_size()));
assert_eq!(penalty.dim(), gram.dim());
assert_eq!(evaluator.nullspace_dimension(), 1);
let mut positive_penalty_diagonal = 0usize;
for row in 0..evaluator.basis_size() {
assert!(gram[[row, row]] > 0.0);
assert_machine_close(
penalty[[row, row]],
gram[[row, row]] * evaluator.laplace_eigenvalues()[row].powi(2),
"restricted spectral penalty",
);
if penalty[[row, row]] > 0.0 {
positive_penalty_diagonal += 1;
}
for column in 0..evaluator.basis_size() {
if row != column {
assert_eq!(gram[[row, column]], 0.0);
assert_eq!(penalty[[row, column]], 0.0);
}
}
}
assert_eq!(
positive_penalty_diagonal,
evaluator.basis_size() - 1,
"the exact restricted penalty null space must contain only constants"
);
}
#[test]
fn quotient_spectral_projective_plane_group_average_jets_penalty_and_null_are_exact() {
let harmonic_order = 3;
let evaluator = QuotientSpectralEvaluator::projective_plane(harmonic_order).unwrap();
assert_eq!(evaluator.quotient_name(), "projective-plane");
assert_eq!(evaluator.basis_size(), 28);
let cover = SphericalHarmonicEvaluator::new(2 * harmonic_order).unwrap();
let cover_modes = cover.spectral_modes();
assert_eq!(evaluator.cover_width(), cover.basis_size());
for (quotient_column, &cover_column) in evaluator.cover_columns().iter().enumerate() {
assert_eq!(
evaluator.laplace_eigenvalues()[quotient_column],
cover_modes[cover_column].laplace_eigenvalue
);
assert_eq!(
evaluator.l2_gram_weights()[quotient_column],
cover_modes[cover_column].l2_gram_weight
);
}
let split = evaluator.phi_eta_split(evaluator.basis_size()).unwrap();
assert_eq!(split.base_cols.len(), 6, "l=0 and l=2 form the RP2 core");
assert_exact_quotient_spectral_contract(&evaluator);
let coords = Array2::from_shape_vec(
(5, 2),
vec![-1.2, -2.8, -0.47, -0.31, 0.0, 0.91, 0.63, 2.17, 1.31, 5.4],
)
.unwrap();
let mut twins = coords.clone();
for row in 0..twins.nrows() {
twins[[row, 0]] = -twins[[row, 0]];
twins[[row, 1]] += std::f64::consts::PI;
}
let cover_eigenvalues = cover_modes
.iter()
.map(|mode| mode.laplace_eigenvalue)
.collect::<Vec<_>>();
assert_group_average_equals_diagonal_restriction(
&evaluator,
&cover,
&cover_eigenvalues,
&coords,
&twins,
);
assert_quotient_deck_covariance(&evaluator, &coords, &twins, [-1.0, 1.0]);
}
#[test]
fn quotient_spectral_klein_group_average_jets_penalty_and_null_are_exact() {
let num_harmonics = 3;
let evaluator = QuotientSpectralEvaluator::klein_bottle(num_harmonics).unwrap();
assert_eq!(evaluator.quotient_name(), "klein-bottle");
assert_eq!(evaluator.basis_size(), 24);
let cover = TorusHarmonicEvaluator::new(2, num_harmonics).unwrap();
let cover_modes = cover.spectral_modes();
assert_eq!(evaluator.cover_width(), cover.basis_size());
for (quotient_column, &cover_column) in evaluator.cover_columns().iter().enumerate() {
assert_eq!(
evaluator.laplace_eigenvalues()[quotient_column],
cover_modes[cover_column].laplace_eigenvalue
);
assert_eq!(
evaluator.l2_gram_weights()[quotient_column],
cover_modes[cover_column].l2_gram_weight
);
}
let split = evaluator.phi_eta_split(evaluator.basis_size()).unwrap();
assert_eq!(
split.base_cols.len(),
7,
"the standard R4 Klein embedding is one constant plus six coordinates"
);
assert_eq!(split.curved_cols.len(), evaluator.basis_size() - 7);
let mut embedding_category_counts = [0usize; 4];
for quotient_column in split.base_cols {
let cover_mode = &cover_modes[evaluator.cover_columns()[quotient_column]];
let theta = cover_mode.components[0];
let phi = cover_mode.components[1];
match (theta.harmonic(), phi.harmonic(), phi.reflection_sign()) {
(0, 0, 1) => embedding_category_counts[0] += 1,
(2, 0, 1) => embedding_category_counts[1] += 1,
(2, 1, 1) => embedding_category_counts[2] += 1,
(1, 1, -1) => embedding_category_counts[3] += 1,
identity => panic!("unexpected Klein embedding-base character {identity:?}"),
}
}
assert_eq!(embedding_category_counts, [1, 2, 2, 2]);
assert_exact_quotient_spectral_contract(&evaluator);
let coords = Array2::from_shape_vec(
(5, 2),
vec![-0.3, -0.41, 0.0, 0.0, 0.17, 0.29, 0.73, -0.88, 1.21, 1.7],
)
.unwrap();
let mut twins = coords.clone();
for row in 0..twins.nrows() {
twins[[row, 0]] += 0.5;
twins[[row, 1]] = -twins[[row, 1]];
}
let cover_eigenvalues = cover_modes
.iter()
.map(|mode| mode.laplace_eigenvalue)
.collect::<Vec<_>>();
assert_group_average_equals_diagonal_restriction(
&evaluator,
&cover,
&cover_eigenvalues,
&coords,
&twins,
);
assert_quotient_deck_covariance(&evaluator, &coords, &twins, [1.0, -1.0]);
}
#[test]
fn quotient_and_cover_constructors_enforce_minimum_order_and_checked_widths() {
assert!(projective_plane_basis_size(0).is_err());
assert!(klein_bottle_basis_size(0).is_err());
assert!(QuotientSpectralEvaluator::projective_plane(0).is_err());
assert!(QuotientSpectralEvaluator::klein_bottle(0).is_err());
assert!(QuotientSpectralEvaluator::klein_bottle(1).is_err());
let projective_plane = QuotientSpectralEvaluator::projective_plane(1).unwrap();
assert_eq!(projective_plane.basis_size(), 6);
assert_eq!(
projective_plane.basis_size(),
projective_plane_basis_size(1).unwrap()
);
let klein_bottle = QuotientSpectralEvaluator::klein_bottle(2).unwrap();
assert_eq!(klein_bottle.basis_size(), 13);
assert_eq!(
klein_bottle.basis_size(),
klein_bottle_basis_size(2).unwrap()
);
assert_eq!(
klein_bottle
.phi_eta_split(klein_bottle.basis_size())
.unwrap()
.base_cols
.len(),
7
);
assert!(projective_plane_basis_size(usize::MAX).is_err());
assert!(klein_bottle_basis_size(usize::MAX).is_err());
assert!(QuotientSpectralEvaluator::projective_plane(usize::MAX).is_err());
assert!(QuotientSpectralEvaluator::klein_bottle(usize::MAX).is_err());
assert!(SphericalHarmonicEvaluator::new(usize::MAX).is_err());
assert!(TorusHarmonicEvaluator::new(1, usize::MAX).is_err());
assert!(TorusHarmonicEvaluator::new(usize::BITS as usize, 1).is_err());
}
#[test]
fn mobius_basis_is_invariant_under_deck_transform() {
let evaluator = MobiusHarmonicEvaluator::new(3, 2).unwrap();
let coords = Array2::from_shape_vec(
(5, 2),
vec![0.0, -0.8, 0.17, -0.3, 0.51, 0.0, 0.88, 0.4, 1.41, 0.9],
)
.unwrap();
let mut twins = coords.clone();
for row in 0..twins.nrows() {
twins[[row, 0]] += 1.0;
twins[[row, 1]] = -twins[[row, 1]];
}
let (phi, _) = evaluator.evaluate(coords.view()).unwrap();
let (phi_twin, _) = evaluator.evaluate(twins.view()).unwrap();
let max_error = (&phi - &phi_twin)
.iter()
.map(|value| value.abs())
.fold(0.0_f64, f64::max);
assert!(
max_error <= 32.0 * f64::EPSILON,
"every basis column must descend to the Möbius quotient; max deck error {max_error}"
);
}
fn assert_into_matches_evaluate(eval: &dyn SaeBasisEvaluator, coords: &Array2<f64>) {
let (phi_ref, jet_ref) = eval.evaluate(coords.view()).expect("evaluate");
let mut phi = Array2::<f64>::from_elem(phi_ref.dim(), 999.0);
let mut jet = Array3::<f64>::from_elem(jet_ref.dim(), 999.0);
eval.evaluate_into(&mut phi, &mut jet, coords.view())
.expect("evaluate_into");
assert_eq!(
phi, phi_ref,
"evaluate_into Φ must equal evaluate Φ exactly"
);
assert_eq!(
jet, jet_ref,
"evaluate_into jet must equal evaluate jet exactly"
);
}
fn assert_workspace_reuse(
eval: &dyn SaeBasisEvaluator,
coords_a: &Array2<f64>,
coords_b: &Array2<f64>,
) {
let (phi_a, jet_a) = eval.evaluate(coords_a.view()).expect("evaluate a");
let mut phi = Array2::<f64>::zeros(phi_a.dim());
let mut jet = Array3::<f64>::zeros(jet_a.dim());
eval.evaluate_into(&mut phi, &mut jet, coords_a.view())
.expect("into a");
assert_eq!(phi, phi_a);
assert_eq!(jet, jet_a);
let (phi_b_ref, jet_b_ref) = eval.evaluate(coords_b.view()).expect("evaluate b");
eval.evaluate_into(&mut phi, &mut jet, coords_b.view())
.expect("into b (reused workspace)");
assert_eq!(
phi, phi_b_ref,
"reused workspace Φ must not carry stale data"
);
assert_eq!(
jet, jet_b_ref,
"reused workspace jet must not carry stale data"
);
}
#[test]
fn periodic_harmonic_evaluate_into_matches() {
let eval = PeriodicHarmonicEvaluator::new(5).unwrap();
let coords_a = Array2::from_shape_vec((4, 1), vec![0.10, 0.35, 0.60, 0.85]).unwrap();
let coords_b = Array2::from_shape_vec((4, 1), vec![0.20, 0.45, 0.70, 0.05]).unwrap();
assert_into_matches_evaluate(&eval, &coords_a);
assert_workspace_reuse(&eval, &coords_a, &coords_b);
}
#[test]
fn euclidean_patch_evaluate_into_matches() {
let eval = EuclideanPatchEvaluator::new(2, 2).unwrap();
let coords_a =
Array2::from_shape_vec((4, 2), vec![0.1, -0.2, 0.3, 0.4, -0.5, 0.6, 0.7, -0.8])
.unwrap();
let coords_b =
Array2::from_shape_vec((4, 2), vec![-0.3, 0.9, 0.2, -0.1, 0.5, 0.5, -0.7, 0.3])
.unwrap();
assert_into_matches_evaluate(&eval, &coords_a);
assert_workspace_reuse(&eval, &coords_a, &coords_b);
}
#[test]
fn torus_harmonic_evaluate_into_matches() {
let eval = TorusHarmonicEvaluator::new(2, 2).unwrap();
let coords_a =
Array2::from_shape_vec((4, 2), vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]).unwrap();
let coords_b =
Array2::from_shape_vec((4, 2), vec![0.9, 0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65])
.unwrap();
assert_into_matches_evaluate(&eval, &coords_a);
assert_workspace_reuse(&eval, &coords_a, &coords_b);
}
#[test]
fn default_evaluate_into_matches_for_unspecialized_evaluator() {
let eval = AmbientSphereHarmonicEvaluator::new(2).unwrap();
let coords_a =
Array2::from_shape_vec((3, 3), vec![0.2, 0.5, -0.4, 1.1, 0.9, -0.7, 0.3, -0.2, 0.8])
.unwrap();
let coords_b =
Array2::from_shape_vec((3, 3), vec![-0.1, 0.3, 0.6, -0.9, -0.5, 0.8, 0.4, 0.1, -0.6])
.unwrap();
assert_into_matches_evaluate(&eval, &coords_a);
assert_workspace_reuse(&eval, &coords_a, &coords_b);
}
#[test]
fn evaluate_into_rejects_mismatched_buffer() {
let eval = PeriodicHarmonicEvaluator::new(5).unwrap();
let coords = Array2::from_shape_vec((4, 1), vec![0.1, 0.2, 0.3, 0.4]).unwrap();
let mut phi = Array2::<f64>::zeros((4, 4));
let mut jet = Array3::<f64>::zeros((4, 5, 1));
assert!(
eval.evaluate_into(&mut phi, &mut jet, coords.view())
.is_err()
);
}
}