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))
}
}
pub const SPHERE_CHART_PENALTY_DIAGONAL: [f64; 7] = [1e-8, 1.0, 1.0, 1.0, 4.0, 4.0, 4.0];
pub fn sphere_chart_basis_jet(
coords: ArrayView2<'_, f64>,
) -> Result<(Array2<f64>, Array3<f64>), String> {
if coords.ncols() != 2 {
return Err(format!(
"sphere_chart_basis_jet expects latent_dim == 2, got {}",
coords.ncols()
));
}
let n = coords.nrows();
let mut phi = Array2::<f64>::zeros((n, 7));
let mut jet = Array3::<f64>::zeros((n, 7, 2));
for row in 0..n {
let lat = coords[[row, 0]];
let lon = coords[[row, 1]];
let clat = lat.cos();
let slat = lat.sin();
let clon = lon.cos();
let slon = lon.sin();
let x = clat * clon;
let y = clat * slon;
let z = slat;
phi[[row, 0]] = 1.0;
phi[[row, 1]] = x;
phi[[row, 2]] = y;
phi[[row, 3]] = z;
phi[[row, 4]] = x * y;
phi[[row, 5]] = y * z;
phi[[row, 6]] = x * z;
let dx_dlat = -slat * clon;
let dx_dlon = -clat * slon;
let dy_dlat = -slat * slon;
let dy_dlon = clat * clon;
let dz_dlat = clat;
jet[[row, 1, 0]] = dx_dlat;
jet[[row, 1, 1]] = dx_dlon;
jet[[row, 2, 0]] = dy_dlat;
jet[[row, 2, 1]] = dy_dlon;
jet[[row, 3, 0]] = dz_dlat;
jet[[row, 4, 0]] = dx_dlat * y + x * dy_dlat;
jet[[row, 4, 1]] = dx_dlon * y + x * dy_dlon;
jet[[row, 5, 0]] = dy_dlat * z + y * dz_dlat;
jet[[row, 5, 1]] = dy_dlon * z;
jet[[row, 6, 0]] = dx_dlat * z + x * dz_dlat;
jet[[row, 6, 1]] = dx_dlon * z;
}
Ok((phi, jet))
}
#[derive(Debug, Clone)]
pub struct SphereChartEvaluator;
impl SaeBasisEvaluator for SphereChartEvaluator {
fn phi_eta_split(&self, n_basis: usize) -> Result<PhiEtaSplit, String> {
if n_basis != 7 {
return Err(format!(
"SphereChartEvaluator::phi_eta_split: n_basis {n_basis} != 7"
));
}
let mut curved = vec![false; n_basis];
for col in 4..7 {
curved[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> {
sphere_chart_basis_jet(coords)
}
}
impl SaeBasisSecondJet for SphereChartEvaluator {
fn second_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array4<f64>, String> {
if coords.ncols() != 2 {
return Err(format!(
"SphereChartEvaluator::second_jet expects latent_dim == 2, got {}",
coords.ncols()
));
}
let n = coords.nrows();
let mut h = Array4::<f64>::zeros((n, 7, 2, 2));
for row in 0..n {
let lat = coords[[row, 0]];
let lon = coords[[row, 1]];
let clat = lat.cos();
let slat = lat.sin();
let clon = lon.cos();
let slon = lon.sin();
let x = clat * clon;
let y = clat * slon;
let z = slat;
let dx = [-slat * clon, -clat * slon];
let dy = [-slat * slon, clat * clon];
let dz = [clat, 0.0];
let hx = [[-x, slat * slon], [slat * slon, -x]];
let hy = [[-y, -slat * clon], [-slat * clon, -y]];
let hz = [[-z, 0.0], [0.0, 0.0]];
for axis_a in 0..2 {
for axis_b in 0..2 {
h[[row, 1, axis_a, axis_b]] = hx[axis_a][axis_b];
h[[row, 2, axis_a, axis_b]] = hy[axis_a][axis_b];
h[[row, 3, axis_a, axis_b]] = hz[axis_a][axis_b];
}
}
let pair = |hf: [[f64; 2]; 2],
df: [f64; 2],
f: f64,
hg: [[f64; 2]; 2],
dg: [f64; 2],
g: f64|
-> [[f64; 2]; 2] {
let mut out = [[0.0; 2]; 2];
for axis_a in 0..2 {
for axis_b in 0..2 {
out[axis_a][axis_b] = hf[axis_a][axis_b] * g
+ df[axis_a] * dg[axis_b]
+ df[axis_b] * dg[axis_a]
+ f * hg[axis_a][axis_b];
}
}
out
};
let hxy = pair(hx, dx, x, hy, dy, y);
let hyz = pair(hy, dy, y, hz, dz, z);
let hxz = pair(hx, dx, x, hz, dz, z);
for axis_a in 0..2 {
for axis_b in 0..2 {
h[[row, 4, axis_a, axis_b]] = hxy[axis_a][axis_b];
h[[row, 5, axis_a, axis_b]] = hyz[axis_a][axis_b];
h[[row, 6, axis_a, axis_b]] = hxz[axis_a][axis_b];
}
}
}
Ok(h)
}
}
impl SaeBasisThirdJet for SphereChartEvaluator {
fn third_jet(&self, coords: ArrayView2<'_, f64>) -> Result<Array5<f64>, String> {
if coords.ncols() != 2 {
return Err(format!(
"SphereChartEvaluator::third_jet expects latent_dim == 2, got {}",
coords.ncols()
));
}
let n = coords.nrows();
let mut t3 = Array5::<f64>::zeros((n, 7, 2, 2, 2));
let single = |lat: &[f64; 4], lon: &[f64; 4], ax: [usize; 3]| -> f64 {
let n_lat = ax.iter().filter(|&&q| q == 0).count();
lat[n_lat] * lon[3 - n_lat]
};
let product = |f_lat: &[f64; 4],
f_lon: &[f64; 4],
g_lat: &[f64; 4],
g_lon: &[f64; 4],
ax: [usize; 3]|
-> f64 {
let mut acc = 0.0;
for mask in 0u8..8 {
let (mut f_lat_n, mut f_lon_n, mut g_lat_n, mut g_lon_n) = (0, 0, 0, 0);
for (i, &axis) in ax.iter().enumerate() {
let to_f = (mask >> i) & 1 == 1;
match (to_f, axis == 0) {
(true, true) => f_lat_n += 1,
(true, false) => f_lon_n += 1,
(false, true) => g_lat_n += 1,
(false, false) => g_lon_n += 1,
}
}
acc += f_lat[f_lat_n] * f_lon[f_lon_n] * g_lat[g_lat_n] * g_lon[g_lon_n];
}
acc
};
for row in 0..n {
let lat = coords[[row, 0]];
let lon = coords[[row, 1]];
let clat = lat.cos();
let slat = lat.sin();
let clon = lon.cos();
let slon = lon.sin();
let cos_lat = [clat, -slat, -clat, slat];
let sin_lat = [slat, clat, -slat, -clat];
let cos_lon = [clon, -slon, -clon, slon];
let sin_lon = [slon, clon, -slon, -clon];
let const_lon = [1.0, 0.0, 0.0, 0.0];
let (x_lat, x_lon) = (&cos_lat, &cos_lon);
let (y_lat, y_lon) = (&cos_lat, &sin_lon);
let (z_lat, z_lon) = (&sin_lat, &const_lon);
for axis_a in 0..2 {
for axis_b in 0..2 {
for axis_c in 0..2 {
let ax = [axis_a, axis_b, axis_c];
t3[[row, 1, axis_a, axis_b, axis_c]] = single(x_lat, x_lon, ax);
t3[[row, 2, axis_a, axis_b, axis_c]] = single(y_lat, y_lon, ax);
t3[[row, 3, axis_a, axis_b, axis_c]] = single(z_lat, z_lon, ax);
t3[[row, 4, axis_a, axis_b, axis_c]] =
product(x_lat, x_lon, y_lat, y_lon, ax);
t3[[row, 5, axis_a, axis_b, axis_c]] =
product(y_lat, y_lon, z_lat, z_lon, ax);
t3[[row, 6, axis_a, axis_b, axis_c]] =
product(x_lat, x_lon, z_lat, z_lon, ax);
}
}
}
}
Ok(t3)
}
}
#[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)
}
}
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 characters = cover
.spectral_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(
"projective-plane",
Arc::new(cover),
cover_width,
characters,
)?;
if evaluator.basis_size() != expected_width {
return Err(format!(
"QuotientSpectralEvaluator::projective_plane: 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
}
#[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 (fixed_phi, _) = SphereChartEvaluator.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}, fixed-chart 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}, fixed-chart 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}, fixed-chart 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 = SphereChartEvaluator;
let coords_a =
Array2::from_shape_vec((3, 2), vec![0.2, 0.5, -0.4, 1.1, 0.9, -0.7]).unwrap();
let coords_b =
Array2::from_shape_vec((3, 2), vec![-0.1, 0.3, 0.6, -0.9, -0.5, 0.8]).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()
);
}
}