use horon_engine::constants;
use horon_engine::Store;
use g_math::fixed_point::FixedPoint;
const REQUIRED_DEPTH: usize = 20;
fn build_spine(depth: usize) -> (Store, String, String) {
let store = Store::new();
let mut prefix = String::new();
for level in 0..depth {
prefix.push_str(&format!("/l{}", level));
store.put(&prefix, b"x").unwrap();
}
let a = format!("{}/sibling_a", prefix);
let b = format!("{}/sibling_b", prefix);
store.put(&a, b"a").unwrap();
store.put(&b, b"b").unwrap();
(store, a, b)
}
#[test]
fn siblings_stay_discoverable_to_required_depth() {
for depth in 1..=REQUIRED_DEPTH {
let (store, a, b) = build_spine(depth);
let neighbors = store.neighbors(&a, 3).unwrap();
assert!(
neighbors.contains(&b),
"at depth {} the sibling is not among the 3 nearest neighbours of {} \
(got {:?}) — the distance kernel is probably saturating; see \
constants::min_safe_denominator",
depth,
a,
neighbors
);
}
}
#[test]
fn norms_grow_monotonically_with_depth() {
let mut previous = 0.0f64;
for depth in 1..=REQUIRED_DEPTH {
let (store, a, _) = build_spine(depth);
let coords = store.position(&a).unwrap();
let norm: f64 = coords.iter().map(|c| c.to_f64() * c.to_f64()).sum::<f64>().sqrt();
assert!(
norm > previous,
"‖p‖ must grow with depth, but depth {} gave {:.12} after {:.12} \
— points are being rescaled back toward the origin",
depth,
norm,
previous
);
assert!(norm < 1.0, "‖p‖ must stay strictly inside the disk at depth {}", depth);
previous = norm;
}
}
#[test]
fn kernel_does_not_saturate_at_depth() {
let saturation =
(FixedPoint::from_int(2) * constants::near_boundary().atanh()).to_f64();
for depth in 4..=REQUIRED_DEPTH {
let (store, a, b) = build_spine(depth);
let d = distance_between(&store, &a, &b);
assert!(
(d - saturation).abs() > 1.0,
"at depth {} the kernel returned {:.8}, which is the saturation \
value {:.8} — the degenerate-denominator guard is firing on valid \
geometry",
depth,
d,
saturation
);
assert!(
d > 1.0 && d < 3.0,
"sibling separation at depth {} is {:.8}, outside the usable band \
— placement geometry has changed materially",
depth,
d
);
}
}
#[test]
fn nearest_neighbours_stay_exact_at_depth() {
const K: usize = 5;
for depth in [4usize, 12, 18, REQUIRED_DEPTH] {
let store = Store::new();
let mut prefix = String::new();
let mut leaves = Vec::new();
for level in 0..depth {
prefix.push_str(&format!("/l{}", level));
store.put(&prefix, b"spine").unwrap();
for child in 0..3 {
let key = format!("{}/c{}", prefix, child);
store.put(&key, b"leaf").unwrap();
leaves.push(key);
}
}
let positioned: Vec<(String, Vec<f64>)> = store
.list("/")
.unwrap()
.into_iter()
.filter(|k| k != "/")
.filter_map(|k| {
store.position(&k).ok().map(|p| {
(k, p.iter().map(|c| c.to_f64()).collect::<Vec<f64>>())
})
})
.collect();
for query in &leaves {
let qp: Vec<f64> =
store.position(query).unwrap().iter().map(|c| c.to_f64()).collect();
let mut truth: Vec<(String, f64)> = positioned
.iter()
.filter(|(k, _)| k != query)
.map(|(k, p)| (k.clone(), hyperbolic(&qp, p)))
.collect();
truth.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let got: Vec<String> = store
.neighbors(query, K + 1)
.unwrap()
.into_iter()
.filter(|k| k != "/")
.take(K)
.collect();
let cutoff = truth.get(K - 1).map(|(_, d)| *d).unwrap_or(f64::MAX);
let acceptable: std::collections::HashSet<&String> = truth
.iter()
.filter(|(_, d)| *d <= cutoff + 1e-9)
.map(|(k, _)| k)
.collect();
assert_eq!(
got.len(),
K.min(truth.len()),
"depth {}: expected {} neighbours for {}, got {:?}",
depth,
K,
query,
got
);
for candidate in &got {
assert!(
acceptable.contains(candidate),
"depth {}: {} returned {} as a nearest neighbour, but it is \
outside the true k-NN set (cutoff {:.8}); true top {}: {:?}",
depth,
query,
candidate,
cutoff,
K,
truth.iter().take(K).collect::<Vec<_>>()
);
}
}
}
}
fn hyperbolic(pa: &[f64], pb: &[f64]) -> f64 {
let na: f64 = pa.iter().map(|x| x * x).sum();
let nb: f64 = pb.iter().map(|x| x * x).sum();
let d2: f64 = pa.iter().zip(pb.iter()).map(|(x, y)| (x - y) * (x - y)).sum();
(1.0 + 2.0 * d2 / ((1.0 - na) * (1.0 - nb))).acosh()
}
fn distance_between(store: &Store, a: &str, b: &str) -> f64 {
let pa: Vec<f64> = store.position(a).unwrap().iter().map(|c| c.to_f64()).collect();
let pb: Vec<f64> = store.position(b).unwrap().iter().map(|c| c.to_f64()).collect();
let na: f64 = pa.iter().map(|x| x * x).sum();
let nb: f64 = pb.iter().map(|x| x * x).sum();
let d2: f64 = pa.iter().zip(pb.iter()).map(|(x, y)| (x - y) * (x - y)).sum();
(1.0 + 2.0 * d2 / ((1.0 - na) * (1.0 - nb))).acosh()
}