use super::*;
use ndarray::Array2;
fn circle_points(n: usize, r: f64) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((n, 2));
for i in 0..n {
let theta = std::f64::consts::TAU * (i as f64) / (n as f64);
pts[[i, 0]] = r * theta.cos();
pts[[i, 1]] = r * theta.sin();
}
pts
}
fn cluster_ring_points(clusters: usize, per: usize, r: f64, jitter: f64) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((clusters * per, 2));
let mut idx = 0;
for c in 0..clusters {
let theta = std::f64::consts::TAU * (c as f64) / (clusters as f64);
let cx = r * theta.cos();
let cy = r * theta.sin();
for j in 0..per {
let a = (j % 3) as f64 - 1.0;
let b = (j / 3) as f64 - 1.0;
pts[[idx, 0]] = cx + jitter * a;
pts[[idx, 1]] = cy + jitter * b;
idx += 1;
}
}
pts
}
fn line_points(n: usize, length: f64) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((n, 2));
for i in 0..n {
let t = length * (i as f64) / ((n - 1) as f64);
pts[[i, 0]] = t;
pts[[i, 1]] = 0.0;
}
pts
}
fn torus_points(nu: usize, nv: usize) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((nu * nv, 4));
let mut row = 0usize;
for i in 0..nu {
let u = std::f64::consts::TAU * (i as f64) / (nu as f64);
for j in 0..nv {
let v = std::f64::consts::TAU * (j as f64) / (nv as f64);
pts[[row, 0]] = u.cos();
pts[[row, 1]] = u.sin();
pts[[row, 2]] = v.cos();
pts[[row, 3]] = v.sin();
row += 1;
}
}
pts
}
fn embedded_torus_points(nu: usize, nv: usize, major: f64, minor: f64) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((nu * nv, 3));
let mut row = 0usize;
for i in 0..nu {
let u = std::f64::consts::TAU * (i as f64) / (nu as f64);
for j in 0..nv {
let v = std::f64::consts::TAU * (j as f64) / (nv as f64);
let tube = major + minor * v.cos();
pts[[row, 0]] = tube * u.cos();
pts[[row, 1]] = tube * u.sin();
pts[[row, 2]] = minor * v.sin();
row += 1;
}
}
pts
}
fn octahedron_sphere_points() -> Array2<f64> {
Array2::from_shape_vec(
(6, 3),
vec![
1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
-1.0,
],
)
.expect("6 octahedron vertices x 3 coordinates matches the (6, 3) shape")
}
#[test]
fn vietoris_rips_finds_the_circle_loop() {
let pts = circle_points(24, 1.0);
let diagram = vietoris_rips_persistence(pts.view());
let essential_h0 = diagram.h0.iter().filter(|b| b.is_essential()).count();
assert_eq!(essential_h0, 1, "a circle is one connected component");
let top_h1 = diagram
.h1
.iter()
.map(|b| b.persistence())
.fold(0.0_f64, f64::max);
assert!(
top_h1 > 1.0,
"the circle's loop must persist well past unit spacing; got {top_h1}"
);
}
#[test]
fn circle_cloud_agrees_with_a_raced_circle() {
let pts = circle_points(40, 2.0);
let verdict = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Periodic)
.expect("periodic atom has a topology prediction");
assert_eq!(verdict.measured_betti.b0, 1, "circle is connected");
assert_eq!(verdict.measured_betti.b1, 1, "circle must show one loop");
assert_eq!(
verdict.expected_betti.b1, 1,
"periodic type predicts one loop"
);
assert!(
!verdict.contested,
"a true circle raced as a circle is not contested: {}",
verdict.note
);
}
#[test]
fn seven_cluster_ring_forced_through_circle_is_contested() {
let pts = cluster_ring_points(7, 6, 3.0, 0.01);
let verdict = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Periodic)
.expect("periodic atom has a topology prediction");
assert_eq!(
verdict.measured_betti.b0, 7,
"the seven blobs must register as seven H₀ components: {}",
verdict.note
);
assert!(
verdict.contested,
"seven clusters disagree with a connected circle winner: {}",
verdict.note
);
}
#[test]
fn line_is_clean_against_a_line_and_contested_against_a_circle() {
let pts = line_points(40, 5.0);
let as_line = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Linear)
.expect("linear atom has a topology prediction");
assert_eq!(as_line.measured_betti.b0, 1, "a line is one component");
assert_eq!(as_line.measured_betti.b1, 0, "a line has no loop");
assert!(
!as_line.contested,
"a line raced as a line is clean: {}",
as_line.note
);
let as_circle = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Periodic)
.expect("periodic atom has a topology prediction");
assert_eq!(as_circle.expected_betti.b1, 1, "periodic predicts a loop");
assert_eq!(
as_circle.measured_betti.b1, 0,
"the line has no loop to find"
);
assert!(
as_circle.contested,
"a circle fit on a line is contested: {}",
as_circle.note
);
}
#[test]
fn torus_signature_requires_two_independent_loops() {
let pts = torus_points(12, 10);
let as_torus = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Torus)
.expect("torus atom has a topology prediction");
assert_eq!(
as_torus.resolution,
TopologyResolution::Resolved,
"a 12x10 Clifford cover resolves H1; note: {}",
as_torus.note
);
assert_eq!(as_torus.measured_betti.b0, 1, "torus is connected");
assert_eq!(
as_torus.measured_betti.b1, 2,
"torus must show two H1 loops"
);
assert_eq!(as_torus.expected_betti.b1, 2, "torus predicts two H1 loops");
let as_circle = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Periodic)
.expect("periodic atom has a topology prediction");
assert_eq!(
as_circle.measured_betti.b1, 2,
"same cloud still measures two loops"
);
assert_eq!(as_circle.expected_betti.b1, 1, "circle predicts one loop");
assert!(
as_circle.contested,
"a circle candidate on RESOLVED torus support must be contested: {}",
as_circle.note
);
}
#[test]
fn under_sampled_torus_refuses_instead_of_reporting_its_fallback_2552() {
let verdict = topology_persistence_verdict(
torus_points(4, 4).view(),
&SaeAtomBasisKind::Torus,
)
.expect("torus atom has a topology prediction");
assert!(
matches!(
verdict.resolution,
TopologyResolution::UnderSampled { finite_bars } if finite_bars > 0
),
"a 16-point 2-torus cover is under-sampled, not a measurement of b1={}; \
resolution={:?}",
verdict.measured_betti.b1,
verdict.resolution
);
assert!(
!verdict.contested,
"an unresolved cover is not evidence against the raced type: {}",
verdict.note
);
}
#[test]
fn embedded_torus_grid_does_not_resolve_h1_at_this_cover_2552() {
let pts = embedded_torus_points(16, 14, 2.5, 1.5);
let verdict = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Torus)
.expect("torus atom has a topology prediction");
assert_eq!(verdict.measured_betti.b0, 1, "torus is connected");
assert_eq!(
verdict.measured_betti.b2,
Some(1),
"torus encloses one H2 void"
);
assert!(
matches!(
verdict.resolution,
TopologyResolution::UnstableUnderCoarsening { .. }
),
"the embedded 16x14 cover must refuse H1 rather than report b1={}; \
resolution={:?}; note: {}",
verdict.measured_betti.b1,
verdict.resolution,
verdict.note
);
assert!(
!verdict.contested,
"an unresolved cover is not evidence against the raced torus: {}",
verdict.note
);
}
#[test]
fn sphere_signature_measures_h2_shell() {
let pts = octahedron_sphere_points();
let verdict = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Sphere)
.expect("sphere atom has a topology prediction");
assert_eq!(verdict.measured_betti.b0, 1, "sphere is connected");
assert_eq!(verdict.measured_betti.b1, 0, "sphere has no H1 loop");
assert_eq!(
verdict.measured_betti.b2,
Some(1),
"sphere has one H2 shell"
);
assert!(
!verdict.contested,
"octahedron sphere should match the sphere signature: {}",
verdict.note
);
}
fn arc_points(n: usize, r: f64) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((n, 2));
for i in 0..n {
let theta = std::f64::consts::PI * (i as f64) / ((n - 1) as f64);
pts[[i, 0]] = r * theta.cos();
pts[[i, 1]] = r * theta.sin();
}
pts
}
#[test]
fn atlas_nerve_recovers_circle_and_arc() {
let circle = atlas_nerve(circle_points(60, 2.0).view());
assert!(
circle.is_circle(),
"the nerve of a circle cover must recover S¹ (b₁=1, one component): {circle:?}"
);
let arc = atlas_nerve(arc_points(60, 2.0).view());
assert!(
arc.is_arc(),
"the nerve of an arc cover must recover a path (b₁=0, one component): {arc:?}"
);
assert!(
!arc.is_circle(),
"an arc must not be mistaken for a circle: {arc:?}"
);
}
#[test]
fn torus_two_h1_generators_resolution_robust_2159() {
for &(nu, nv) in &[(12usize, 10usize), (14, 12), (16, 14)] {
let clifford =
topology_persistence_verdict(torus_points(nu, nv).view(), &SaeAtomBasisKind::Torus)
.expect("torus atom has a topology prediction");
assert_eq!(
clifford.resolution,
TopologyResolution::Resolved,
"Clifford {nu}x{nv} is expected to resolve H1; note: {}",
clifford.note
);
assert_eq!(
clifford.measured_betti.b1, 2,
"Clifford torus {nu}x{nv} must measure two H1 generators; note: {}; H1: {:?}",
clifford.note, clifford.h1
);
assert_eq!(
clifford.measured_betti.b0, 1,
"torus {nu}x{nv} is connected"
);
}
let mut inadmissible = vec![(
"clifford_10x8".to_string(),
topology_persistence_verdict(torus_points(10, 8).view(), &SaeAtomBasisKind::Torus)
.expect("torus atom has a topology prediction"),
)];
for &(nu, nv) in &[(12usize, 10usize), (14, 12), (16, 14), (10, 8)] {
inadmissible.push((
format!("embedded_{nu}x{nv}"),
topology_persistence_verdict(
embedded_torus_points(nu, nv, 2.5, 1.5).view(),
&SaeAtomBasisKind::Torus,
)
.expect("torus atom has a topology prediction"),
));
}
for (label, verdict) in inadmissible {
assert!(
!verdict.resolution.is_resolved(),
"{label}: this cover does not resolve H1 and must not claim to (b1={}, resolution={:?})",
verdict.measured_betti.b1,
verdict.resolution
);
assert!(
!verdict.contested,
"{label}: an unresolved cover is not evidence against the raced type: {}",
verdict.note
);
}
}
#[test]
fn precomputed_kind_has_no_prediction_to_contest() {
let pts = circle_points(20, 1.0);
let verdict =
topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Precomputed("x".into()));
assert!(
verdict.is_none(),
"a caller-supplied basis carries no library topology to audit"
);
}