use crate::basis::{
CenterStrategy, ConstantCurvatureBasisSpec, ConstantCurvatureIdentifiability,
constant_curvature_center_chart_radius2, constant_curvature_data_chart_radius2,
constant_curvature_kernel_matrix,
};
use crate::smooth::{
CONSTANT_CURVATURE_KAPPA_CHART_FRACTION, CONSTANT_CURVATURE_MIN_CHART_RADIUS2, ShapeConstraint,
SmoothBasisSpec, SmoothTermSpec, TermCollectionSpec, constant_curvature_kappa_bounds,
};
use gam_geometry::manifolds::constant_curvature::ConstantCurvature;
use ndarray::{Array2, array};
fn spec_with(strategy: CenterStrategy, dim: usize) -> TermCollectionSpec {
TermCollectionSpec {
linear_terms: vec![],
random_effect_terms: vec![],
smooth_terms: vec![SmoothTermSpec {
frozen_parametric_residualization: None,
name: "curv".to_string(),
basis: SmoothBasisSpec::ConstantCurvature {
feature_cols: (0..dim).collect(),
spec: ConstantCurvatureBasisSpec {
center_strategy: strategy,
kappa: 0.0,
kappa_fixed: false,
length_scale: 0.0,
length_scale_fixed: false,
double_penalty: false,
identifiability: ConstantCurvatureIdentifiability::CenterSumToZero,
},
},
shape: ShapeConstraint::None,
joint_null_rotation: None,
}],
}
}
fn ring(n: usize, r: f64, dim: usize) -> Array2<f64> {
let mut data = Array2::<f64>::zeros((n, dim));
for i in 0..n {
let theta = std::f64::consts::TAU * (i as f64) / (n as f64);
data[(i, 0)] = r * theta.cos();
data[(i, 1)] = r * theta.sin();
}
data
}
#[test]
fn data_driven_center_strategies_keep_the_pre_2716_box_bit_for_bit() {
let data = ring(64, 0.6, 2);
let max_r2 = data
.outer_iter()
.map(|row| row.dot(&row))
.fold(CONSTANT_CURVATURE_MIN_CHART_RADIUS2, f64::max);
let expected = CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / max_r2;
for strategy in [
CenterStrategy::FarthestPoint { num_centers: 8 },
CenterStrategy::KMeans {
num_centers: 8,
max_iter: 10,
},
CenterStrategy::EqualMass { num_centers: 8 },
CenterStrategy::EqualMassCovarRepresentative { num_centers: 8 },
CenterStrategy::Auto(Box::new(CenterStrategy::FarthestPoint { num_centers: 8 })),
] {
let spec = spec_with(strategy.clone(), 2);
let (lo, hi) = constant_curvature_kappa_bounds(data.view(), &spec, 0);
assert_eq!(
(lo, hi),
(-expected, expected),
"{strategy:?}: the κ box must not move for a strategy whose centers \
are inside the data hull"
);
}
}
#[test]
fn user_provided_centers_beyond_twice_the_data_radius_no_longer_put_the_box_past_the_fold_2716() {
let data = ring(32, 0.3, 2);
let centers = array![[0.8, 0.0], [-0.8, 0.0], [0.0, 0.8]];
let spec = spec_with(CenterStrategy::UserProvided(centers.clone()), 2);
let (lo, hi) = constant_curvature_kappa_bounds(data.view(), &spec, 0);
let fold = 1.0 / (0.3 * 0.8);
let old_hi = CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / (0.3 * 0.3);
assert!(
old_hi > fold,
"the configuration this test is about: the pre-#2716 upper end {old_hi} \
must be past the fold {fold}"
);
assert!(
(hi - CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / 0.64).abs() <= 1e-12,
"the upper end must be F/max(R_x, R_c)²: got {hi}, expected {}",
CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / 0.64
);
assert!(
hi < fold,
"and it must be strictly inside the data × center fold {fold}"
);
assert_eq!(lo, -hi, "the window stays symmetric: one radius, two walls");
let x = array![0.3, 0.0];
let c = array![-0.8, 0.0];
let rho2 = x.dot(&x) * c.dot(&c);
let denom = |k: f64| 1.0 + 2.0 * k * x.dot(&c) + k * k * rho2;
let floor = (1.0_f64 - CONSTANT_CURVATURE_KAPPA_CHART_FRACTION).powi(2);
assert!(
denom(hi) >= floor - 1e-12,
"at the box's upper end {hi} the pair gauge D = {} must stay at or above \
(1−F)² = {floor}",
denom(hi)
);
let antipodal_fraction = |k: f64| {
ConstantCurvature::new(2, k)
.distance(x.view(), c.view())
.map(|d| d * k.sqrt() / std::f64::consts::PI)
};
let old_twin = 1.0 / (old_hi * rho2);
assert!(
old_twin > 0.0 && old_twin < old_hi,
"the twin {old_twin} of the pre-#2716 upper end must itself be inside the \
old box (0, {old_hi}] — that is what 'doubly covered' means"
);
let (f_end, f_twin) = (
antipodal_fraction(old_hi).expect("past the fold the chart still evaluates"),
antipodal_fraction(old_twin).expect("the twin is interior"),
);
assert!(
(f_end - f_twin).abs() <= 1e-12,
"the defect: the pre-#2716 box contained BOTH κ = {old_hi} and κ = {old_twin}, \
which give the extreme pair an identical scale-free geometry \
({f_end} vs {f_twin})"
);
let new_twin = 1.0 / (hi * rho2);
assert!(
new_twin > hi,
"the repaired box's upper end {hi} must sit BELOW its own involution fixed \
point, so the twin {new_twin} is outside the box"
);
let old_lo = -CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / (0.3 * 0.3);
assert!(
1.0 + lo * 0.64 > 0.0,
"at the box's lower end {lo} the farthest center (‖c‖² = 0.64) must stay \
inside the chart; gauge = {}",
1.0 + lo * 0.64
);
assert!(
1.0 + old_lo * 0.64 <= 0.0,
"the defect: at the pre-#2716 lower end {old_lo} the farthest center is \
OUTSIDE the chart; gauge = {}",
1.0 + old_lo * 0.64
);
assert!(
constant_curvature_kernel_matrix(data.view(), centers.view(), lo, 1.0).is_ok(),
"the kernel must build at the box's own lower end"
);
assert!(
constant_curvature_kernel_matrix(data.view(), centers.view(), old_lo, 1.0).is_err(),
"the defect: the kernel could not build at the pre-#2716 lower end"
);
let manifold_at_fold = ConstantCurvature::new(2, fold);
assert!(
manifold_at_fold.distance(x.view(), c.view()).is_err(),
"the fold must be where the shipped distance refuses"
);
assert!(
ConstantCurvature::new(2, hi)
.distance(x.view(), c.view())
.is_ok(),
"the box's upper end must be strictly inside it"
);
}
#[test]
fn uniform_grid_corner_centers_leave_the_hull_and_move_the_box_2716() {
let dim = 5usize;
let mut data = Array2::<f64>::zeros((2 * dim, dim));
for axis in 0..dim {
data[(2 * axis, axis)] = 1.0;
data[(2 * axis + 1, axis)] = -1.0;
}
let spec = spec_with(CenterStrategy::UniformGrid { points_per_dim: 3 }, dim);
let (lo, hi) = constant_curvature_kappa_bounds(data.view(), &spec, 0);
let corner_r2 = dim as f64;
let data_center_fold = 1.0 / corner_r2.sqrt();
let center_center_fold = 1.0 / corner_r2;
let old_hi = CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / 1.0;
assert!(
old_hi > data_center_fold && old_hi > center_center_fold,
"d = {dim}: the pre-#2716 upper end {old_hi} must be past BOTH corner \
folds ({data_center_fold}, {center_center_fold})"
);
let expected = CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / corner_r2;
assert!(
(hi - expected).abs() <= 1e-12 && (lo + expected).abs() <= 1e-12,
"the box must be denominated in the CORNER radius, the largest evaluated \
point: got [{lo}, {hi}], expected ±{expected}"
);
assert!(
hi < center_center_fold,
"and it must be strictly inside the binding fold {center_center_fold}"
);
}
#[test]
fn degenerate_radii_still_yield_a_finite_bracket() {
let data = Array2::<f64>::zeros((8, 2));
let floor_bound =
CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / CONSTANT_CURVATURE_MIN_CHART_RADIUS2;
for strategy in [
CenterStrategy::FarthestPoint { num_centers: 4 },
CenterStrategy::UserProvided(Array2::<f64>::zeros((3, 2))),
CenterStrategy::UniformGrid { points_per_dim: 2 },
] {
let spec = spec_with(strategy.clone(), 2);
let (lo, hi) = constant_curvature_kappa_bounds(data.view(), &spec, 0);
assert!(
lo.is_finite() && hi.is_finite() && hi > lo,
"{strategy:?}: degenerate geometry must still bracket, got [{lo}, {hi}]"
);
assert!(
(hi - floor_bound).abs() <= 1e-9 && (lo + floor_bound).abs() <= 1e-9,
"{strategy:?}: the degenerate bracket is the radius floor's, got [{lo}, {hi}]"
);
}
}
#[test]
fn centers_inside_the_data_hull_never_widen_the_box() {
let data = ring(16, 2.0, 2);
let centers = array![[0.5, 0.0], [-0.5, 0.0], [0.0, 0.0]];
let spec = spec_with(CenterStrategy::UserProvided(centers), 2);
let (lo, hi) = constant_curvature_kappa_bounds(data.view(), &spec, 0);
let data_only = CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / 4.0;
assert!(
(hi - data_only).abs() <= 1e-12 && (lo + data_only).abs() <= 1e-12,
"centers inside the hull must leave the box at the data radius, got [{lo}, {hi}]"
);
assert!(
hi < CONSTANT_CURVATURE_KAPPA_CHART_FRACTION / (2.0 * 0.5),
"and it must stay strictly inside the wider data × center fold, which a \
pair-wise bound would have handed the optimizer"
);
}
#[test]
fn each_wall_retreats_by_f_from_its_own_branchs_gauge_never_the_others_2687() {
let data = ring(48, 0.7, 2);
let f = CONSTANT_CURVATURE_KAPPA_CHART_FRACTION;
for strategy in [
CenterStrategy::FarthestPoint { num_centers: 6 },
CenterStrategy::KMeans {
num_centers: 6,
max_iter: 10,
},
CenterStrategy::EqualMass { num_centers: 6 },
CenterStrategy::UniformGrid { points_per_dim: 3 },
CenterStrategy::UserProvided(array![[1.4, 0.0], [-1.4, 0.0], [0.0, 0.2]]),
] {
let spec = spec_with(strategy.clone(), 2);
let (lo, hi) = constant_curvature_kappa_bounds(data.view(), &spec, 0);
let feature_cols = [0usize, 1usize];
let r2 = constant_curvature_data_chart_radius2(data.view(), &feature_cols)
.max(constant_curvature_center_chart_radius2(
data.view(),
&feature_cols,
&strategy,
))
.max(CONSTANT_CURVATURE_MIN_CHART_RADIUS2);
let lambda = 1.0 + lo * r2;
assert!(
(lambda - (1.0 - f)).abs() <= 1e-12,
"{strategy:?}: κ_min = {lo} must leave the per-point chart gauge at \
1 − F = {}, got λ = {lambda}",
1.0 - f
);
let r = r2.sqrt();
let x = array![r, 0.0];
let c = array![-r, 0.0];
let d_pair = 1.0 + 2.0 * hi * x.dot(&c) + hi * hi * x.dot(&x) * c.dot(&c);
assert!(
(d_pair.sqrt() - (1.0 - f)).abs() <= 1e-9,
"{strategy:?}: κ_max = {hi} must leave the per-pair Möbius gauge at \
1 − F = {}, got √D = {}",
1.0 - f,
d_pair.sqrt()
);
assert!(
ConstantCurvature::new(2, 1.0 / r2)
.distance(x.view(), c.view())
.is_err(),
"{strategy:?}: the κ>0 branch must have a wall of its own at 1/R² = {}",
1.0 / r2
);
assert!(
ConstantCurvature::new(2, hi)
.distance(x.view(), c.view())
.is_ok(),
"{strategy:?}: and the box's upper end {hi} must be strictly inside it"
);
for kappa in [lo, hi] {
assert!(
constant_curvature_kernel_matrix(data.view(), data.view(), kappa, 1.0).is_ok(),
"{strategy:?}: the per-point guard must accept at the box end {kappa}"
);
}
}
}
#[test]
fn freezing_a_data_driven_center_set_to_user_provided_does_not_move_the_box() {
let data = ring(40, 0.6, 2);
for realized in [
array![[0.6, 0.0], [-0.6, 0.0], [0.0, 0.0]],
array![[0.3, 0.1], [-0.2, 0.25], [0.0, 0.0]],
array![[0.05, 0.0], [-0.05, 0.0], [0.0, 0.0]],
] {
let before = constant_curvature_kappa_bounds(
data.view(),
&spec_with(CenterStrategy::FarthestPoint { num_centers: 3 }, 2),
0,
);
let after = constant_curvature_kappa_bounds(
data.view(),
&spec_with(CenterStrategy::UserProvided(realized.clone()), 2),
0,
);
assert_eq!(
before, after,
"freezing {realized:?} must not move the κ box: {before:?} -> {after:?}"
);
}
}