use super::*;
use approx::assert_abs_diff_eq;
use ndarray::array;
pub(crate) fn assert_jacobian_matches_central_difference<E: SaeBasisEvaluator>(
evaluator: &E,
coords: Array2<f64>,
tolerance: f64,
) {
let epsilon = 1.0e-6;
let (phi, jet) = evaluator
.evaluate(coords.view())
.expect("the caller's coords lie in the evaluator's chart domain");
let (n_rows, n_basis) = phi.dim();
let latent_dim = coords.ncols();
assert_eq!(jet.dim(), (n_rows, n_basis, latent_dim));
for row in 0..n_rows {
for axis in 0..latent_dim {
let mut plus = coords.clone();
let mut minus = coords.clone();
plus[[row, axis]] += epsilon;
minus[[row, axis]] -= epsilon;
let (phi_plus, plus_jet) = evaluator
.evaluate(plus.view())
.expect("the +epsilon probe stays in the chart domain");
let (phi_minus, minus_jet) = evaluator
.evaluate(minus.view())
.expect("the -epsilon probe stays in the chart domain");
assert_eq!(plus_jet.dim(), jet.dim());
assert_eq!(minus_jet.dim(), jet.dim());
for basis in 0..n_basis {
let finite_difference =
(phi_plus[[row, basis]] - phi_minus[[row, basis]]) / (2.0 * epsilon);
let analytic = jet[[row, basis, axis]];
let error = (analytic - finite_difference).abs();
assert!(
error <= tolerance,
"row={row} basis={basis} axis={axis}: analytic={analytic:.12e}, \
finite_difference={finite_difference:.12e}, error={error:.12e}, \
tolerance={tolerance:.12e}"
);
}
}
}
}
#[test]
pub(crate) fn sae_basis_evaluator_jacobians_match_central_differences() {
assert_jacobian_matches_central_difference(
&PeriodicHarmonicEvaluator::new(7).unwrap(),
array![[-0.37], [0.0], [0.125], [0.41]],
1.0e-6,
);
assert_jacobian_matches_central_difference(
&RawPeriodicCircleEvaluator::new(3).unwrap(),
array![[-1.2, 0.3, 2.0], [0.0, -0.4, 0.8], [2.4, 1.1, -0.7]],
1.0e-6,
);
let sphere_coords = array![
[0.0, 0.0, 1.0],
[0.6, -0.8, 0.0],
[0.36, 0.48, 0.8],
[-0.48, 0.6, -0.64]
];
assert_jacobian_matches_central_difference(
&AmbientSphereHarmonicEvaluator::new(2).unwrap(),
sphere_coords.clone(),
1.0e-6,
);
let (sphere_phi, sphere_jet) = AmbientSphereHarmonicEvaluator::new(2)
.unwrap()
.evaluate(sphere_coords.view())
.unwrap();
assert_eq!(sphere_phi.dim(), (sphere_coords.nrows(), 9));
assert_eq!(sphere_jet.dim(), (sphere_coords.nrows(), 9, 3));
for row in 0..sphere_coords.nrows() {
let lat = sphere_coords[[row, 0]];
let lon = sphere_coords[[row, 1]];
let clat = lat.cos();
let slat = lat.sin();
let clon = lon.cos();
let slon = lon.sin();
let z = slat;
let dx_dlon = -clat * slon;
let dy_dlon = clat * clon;
assert_eq!(sphere_jet[[row, 3, 1]], 0.0);
assert!((sphere_jet[[row, 5, 1]] - dy_dlon * z).abs() <= 1.0e-12);
assert!((sphere_jet[[row, 6, 1]] - dx_dlon * z).abs() <= 1.0e-12);
}
assert_jacobian_matches_central_difference(
&AffineCoordinateEvaluator::new(3),
array![[0.0, -1.0, 2.0], [3.5, 0.25, -0.75]],
1.0e-6,
);
let torus_coords = array![[0.1, 0.7], [0.42, 0.0], [0.95, 0.33], [0.5, 0.5]];
assert_jacobian_matches_central_difference(
&TorusHarmonicEvaluator::new(2, 3).unwrap(),
torus_coords.clone(),
1.0e-6,
);
let (torus_phi, torus_jet) = TorusHarmonicEvaluator::new(2, 3)
.unwrap()
.evaluate(torus_coords.view())
.unwrap();
assert_eq!(torus_phi.dim(), (torus_coords.nrows(), 49));
assert_eq!(torus_jet.dim(), (torus_coords.nrows(), 49, 2));
for row in 0..torus_coords.nrows() {
assert!((torus_phi[[row, 0]] - 1.0).abs() <= 1.0e-12);
assert!(torus_jet[[row, 0, 0]].abs() <= 1.0e-12);
assert!(torus_jet[[row, 0, 1]].abs() <= 1.0e-12);
}
}
#[test]
pub(crate) fn seed_coords_by_decoder_projection_recovers_continuous_minimiser() {
use std::f64::consts::PI;
let init_coords = array![[0.05], [0.05]];
let evaluator = Arc::new(PeriodicHarmonicEvaluator::new(3).unwrap());
let (phi0, jet0) = evaluator.evaluate(init_coords.view()).unwrap();
let decoder = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
let atom = SaeManifoldAtom::new_with_provided_function_gram(
"periodic",
SaeAtomBasisKind::Periodic,
1,
phi0,
jet0,
decoder,
Array2::<f64>::eye(3),
)
.unwrap()
.with_basis_evaluator(evaluator.clone());
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
Array2::<f64>::zeros((2, 1)),
vec![init_coords],
vec![LatentManifold::Circle { period: 1.0 }],
AssignmentMode::softmax(1.0),
)
.unwrap();
let mut term = SaeManifoldTerm::new(vec![atom], assignment).unwrap();
let phases = [0.173_205_080_756_887_73, 0.731_058_578_630_004_9];
let mut target = Array2::<f64>::zeros((2, 2));
for (row, &t) in phases.iter().enumerate() {
target[[row, 0]] = (2.0 * PI * t).sin();
target[[row, 1]] = (2.0 * PI * t).cos();
}
term.seed_coords_by_decoder_projection(target.view())
.unwrap();
let seeded = term.assignment.coords[0].as_matrix();
let mut expected_coords = Array2::<f64>::zeros((2, 1));
for (row, &expected) in phases.iter().enumerate() {
assert_abs_diff_eq!(seeded[[row, 0]], expected, epsilon = 1e-10);
expected_coords[[row, 0]] = expected;
}
let (phi_expected, _) = evaluator.evaluate(expected_coords.view()).unwrap();
assert_abs_diff_eq!(
(&term.atoms[0].basis_values - &phi_expected)
.mapv(f64::abs)
.sum(),
0.0,
epsilon = 1e-12
);
}
#[test]
pub(crate) fn seed_coords_by_decoder_projection_rejects_shape_mismatch() {
let init_coords = array![[0.05], [0.05]];
let evaluator = Arc::new(PeriodicHarmonicEvaluator::new(3).unwrap());
let (phi0, jet0) = evaluator.evaluate(init_coords.view()).unwrap();
let decoder = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
let atom = SaeManifoldAtom::new_with_provided_function_gram(
"periodic",
SaeAtomBasisKind::Periodic,
1,
phi0,
jet0,
decoder,
Array2::<f64>::eye(3),
)
.unwrap()
.with_basis_evaluator(evaluator);
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
Array2::<f64>::zeros((2, 1)),
vec![init_coords],
vec![LatentManifold::Circle { period: 1.0 }],
AssignmentMode::softmax(1.0),
)
.unwrap();
let mut term = SaeManifoldTerm::new(vec![atom], assignment).unwrap();
let bad_target = Array2::<f64>::zeros((2, 3));
let err = term
.seed_coords_by_decoder_projection(bad_target.view())
.unwrap_err();
assert!(
err.contains("target shape"),
"expected a target-shape error, got: {err}"
);
}
pub(crate) fn assert_second_jet_matches_central_difference<E: SaeBasisSecondJet>(
evaluator: &E,
coords: Array2<f64>,
abs_tol: f64,
rel_tol: f64,
) -> Result<(), String> {
let epsilon = 1.0e-4;
let second = evaluator.second_jet(coords.view())?;
let (_phi, jet) = evaluator.evaluate(coords.view())?;
let (n_rows, n_basis, latent_dim, latent_dim_b) = second.dim();
assert_eq!(latent_dim, latent_dim_b);
assert_eq!((n_rows, n_basis, latent_dim), jet.dim());
for row in 0..n_rows {
for axis_c in 0..latent_dim {
let mut plus = coords.clone();
let mut minus = coords.clone();
plus[[row, axis_c]] += epsilon;
minus[[row, axis_c]] -= epsilon;
let (_, jet_plus) = evaluator
.evaluate(plus.view())
.expect("the +epsilon probe stays in the chart domain");
let (_, jet_minus) = evaluator
.evaluate(minus.view())
.expect("the -epsilon probe stays in the chart domain");
for basis in 0..n_basis {
for axis_a in 0..latent_dim {
let fd = (jet_plus[[row, basis, axis_a]] - jet_minus[[row, basis, axis_a]])
/ (2.0 * epsilon);
let analytic = second[[row, basis, axis_a, axis_c]];
let error = (analytic - fd).abs();
let threshold = abs_tol + rel_tol * analytic.abs().max(fd.abs());
assert!(
error <= threshold,
"row={row} basis={basis} axis_a={axis_a} axis_c={axis_c}: \
analytic={analytic:.12e}, fd={fd:.12e}, error={error:.12e}, \
threshold={threshold:.12e}"
);
}
}
}
}
for row in 0..n_rows {
for basis in 0..n_basis {
for axis_a in 0..latent_dim {
for axis_b in 0..latent_dim {
let h_ab = second[[row, basis, axis_a, axis_b]];
let h_ba = second[[row, basis, axis_b, axis_a]];
assert!(
(h_ab - h_ba).abs() <= 1.0e-12,
"second_jet not symmetric: row={row} basis={basis} \
({axis_a},{axis_b})={h_ab:.6e} vs ({axis_b},{axis_a})={h_ba:.6e}"
);
}
}
}
}
Ok(())
}
pub(crate) fn assert_third_jet_matches_central_difference<E: SaeBasisThirdJet>(
evaluator: &E,
coords: Array2<f64>,
abs_tol: f64,
rel_tol: f64,
) -> Result<(), String> {
let epsilon = 1.0e-4;
let third = evaluator.third_jet(coords.view())?;
let second = evaluator.second_jet(coords.view())?;
let (n_rows, n_basis, latent_dim, ld_b, ld_c) = third.dim();
assert_eq!(latent_dim, ld_b);
assert_eq!(latent_dim, ld_c);
assert_eq!((n_rows, n_basis, latent_dim, latent_dim), second.dim());
for row in 0..n_rows {
for axis_e in 0..latent_dim {
let mut plus2 = coords.clone();
let mut plus = coords.clone();
let mut minus = coords.clone();
let mut minus2 = coords.clone();
plus2[[row, axis_e]] += 2.0 * epsilon;
plus[[row, axis_e]] += epsilon;
minus[[row, axis_e]] -= epsilon;
minus2[[row, axis_e]] -= 2.0 * epsilon;
let second_plus2 = evaluator.second_jet(plus2.view())?;
let second_plus = evaluator.second_jet(plus.view())?;
let second_minus = evaluator.second_jet(minus.view())?;
let second_minus2 = evaluator.second_jet(minus2.view())?;
for basis in 0..n_basis {
for axis_a in 0..latent_dim {
for axis_c in 0..latent_dim {
let fd = (-second_plus2[[row, basis, axis_a, axis_c]]
+ 8.0 * second_plus[[row, basis, axis_a, axis_c]]
- 8.0 * second_minus[[row, basis, axis_a, axis_c]]
+ second_minus2[[row, basis, axis_a, axis_c]])
/ (12.0 * epsilon);
let analytic = third[[row, basis, axis_a, axis_c, axis_e]];
let error = (analytic - fd).abs();
let threshold = abs_tol + rel_tol * analytic.abs().max(fd.abs());
assert!(
error <= threshold,
"row={row} basis={basis} a={axis_a} c={axis_c} e={axis_e}: \
analytic={analytic:.12e}, fd={fd:.12e}, error={error:.6e}, \
threshold={threshold:.6e}"
);
}
}
}
}
}
for row in 0..n_rows {
for basis in 0..n_basis {
for a in 0..latent_dim {
for b in 0..latent_dim {
for c in 0..latent_dim {
let reference = third[[row, basis, a, b, c]];
for perm in [[a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]] {
let permuted = third[[row, basis, perm[0], perm[1], perm[2]]];
assert!(
(reference - permuted).abs() <= 1.0e-10,
"third_jet not symmetric: row={row} basis={basis} \
({a},{b},{c})={reference:.6e} vs ({},{},{})={permuted:.6e}",
perm[0],
perm[1],
perm[2]
);
}
}
}
}
}
}
Ok(())
}
#[test]
pub(crate) fn isometry_periodic_second_jet_matches_fd() -> Result<(), String> {
assert_second_jet_matches_central_difference(
&PeriodicHarmonicEvaluator::new(7).unwrap(),
array![[-0.37], [0.0], [0.125], [0.41]],
1.0e-6,
1.0e-5,
)?;
Ok(())
}
#[test]
pub(crate) fn isometry_sphere_second_jet_matches_fd() -> Result<(), String> {
let sphere_coords = array![
[0.0, 0.0, 1.0],
[0.6, -0.8, 0.0],
[0.36, 0.48, 0.8],
[-0.48, 0.6, -0.64]
];
assert_second_jet_matches_central_difference(
&AmbientSphereHarmonicEvaluator::new(2).unwrap(),
sphere_coords,
1.0e-6,
1.0e-5,
)?;
Ok(())
}
#[test]
pub(crate) fn isometry_torus_second_jet_matches_fd() -> Result<(), String> {
let torus_coords = array![[0.1, 0.7], [0.42, 0.0], [0.95, 0.33], [0.5, 0.5]];
let evaluator = TorusHarmonicEvaluator::new(2, 3).unwrap();
assert!(evaluator.basis_size() > 0);
assert_second_jet_matches_central_difference(&evaluator, torus_coords, 1.0e-6, 1.0e-5)?;
Ok(())
}
#[test]
pub(crate) fn isometry_periodic_third_jet_matches_fd() -> Result<(), String> {
assert_third_jet_matches_central_difference(
&PeriodicHarmonicEvaluator::new(7).unwrap(),
array![[-0.37], [0.0], [0.125], [0.41]],
1.0e-6,
1.0e-5,
)?;
Ok(())
}
#[test]
pub(crate) fn isometry_sphere_third_jet_matches_fd() -> Result<(), String> {
let sphere_coords = array![
[0.0, 0.0, 1.0],
[0.6, -0.8, 0.0],
[0.36, 0.48, 0.8],
[-0.48, 0.6, -0.64]
];
assert_third_jet_matches_central_difference(
&AmbientSphereHarmonicEvaluator::new(2).unwrap(),
sphere_coords,
1.0e-6,
1.0e-5,
)?;
Ok(())
}
#[test]
pub(crate) fn isometry_torus_third_jet_matches_fd() -> Result<(), String> {
let torus_coords = array![[0.1, 0.7], [0.42, 0.0], [0.95, 0.33], [0.5, 0.5]];
let evaluator = TorusHarmonicEvaluator::new(2, 3).unwrap();
assert!(evaluator.basis_size() > 0);
assert_third_jet_matches_central_difference(&evaluator, torus_coords, 1.0e-6, 1.0e-5)?;
Ok(())
}
#[test]
pub(crate) fn isometry_affine_third_jet_is_trivial_zero() -> Result<(), String> {
let evaluator = AffineCoordinateEvaluator { latent_dim: 3 };
let coords = array![[0.2, -0.3, 0.7], [1.1, 0.0, -0.4]];
let third = evaluator.third_jet(coords.view())?;
assert_eq!(third.dim(), (coords.nrows(), 4, 3, 3, 3));
assert!(
third.iter().all(|x| *x == 0.0),
"affine third jet must vanish identically, got {third:?}"
);
Ok(())
}
#[test]
pub(crate) fn isometry_euclidean_patch_third_jet_matches_fd() -> Result<(), String> {
let evaluator = EuclideanPatchEvaluator::new(2, 4)?;
let coords = array![[0.2, -0.3], [0.7, 0.4], [-0.5, 0.9]];
assert_third_jet_matches_central_difference(&evaluator, coords, 1.0e-6, 1.0e-5)?;
Ok(())
}
fn cylinder_test_coords() -> Array2<f64> {
array![
[0.0_f64, -1.3],
[0.125, 0.0],
[0.4, 0.7],
[0.91, 2.2],
[0.6, -0.45]
]
}
#[test]
pub(crate) fn cylinder_phi_is_circle_tensor_line_product() -> Result<(), String> {
let h = 2usize;
let degree = 2usize;
let evaluator = CylinderHarmonicEvaluator::new(h, degree)?;
let mc = 2 * h + 1;
let ml = degree + 1;
assert_eq!(evaluator.circle_basis_size(), mc);
assert_eq!(evaluator.line_basis_size(), ml);
assert_eq!(evaluator.basis_size(), mc * ml);
let coords = cylinder_test_coords();
let (phi, jet) = evaluator.evaluate(coords.view())?;
assert_eq!(phi.dim(), (coords.nrows(), mc * ml));
assert_eq!(jet.dim(), (coords.nrows(), mc * ml, 2));
let two_pi = std::f64::consts::TAU;
for row in 0..coords.nrows() {
let t0 = coords[[row, 0]];
let t1 = coords[[row, 1]];
let mut circ = vec![0.0_f64; mc];
circ[0] = 1.0;
for k in 1..=h {
circ[2 * k - 1] = (two_pi * k as f64 * t0).sin();
circ[2 * k] = (two_pi * k as f64 * t0).cos();
}
let line: Vec<f64> = (0..ml).map(|j| t1.powi(j as i32)).collect();
for c in 0..mc {
for l in 0..ml {
let col = c * ml + l;
let expect = circ[c] * line[l];
assert_abs_diff_eq!(phi[[row, col]], expect, epsilon = 1e-12);
}
}
assert_abs_diff_eq!(phi[[row, 0]], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(jet[[row, 0, 0]], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(jet[[row, 0, 1]], 0.0, epsilon = 1e-12);
}
Ok(())
}
#[test]
pub(crate) fn cylinder_jacobian_matches_central_difference() {
assert_jacobian_matches_central_difference(
&CylinderHarmonicEvaluator::new(3, 3).unwrap(),
cylinder_test_coords(),
1.0e-6,
);
}
#[test]
pub(crate) fn cylinder_second_jet_matches_fd() -> Result<(), String> {
let evaluator = CylinderHarmonicEvaluator::new(3, 3)?;
assert_second_jet_matches_central_difference(
&evaluator,
cylinder_test_coords(),
1.0e-6,
1.0e-5,
)?;
Ok(())
}
#[test]
pub(crate) fn cylinder_third_jet_matches_fd() -> Result<(), String> {
let evaluator = CylinderHarmonicEvaluator::new(3, 3)?;
assert_third_jet_matches_central_difference(
&evaluator,
cylinder_test_coords(),
1.0e-6,
1.0e-5,
)?;
Ok(())
}
#[test]
pub(crate) fn cylinder_roughness_gram_is_psd_with_constant_nullspace() {
let h = 2usize;
let degree = 2usize;
let evaluator = CylinderHarmonicEvaluator::new(h, degree).unwrap();
let mc = 2 * h + 1;
let ml = degree + 1;
let m = mc * ml;
let s = evaluator.roughness_gram();
assert_eq!(s.dim(), (m, m));
for i in 0..m {
for j in 0..m {
assert_abs_diff_eq!(s[[i, j]], s[[j, i]], epsilon = 1e-12);
}
}
for j in 0..m {
assert_abs_diff_eq!(s[[0, j]], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(s[[j, 0]], 0.0, epsilon = 1e-12);
}
let two_pi = std::f64::consts::TAU;
for k in 1..=h {
let omega4 = (two_pi * k as f64).powi(4);
let s_idx = 2 * k - 1;
let c_idx = 2 * k;
assert_abs_diff_eq!(s[[s_idx * ml, s_idx * ml]], omega4 * 0.5, epsilon = 1e-6);
assert_abs_diff_eq!(s[[c_idx * ml, c_idx * ml]], omega4 * 0.5, epsilon = 1e-6);
}
if degree >= 2 {
let col = 2; assert_abs_diff_eq!(s[[col, col]], 4.0, epsilon = 1e-12);
}
let (evals, _) = s.eigh(Side::Lower).unwrap();
for &lam in evals.iter() {
assert!(
lam >= -1.0e-9,
"cylinder roughness Gram must be PSD; got eigenvalue {lam:.3e}"
);
}
}
#[test]
pub(crate) fn cylinder_rejects_zero_harmonics() {
assert!(CylinderHarmonicEvaluator::new(0, 2).is_err());
assert!(CylinderHarmonicEvaluator::new(1, 0).is_ok());
}
#[test]
pub(crate) fn cylinder_latent_manifold_is_circle_times_line() {
let manifold = SaeAtomBasisKind::Cylinder.latent_manifold(2);
match manifold {
LatentManifold::Product(parts) => {
assert_eq!(parts.len(), 2);
assert!(matches!(parts[0], LatentManifold::Circle { period } if period == 1.0));
assert!(matches!(parts[1], LatentManifold::Euclidean));
}
other => panic!("expected Product[Circle, Euclidean], got {other:?}"),
}
}
#[test]
pub(crate) fn duchon_coordinate_evaluator_phi_and_jet_share_column_count() {
for (d, centers) in [
(1usize, array![[-1.0], [-0.4], [0.1], [0.6], [1.2], [1.9]]),
(
2usize,
array![
[-1.0, -0.8],
[-0.3, 0.4],
[0.2, -0.5],
[0.7, 0.9],
[1.1, -0.2],
[1.6, 0.6],
],
),
] {
let evaluator = DuchonCoordinateEvaluator::new(centers, 2).unwrap();
let coords = match d {
1 => array![[-0.5], [0.0], [0.3], [0.8]],
_ => array![[-0.5, 0.2], [0.0, -0.3], [0.3, 0.7], [0.8, -0.1]],
};
let (phi, jet) = evaluator.evaluate(coords.view()).unwrap();
assert_eq!(
phi.ncols(),
jet.shape()[1],
"Duchon d={d}: Phi has {} columns but jet has {}",
phi.ncols(),
jet.shape()[1]
);
assert_eq!(jet.shape()[0], coords.nrows());
assert_eq!(jet.shape()[2], d);
}
}
#[test]
pub(crate) fn duchon_coordinate_evaluator_jacobian_matches_fd() {
let centers = array![
[-1.0, -0.8],
[-0.3, 0.4],
[0.2, -0.5],
[0.7, 0.9],
[1.1, -0.2],
[1.6, 0.6],
];
let evaluator = DuchonCoordinateEvaluator::new(centers, 2).unwrap();
let coords = array![[-0.5, 0.2], [0.05, -0.35], [0.45, 0.75], [1.3, 0.1]];
assert_jacobian_matches_central_difference(&evaluator, coords, 1.0e-4);
}
#[test]
pub(crate) fn duchon_coordinate_evaluator_second_jet_matches_fd() -> Result<(), String> {
let centers = array![
[-1.0, -0.8],
[-0.3, 0.4],
[0.2, -0.5],
[0.7, 0.9],
[1.1, -0.2],
[1.6, 0.6],
];
let evaluator = DuchonCoordinateEvaluator::new(centers, 2).unwrap();
let coords = array![[-0.5, 0.2], [0.05, -0.35], [0.45, 0.75], [1.3, 0.1]];
assert_second_jet_matches_central_difference(&evaluator, coords, 1.0e-4, 1.0e-4)?;
Ok(())
}
#[test]
pub(crate) fn duchon_coordinate_evaluator_third_jet_matches_fd() -> Result<(), String> {
let centers = array![
[-1.0, -0.8],
[-0.3, 0.4],
[0.2, -0.5],
[0.7, 0.9],
[1.1, -0.2],
[1.6, 0.6],
];
let evaluator = DuchonCoordinateEvaluator::new(centers, 2).unwrap();
let coords = array![[-0.5, 0.2], [0.05, -0.35], [0.45, 0.75], [1.3, 0.1]];
assert_third_jet_matches_central_difference(&evaluator, coords, 1.0e-4, 1.0e-4)?;
Ok(())
}
#[test]
pub(crate) fn euclidean_patch_evaluator_jets_match_fd() -> Result<(), String> {
let evaluator = EuclideanPatchEvaluator::new(2, 2).unwrap();
let coords = array![[0.0, -1.0], [3.5, 0.25], [-0.75, 1.2], [0.4, 0.9]];
assert_jacobian_matches_central_difference(&evaluator, coords.clone(), 1.0e-6);
assert_second_jet_matches_central_difference(&evaluator, coords, 1.0e-5, 1.0e-5)?;
let (phi, _jet) = evaluator.evaluate(array![[0.0, 0.0]].view())?;
assert_eq!(phi.ncols(), 6);
Ok(())
}
#[test]
pub(crate) fn euclidean_affine_gauge_canonicalization_preserves_reconstruction()
-> Result<(), String> {
let evaluator = Arc::new(EuclideanPatchEvaluator::new(1, 2)?);
let canonical = array![[-1.0_f64], [-0.35], [0.1], [0.65], [1.2]];
let mut coords = canonical.clone();
for row in 0..coords.nrows() {
coords[[row, 0]] = 2.75 + 4.0 * canonical[[row, 0]];
}
let (phi, jet) = evaluator.evaluate(coords.view())?;
let decoder = array![[0.25, -0.4], [1.2, 0.3], [-0.15, 0.5]];
let atom = SaeManifoldAtom::new_with_provided_function_gram(
"euclidean_patch",
SaeAtomBasisKind::EuclideanPatch,
1,
phi,
jet,
decoder,
Array2::<f64>::eye(evaluator.basis_size()),
)?
.with_basis_evaluator(evaluator);
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
Array2::<f64>::zeros((coords.nrows(), 1)),
vec![coords],
vec![LatentManifold::Euclidean],
AssignmentMode::softmax(1.0),
)?;
let mut term = SaeManifoldTerm::new(vec![atom], assignment)?;
let before = term.fitted();
term.canonicalize_affine_gauge_after_accept(None)?;
let after = term.fitted();
let max_abs = before
.iter()
.zip(after.iter())
.fold(0.0_f64, |acc, (&a, &b)| acc.max((a - b).abs()));
assert!(
max_abs <= 1.0e-10,
"canonicalization changed reconstruction by {max_abs:.3e}"
);
let live = term.assignment.coords[0].as_matrix();
let mean = live.column(0).sum() / live.nrows() as f64;
let rms = (live.column(0).iter().map(|v| v * v).sum::<f64>() / live.nrows() as f64).sqrt();
assert_abs_diff_eq!(mean, 0.0, epsilon = 1.0e-12);
assert_abs_diff_eq!(rms, 1.0, epsilon = 1.0e-12);
Ok(())
}
#[test]
pub(crate) fn quotient_step_norm_removes_pure_euclidean_affine_gauge() -> Result<(), String> {
let evaluator = Arc::new(EuclideanPatchEvaluator::new(1, 2)?);
let coords = array![[-1.0_f64], [-0.4], [0.2], [0.8], [1.3]];
let (phi, jet) = evaluator.evaluate(coords.view())?;
let decoder = array![[0.1, -0.2], [1.0, 0.4], [0.25, -0.3]];
let atom = SaeManifoldAtom::new_with_provided_function_gram(
"euclidean_patch",
SaeAtomBasisKind::EuclideanPatch,
1,
phi,
jet,
decoder,
Array2::<f64>::eye(evaluator.basis_size()),
)?
.with_basis_evaluator(evaluator);
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
Array2::<f64>::zeros((coords.nrows(), 1)),
vec![coords],
vec![LatentManifold::Euclidean],
AssignmentMode::softmax(1.0),
)?;
let term = SaeManifoldTerm::new(vec![atom], assignment)?;
let gauges = term.dense_step_gauge_vectors()?;
assert!(
gauges.len() >= 2,
"expected translation and scale gauge generators"
);
let n_coord = term.n_obs() * term.assignment.row_block_dim();
let gauge = &gauges[1];
let delta_t = gauge.slice(s![..n_coord]);
let delta_beta = gauge.slice(s![n_coord..]);
let raw = gauge.iter().map(|v| v * v).sum::<f64>();
let quotient =
term.quotient_newton_step_norm_sq(delta_t, delta_beta, raw, &vec![0.0; term.k_atoms()])?;
assert!(
quotient <= raw.max(1.0) * 1.0e-20,
"pure affine gauge step left quotient norm squared {quotient:.3e} from raw {raw:.3e}"
);
Ok(())
}