use g_math::fixed_point::{FixedPoint, FixedVector};
use crate::constants;
use crate::hyperbolic_geometry::HyperbolicPoint;
#[derive(Clone, Debug)]
pub struct KleinPoint {
pub coords: FixedVector,
pub weight: FixedPoint,
}
impl KleinPoint {
pub fn new(coords: FixedVector) -> Self {
let weight = FixedPoint::from_int(1) - coords.length_squared();
Self { coords, weight }
}
pub fn dimension(&self) -> usize {
self.coords.len()
}
}
pub fn poincare_to_klein(p: &HyperbolicPoint) -> KleinPoint {
let dim = p.dimension();
let norm_sq = p.coords().length_squared();
let one = FixedPoint::from_int(1);
let two = FixedPoint::from_int(2);
let denom = one + norm_sq; let scale = two / denom;
let mut klein_coords = FixedVector::new(dim);
for i in 0..dim {
klein_coords[i] = p.coords()[i] * scale;
}
KleinPoint::new(klein_coords)
}
pub fn klein_to_poincare(k: &KleinPoint) -> HyperbolicPoint {
let dim = k.dimension();
let one = FixedPoint::from_int(1);
let norm_sq = k.coords.length_squared();
if norm_sq < constants::small_epsilon() {
return HyperbolicPoint::origin(dim);
}
let sqrt_term = (one - norm_sq).sqrt(); let denom = one + sqrt_term;
let inv_denom = one / denom;
let mut poincare_coords = FixedVector::new(dim);
for i in 0..dim {
poincare_coords[i] = k.coords[i] * inv_denom;
}
HyperbolicPoint::new(poincare_coords)
}
pub fn weighted_barycenter(sites: &[(KleinPoint, FixedPoint)]) -> Option<KleinPoint> {
let zero = FixedPoint::from_int(0);
let one = FixedPoint::from_int(1);
let mut dim = 0;
let mut denom = zero;
let mut numer: Option<FixedVector> = None;
for (site, w) in sites {
if *w <= zero {
continue;
}
let radicand = if site.weight > constants::small_epsilon() {
site.weight
} else {
constants::small_epsilon()
};
let gamma = one / radicand.sqrt();
let coeff = *w * gamma;
if numer.is_none() {
dim = site.dimension();
numer = Some(FixedVector::new(dim));
}
let acc = numer.as_mut().unwrap();
for i in 0..dim {
acc[i] += site.coords[i] * coeff;
}
denom += coeff;
}
let numer = numer?;
if denom <= zero {
return None;
}
let inv = one / denom;
let mut coords = FixedVector::new(dim);
for i in 0..dim {
coords[i] = numer[i] * inv;
}
Some(KleinPoint::new(coords))
}
pub fn power_distance(query: &FixedVector, site: &KleinPoint) -> FixedPoint {
let dim = query.len();
assert_eq!(dim, site.dimension(), "Dimension mismatch");
let mut dist_sq = FixedPoint::from_int(0);
for i in 0..dim {
let d = query[i] - site.coords[i];
dist_sq = dist_sq + d * d;
}
dist_sq - site.weight
}
pub fn nearest_by_power_distance(query: &FixedVector, sites: &[KleinPoint]) -> Option<(usize, FixedPoint)> {
if sites.is_empty() {
return None;
}
let mut best_idx = 0;
let mut best_pd = power_distance(query, &sites[0]);
for (i, site) in sites.iter().enumerate().skip(1) {
let pd = power_distance(query, site);
if pd < best_pd {
best_pd = pd;
best_idx = i;
}
}
Some((best_idx, best_pd))
}
#[derive(Clone, Debug)]
pub struct HalfPlane {
pub normal: FixedVector,
pub offset: FixedPoint,
pub neighbor_id: String,
}
#[derive(Clone, Debug)]
pub struct PowerCell {
pub node_id: String,
pub site: KleinPoint,
pub half_planes: Vec<HalfPlane>,
}
pub fn compute_bisector(site_i: &KleinPoint, site_j: &KleinPoint, neighbor_id: &str) -> HalfPlane {
let dim = site_i.dimension();
assert_eq!(dim, site_j.dimension(), "Dimension mismatch");
let mut normal = FixedVector::new(dim);
for i in 0..dim {
normal[i] = site_j.coords[i] - site_i.coords[i];
}
let offset = site_j.coords.length_squared() - site_i.coords.length_squared();
HalfPlane {
normal,
offset,
neighbor_id: neighbor_id.to_string(),
}
}
pub fn point_in_cell(query: &FixedVector, cell: &PowerCell) -> bool {
for hp in &cell.half_planes {
let dot = query.dot(&hp.normal);
if dot > hp.offset {
return false;
}
}
true
}
pub struct PointLocationGrid {
pub resolution: usize,
dimension: usize,
cell_size: FixedPoint,
inv_cell_size: FixedPoint,
grid: Vec<Option<String>>,
tile_owners: std::collections::HashMap<String, Vec<usize>>,
}
impl PointLocationGrid {
pub fn new(resolution: usize) -> Self {
Self::with_dimension(resolution, 2)
}
pub fn with_dimension(resolution: usize, dimension: usize) -> Self {
let resolution = resolution.max(1);
let res_fp = FixedPoint::from_int(resolution as i32);
let two = FixedPoint::from_int(2);
let cell_size = two / res_fp;
let inv_cell_size = res_fp / two;
Self {
resolution,
dimension,
cell_size,
inv_cell_size,
grid: vec![None; resolution * resolution],
tile_owners: std::collections::HashMap::new(),
}
}
pub fn build(&mut self, sites: &[(String, KleinPoint)]) {
if sites.is_empty() {
return;
}
let one = FixedPoint::from_int(1);
for row in 0..self.resolution {
for col in 0..self.resolution {
let center = self.tile_center(row, col);
if center.length_squared() >= one {
self.grid[row * self.resolution + col] = None;
continue;
}
let mut best_id: Option<&str> = None;
let mut best_pd = FixedPoint::from_int(0);
let mut first = true;
for (id, site) in sites {
let pd = power_distance(¢er, site);
if first || pd < best_pd {
best_pd = pd;
best_id = Some(id.as_str());
first = false;
}
}
self.grid[row * self.resolution + col] = best_id.map(|s| s.to_string());
}
}
self.tile_owners.clear();
for (idx, cell) in self.grid.iter().enumerate() {
if let Some(ref id) = cell {
self.tile_owners.entry(id.clone()).or_default().push(idx);
}
}
}
pub fn query(&self, query_klein: &FixedVector) -> Option<&str> {
let (row, col) = self.coords_to_tile(query_klein);
if row >= self.resolution || col >= self.resolution {
return None;
}
self.grid[row * self.resolution + col].as_deref()
}
pub fn update_insert(&mut self, parent_id: &str, new_id: &str, new_site: &KleinPoint, parent_site: &KleinPoint) {
let one = FixedPoint::from_int(1);
let parent_tiles = match self.tile_owners.get(parent_id) {
Some(tiles) => tiles.clone(),
None => return,
};
let mut tiles_to_reassign = Vec::new();
for &idx in &parent_tiles {
let row = idx / self.resolution;
let col = idx % self.resolution;
let center = self.tile_center(row, col);
if center.length_squared() >= one {
continue;
}
let pd_parent = power_distance(¢er, parent_site);
let pd_new = power_distance(¢er, new_site);
if pd_new < pd_parent {
tiles_to_reassign.push(idx);
}
}
for &idx in &tiles_to_reassign {
self.grid[idx] = Some(new_id.to_string());
}
if !tiles_to_reassign.is_empty() {
if let Some(parent_list) = self.tile_owners.get_mut(parent_id) {
parent_list.retain(|idx| !tiles_to_reassign.contains(idx));
}
self.tile_owners.entry(new_id.to_string())
.or_default()
.extend(&tiles_to_reassign);
}
}
pub fn update_delete(&mut self, deleted_id: &str, parent_id: &str) {
let deleted_tiles = match self.tile_owners.remove(deleted_id) {
Some(tiles) => tiles,
None => return,
};
for &idx in &deleted_tiles {
self.grid[idx] = Some(parent_id.to_string());
}
self.tile_owners.entry(parent_id.to_string())
.or_default()
.extend(deleted_tiles);
}
fn tile_center(&self, row: usize, col: usize) -> FixedVector {
let half = constants::half();
let one = FixedPoint::from_int(1);
let col_fp = FixedPoint::from_int(col as i32);
let row_fp = FixedPoint::from_int(row as i32);
let x = -one + (col_fp + half) * self.cell_size;
let y = -one + (row_fp + half) * self.cell_size;
let mut v = FixedVector::new(self.dimension);
v[0] = x;
if self.dimension >= 2 {
v[1] = y;
}
v
}
fn coords_to_tile(&self, coords: &FixedVector) -> (usize, usize) {
let one = FixedPoint::from_int(1);
let x = coords[0];
let y = if coords.len() >= 2 { coords[1] } else { FixedPoint::from_int(0) };
let col_fp = (x + one) * self.inv_cell_size;
let row_fp = (y + one) * self.inv_cell_size;
let col = col_fp.to_int().max(0) as usize;
let row = row_fp.to_int().max(0) as usize;
(row.min(self.resolution - 1), col.min(self.resolution - 1))
}
pub fn assigned_tile_count(&self) -> usize {
self.grid.iter().filter(|t| t.is_some()).count()
}
pub fn resolution(&self) -> usize {
self.resolution
}
}
impl std::fmt::Debug for PointLocationGrid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PointLocationGrid(resolution={}, assigned={})",
self.resolution, self.assigned_tile_count())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants;
fn fp(v: i32) -> FixedPoint {
FixedPoint::from_int(v)
}
fn fp_approx_eq(a: FixedPoint, b: FixedPoint, tol: FixedPoint) -> bool {
(a - b).abs() < tol
}
fn klein_at(x: f32, y: f32) -> KleinPoint {
poincare_to_klein(&HyperbolicPoint::from_f32_slice(&[x, y]))
}
#[test]
fn barycenter_single_site_is_identity() {
let site = klein_at(0.4, -0.2);
let m = weighted_barycenter(&[(site.clone(), fp(3))]).unwrap();
assert!(fp_approx_eq(m.coords[0], site.coords[0], constants::epsilon()));
assert!(fp_approx_eq(m.coords[1], site.coords[1], constants::epsilon()));
}
#[test]
fn barycenter_equal_weights_matches_verified_midpoint() {
let pa = HyperbolicPoint::from_f32_slice(&[0.5, 0.1]);
let pb = HyperbolicPoint::from_f32_slice(&[-0.2, 0.4]);
let expected = pa.hyperbolic_midpoint(&pb);
let m = weighted_barycenter(&[
(poincare_to_klein(&pa), fp(1)),
(poincare_to_klein(&pb), fp(1)),
])
.unwrap();
let got = klein_to_poincare(&m);
let tol = FixedPoint::from_int(1) / FixedPoint::from_int(1000);
assert!(
fp_approx_eq(got.coords()[0], expected.coords()[0], tol)
&& fp_approx_eq(got.coords()[1], expected.coords()[1], tol),
"einstein midpoint {:?} != gyro midpoint {:?}",
got, expected
);
}
#[test]
fn barycenter_is_weight_scale_invariant() {
let sites = [klein_at(0.3, 0.3), klein_at(-0.4, 0.1), klein_at(0.0, -0.5)];
let a = weighted_barycenter(&[
(sites[0].clone(), fp(1)),
(sites[1].clone(), fp(2)),
(sites[2].clone(), fp(3)),
])
.unwrap();
let b = weighted_barycenter(&[
(sites[0].clone(), fp(7)),
(sites[1].clone(), fp(14)),
(sites[2].clone(), fp(21)),
])
.unwrap();
let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100000);
assert!(fp_approx_eq(a.coords[0], b.coords[0], tol));
assert!(fp_approx_eq(a.coords[1], b.coords[1], tol));
}
#[test]
fn barycenter_stays_inside_disk_and_handles_zero_weights() {
let m = weighted_barycenter(&[
(klein_at(0.9, 0.0), fp(100)),
(klein_at(-0.9, 0.0), fp(1)),
])
.unwrap();
assert!(m.coords.length_squared() < FixedPoint::from_int(1));
assert!(weighted_barycenter(&[(klein_at(0.5, 0.0), fp(0))]).is_none());
assert!(weighted_barycenter(&[]).is_none());
let only_positive = weighted_barycenter(&[
(klein_at(0.5, 0.0), fp(0)),
(klein_at(0.2, 0.2), fp(1)),
(klein_at(0.7, 0.0), fp(-2)),
])
.unwrap();
let expected = klein_at(0.2, 0.2);
assert!(fp_approx_eq(only_positive.coords[0], expected.coords[0], constants::epsilon()));
assert!(fp_approx_eq(only_positive.coords[1], expected.coords[1], constants::epsilon()));
}
#[test]
fn test_klein_origin_maps_to_origin() {
let origin = HyperbolicPoint::origin(2);
let k = poincare_to_klein(&origin);
assert!(k.coords[0].abs() < constants::epsilon());
assert!(k.coords[1].abs() < constants::epsilon());
assert!(fp_approx_eq(k.weight, fp(1), constants::epsilon()));
}
#[test]
fn test_klein_roundtrip() {
let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
let k = poincare_to_klein(&p);
let p2 = klein_to_poincare(&k);
let tol = constants::epsilon();
assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
"x roundtrip: {} vs {}", p.coords()[0], p2.coords()[0]);
assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
"y roundtrip: {} vs {}", p.coords()[1], p2.coords()[1]);
}
#[test]
fn test_klein_roundtrip_multiple() {
let test_points: Vec<[f32; 2]> = vec![
[0.3, 0.2],
[-0.4, 0.1],
[0.0, 0.7],
[0.1, -0.5],
[0.8, 0.0],
];
let tol = constants::epsilon();
for coords in &test_points {
let p = HyperbolicPoint::from_f32_slice(coords);
let k = poincare_to_klein(&p);
let p2 = klein_to_poincare(&k);
assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
"Roundtrip failed for ({}, {})", coords[0], coords[1]);
assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
"Roundtrip failed for ({}, {})", coords[0], coords[1]);
}
}
#[test]
fn test_klein_known_example() {
let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
let k = poincare_to_klein(&p);
let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100);
let expected_x = FixedPoint::from_int(4) / FixedPoint::from_int(5); let expected_w = FixedPoint::from_int(36) / FixedPoint::from_int(100);
assert!(fp_approx_eq(k.coords[0], expected_x, tol),
"Klein x: expected 0.8, got {}", k.coords[0]);
assert!(k.coords[1].abs() < tol,
"Klein y: expected 0, got {}", k.coords[1]);
assert!(fp_approx_eq(k.weight, expected_w, tol),
"Klein weight: expected 0.36, got {}", k.weight);
}
#[test]
fn test_klein_boundary_behavior() {
let near_boundary = HyperbolicPoint::from_f32_slice(&[0.95, 0.0]);
let k = poincare_to_klein(&near_boundary);
let k_norm = k.coords.length();
assert!(k_norm > FixedPoint::from_int(9) / FixedPoint::from_int(10),
"Klein norm should be near 1 for boundary point, got {}", k_norm);
assert!(k_norm < FixedPoint::from_int(1),
"Klein norm should be < 1, got {}", k_norm);
}
#[test]
fn test_power_distance_at_site_center() {
let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
let k = poincare_to_klein(&p);
let pd = power_distance(&k.coords, &k);
let expected = -k.weight;
let tol = constants::epsilon();
assert!(fp_approx_eq(pd, expected, tol),
"Power distance at site center should be -weight: {} vs {}", pd, expected);
assert!(pd < FixedPoint::from_int(0),
"Power distance at own site should be negative");
}
#[test]
fn test_power_distance_ordering_matches_hyperbolic() {
let query_p = HyperbolicPoint::from_f32_slice(&[0.1, 0.1]);
let site1_p = HyperbolicPoint::from_f32_slice(&[0.2, 0.0]);
let site2_p = HyperbolicPoint::from_f32_slice(&[0.6, 0.3]);
let query_k = poincare_to_klein(&query_p);
let site1_k = poincare_to_klein(&site1_p);
let site2_k = poincare_to_klein(&site2_p);
let pd1 = power_distance(&query_k.coords, &site1_k);
let pd2 = power_distance(&query_k.coords, &site2_k);
let hd1 = query_p.hyperbolic_distance(&site1_p);
let hd2 = query_p.hyperbolic_distance(&site2_p);
if hd1 < hd2 {
assert!(pd1 < pd2,
"Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
pd1, pd2, hd1, hd2);
} else {
assert!(pd2 <= pd1,
"Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
pd1, pd2, hd1, hd2);
}
}
#[test]
fn test_nearest_by_power_distance() {
let sites = vec![
KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0])),
KleinPoint::new(FixedVector::from_f32_slice(&[0.8, 0.0])),
KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.5])),
];
let query = FixedVector::from_f32_slice(&[0.1, 0.0]);
let (idx, _pd) = nearest_by_power_distance(&query, &sites).unwrap();
assert_eq!(idx, 0, "Nearest should be site 0");
}
#[test]
fn test_compute_bisector_symmetry() {
let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
let hp_ij = compute_bisector(&site_i, &site_j, "j");
let hp_ji = compute_bisector(&site_j, &site_i, "i");
let tol = constants::epsilon();
assert!(fp_approx_eq(hp_ij.normal[0], -hp_ji.normal[0], tol));
assert!(fp_approx_eq(hp_ij.offset, -hp_ji.offset, tol));
}
#[test]
fn test_point_in_cell_at_site_center() {
let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
let hp = compute_bisector(&site_i, &site_j, "j");
let cell = PowerCell {
node_id: "i".to_string(),
site: site_i.clone(),
half_planes: vec![hp],
};
assert!(point_in_cell(&site_i.coords, &cell),
"Site center should be inside its own cell");
}
#[test]
fn test_bisector_midpoint_on_boundary() {
let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
let hp = compute_bisector(&site_i, &site_j, "j");
let mut midpoint = FixedVector::new(2);
midpoint[0] = (site_i.coords[0] + site_j.coords[0]) * constants::half();
midpoint[1] = (site_i.coords[1] + site_j.coords[1]) * constants::half();
let _pd_i = power_distance(&midpoint, &site_i);
let _pd_j = power_distance(&midpoint, &site_j);
let bisector_x = hp.offset / hp.normal[0];
let mut bisector_pt = FixedVector::new(2);
bisector_pt[0] = bisector_x;
let pd_i_bpt = power_distance(&bisector_pt, &site_i);
let pd_j_bpt = power_distance(&bisector_pt, &site_j);
let tol = constants::epsilon();
assert!(fp_approx_eq(pd_i_bpt, pd_j_bpt, tol),
"Bisector point should have equal power distances: {} vs {}", pd_i_bpt, pd_j_bpt);
}
#[test]
fn test_cell_membership_consistency() {
let site_a = KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]));
let site_b = KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]));
let hp_ab = compute_bisector(&site_a, &site_b, "b");
let hp_ba = compute_bisector(&site_b, &site_a, "a");
let cell_a = PowerCell {
node_id: "a".to_string(),
site: site_a.clone(),
half_planes: vec![hp_ab],
};
let cell_b = PowerCell {
node_id: "b".to_string(),
site: site_b.clone(),
half_planes: vec![hp_ba],
};
let test_xs: Vec<f32> = vec![-0.8, -0.5, -0.2, 0.0, 0.2, 0.5, 0.8];
for &x in &test_xs {
let q = FixedVector::from_f32_slice(&[x, 0.0]);
let in_a = point_in_cell(&q, &cell_a);
let in_b = point_in_cell(&q, &cell_b);
assert!(in_a || in_b,
"Point ({}, 0) should be in at least one cell", x);
}
}
#[test]
fn test_grid_single_site() {
let sites = vec![
("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
];
let mut grid = PointLocationGrid::new(16);
grid.build(&sites);
let test_points: Vec<[f32; 2]> = vec![[0.0, 0.0], [0.5, 0.0], [0.0, -0.5], [0.3, 0.3]];
for coords in &test_points {
let q = FixedVector::from_f32_slice(coords);
let result = grid.query(&q);
assert_eq!(result, Some("root"), "Single site should own all tiles");
}
}
#[test]
fn test_grid_query_matches_brute_force() {
let sites = vec![
("a".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]))),
("b".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]))),
("c".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.4]))),
];
let mut grid = PointLocationGrid::new(32);
grid.build(&sites);
let test_points: Vec<[f32; 2]> = vec![
[0.2, 0.0], [-0.2, 0.0], [0.0, 0.3],
[0.5, 0.1], [-0.4, -0.2], [0.1, 0.5],
];
let klein_sites: Vec<KleinPoint> = sites.iter().map(|(_, s)| s.clone()).collect();
let ids: Vec<&str> = sites.iter().map(|(id, _)| id.as_str()).collect();
for coords in &test_points {
let q = FixedVector::from_f32_slice(coords);
if q.length_squared() >= FixedPoint::from_int(1) {
continue;
}
let grid_result = grid.query(&q);
let (brute_idx, _) = nearest_by_power_distance(&q, &klein_sites).unwrap();
let brute_result = ids[brute_idx];
assert_eq!(grid_result, Some(brute_result),
"Grid mismatch at ({}, {}): grid={:?} brute={}",
coords[0], coords[1], grid_result, brute_result);
}
}
#[test]
fn test_grid_insert_update() {
let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
let sites = vec![
("parent".to_string(), parent_site.clone()),
];
let mut grid = PointLocationGrid::new(16);
grid.build(&sites);
let q = FixedVector::from_f32_slice(&[0.5, 0.0]);
assert_eq!(grid.query(&q), Some("parent"));
let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
grid.update_insert("parent", "child", &child_site, &parent_site);
let q_near_child = FixedVector::from_f32_slice(&[0.6, 0.0]);
let result = grid.query(&q_near_child);
assert_eq!(result, Some("child"),
"After insert, tile near child should be owned by child");
let q_origin = FixedVector::from_f32_slice(&[0.0, 0.0]);
assert_eq!(grid.query(&q_origin), Some("parent"),
"Origin tile should still be parent");
}
#[test]
fn test_grid_delete_update() {
let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
let sites = vec![
("parent".to_string(), parent_site.clone()),
("child".to_string(), child_site.clone()),
];
let mut grid = PointLocationGrid::new(16);
grid.build(&sites);
let q = FixedVector::from_f32_slice(&[0.6, 0.0]);
assert_eq!(grid.query(&q), Some("child"));
grid.update_delete("child", "parent");
assert_eq!(grid.query(&q), Some("parent"),
"After delete, child's tiles should revert to parent");
}
#[test]
fn test_grid_tile_count() {
let resolution = 64;
let sites = vec![
("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
];
let mut grid = PointLocationGrid::new(resolution);
grid.build(&sites);
let assigned = grid.assigned_tile_count();
let expected_approx = (std::f64::consts::PI / 4.0 * (resolution as f64).powi(2)) as usize;
let lower = expected_approx * 9 / 10;
let upper = expected_approx * 11 / 10;
assert!(assigned >= lower && assigned <= upper,
"Assigned tiles {} should be near π/4·{}² ≈ {} (range [{}, {}])",
assigned, resolution, expected_approx, lower, upper);
}
#[test]
fn test_klein_roundtrip_4d() {
let p = HyperbolicPoint::from_f32_slice(&[0.3, 0.2, -0.1, 0.15]);
let k = poincare_to_klein(&p);
let p2 = klein_to_poincare(&k);
let tol = constants::epsilon();
for i in 0..4 {
assert!(fp_approx_eq(p.coords()[i], p2.coords()[i], tol),
"4D roundtrip failed at dim {}: {} vs {}", i, p.coords()[i], p2.coords()[i]);
}
}
#[test]
fn test_power_distance_ordering_equidistant_sites() {
let tau = constants::default_tau();
let half_tau = tau * constants::half();
let r = half_tau.tanh();
let angles: Vec<FixedPoint> = vec![
FixedPoint::from_int(0),
FixedPoint::from_int(3) / FixedPoint::from_int(2),
FixedPoint::from_int(3),
FixedPoint::from_int(9) / FixedPoint::from_int(2),
];
let sites_p: Vec<HyperbolicPoint> = angles.iter().map(|a| {
let mut v = FixedVector::new(2);
let (sin_a, cos_a) = a.sincos();
v[0] = r * cos_a;
v[1] = r * sin_a;
HyperbolicPoint::new(v)
}).collect();
let sites_k: Vec<KleinPoint> = sites_p.iter().map(|p| poincare_to_klein(p)).collect();
for (qi, site) in sites_p.iter().enumerate() {
let mut q_coords = site.coords().clone();
q_coords[0] = q_coords[0] + constants::epsilon();
let q_p = HyperbolicPoint::new(q_coords.clone());
let q_k = poincare_to_klein(&q_p);
let (pd_nn, _) = nearest_by_power_distance(&q_k.coords, &sites_k).unwrap();
let mut hyp_dists: Vec<(usize, FixedPoint)> = sites_p.iter().enumerate()
.map(|(i, s)| (i, q_p.hyperbolic_distance(s)))
.collect();
hyp_dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
assert_eq!(pd_nn, hyp_dists[0].0,
"Power NN should match hyperbolic NN near site {}", qi);
}
}
#[test]
fn test_grid_vs_brute_force_stress() {
let site_coords: Vec<[f32; 2]> = vec![
[0.0, 0.0], [0.3, 0.0], [-0.3, 0.0], [0.0, 0.3], [0.0, -0.3],
[0.2, 0.2], [-0.2, 0.2], [0.2, -0.2], [-0.2, -0.2],
[0.5, 0.1], [-0.4, 0.3], [0.1, 0.6], [-0.1, -0.5],
];
let sites: Vec<(String, KleinPoint)> = site_coords.iter().enumerate()
.map(|(i, c)| {
let p = HyperbolicPoint::from_f32_slice(c);
let k = poincare_to_klein(&p);
(format!("node_{}", i), k)
})
.collect();
let mut grid = PointLocationGrid::new(64);
grid.build(&sites);
let klein_only: Vec<KleinPoint> = sites.iter().map(|(_, k)| k.clone()).collect();
let ids: Vec<&str> = sites.iter().map(|(id, _)| id.as_str()).collect();
let mut mismatches = 0;
let mut total = 0;
for xi in -9..=9 {
for yi in -9..=9 {
let x = xi as f32 / 10.0;
let y = yi as f32 / 10.0;
if x * x + y * y >= 0.99 {
continue;
}
let q = FixedVector::from_f32_slice(&[x, y]);
total += 1;
let grid_result = grid.query(&q);
let (brute_idx, _) = nearest_by_power_distance(&q, &klein_only).unwrap();
let brute_result = ids[brute_idx];
if grid_result != Some(brute_result) {
mismatches += 1;
}
}
}
let mismatch_rate = mismatches as f64 / total as f64;
assert!(mismatch_rate < 0.05,
"Grid vs brute-force mismatch rate {} ({}/{}) exceeds 5%",
mismatch_rate, mismatches, total);
}
#[test]
fn test_insert_preserves_grid_correctness() {
let mut sites: Vec<(String, KleinPoint)> = Vec::new();
let mut grid = PointLocationGrid::new(32);
let root_k = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
sites.push(("root".to_string(), root_k.clone()));
grid.build(&sites);
let child_coords: Vec<[f32; 2]> = vec![
[0.3, 0.0], [-0.3, 0.0], [0.0, 0.3], [0.0, -0.3],
[0.5, 0.1], [-0.4, 0.3], [0.1, 0.6], [-0.1, -0.5],
[0.2, 0.2], [-0.2, 0.2], [0.2, -0.2], [-0.2, -0.2],
[0.7, 0.0], [0.0, 0.7], [-0.6, 0.1], [0.3, -0.4],
[0.4, 0.4], [-0.3, -0.3], [0.15, 0.15], [-0.15, 0.45],
];
for (i, coords) in child_coords.iter().enumerate() {
let p = HyperbolicPoint::from_f32_slice(coords);
let k = poincare_to_klein(&p);
let new_id = format!("node_{}", i);
grid.update_insert("root", &new_id, &k, &root_k);
sites.push((new_id, k));
if (i + 1) % 5 == 0 {
let klein_only: Vec<KleinPoint> = sites.iter().map(|(_, k)| k.clone()).collect();
let test_queries: Vec<[f32; 2]> = vec![[0.0, 0.0], [0.2, 0.1], [-0.3, 0.2]];
for q_coords in &test_queries {
let q = FixedVector::from_f32_slice(q_coords);
if q.length_squared() >= fp(1) { continue; }
let grid_r = grid.query(&q);
let (_brute_idx, _) = nearest_by_power_distance(&q, &klein_only).unwrap();
assert!(grid_r.is_some(),
"Grid should return a result for query inside disk");
}
}
}
}
#[test]
fn test_empty_tree_grid() {
let grid = PointLocationGrid::new(16);
let q = FixedVector::from_f32_slice(&[0.0, 0.0]);
assert_eq!(grid.query(&q), None, "Empty grid should return None");
}
#[test]
fn test_query_outside_disk_clamped() {
let sites = vec![
("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
];
let mut grid = PointLocationGrid::new(16);
grid.build(&sites);
let q = FixedVector::from_f32_slice(&[1.5, 0.0]);
let _result = grid.query(&q);
}
#[test]
fn test_inverted_index_consistency_after_build() {
let sites = vec![
("a".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]))),
("b".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]))),
("c".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.4]))),
];
let mut grid = PointLocationGrid::new(32);
grid.build(&sites);
for (id, tiles) in &grid.tile_owners {
for &idx in tiles {
assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()),
"tile_owners[{}] contains idx {} but grid[{}] = {:?}",
id, idx, idx, grid.grid[idx]);
}
}
for (idx, cell) in grid.grid.iter().enumerate() {
if let Some(ref id) = cell {
let tiles = grid.tile_owners.get(id).expect("grid has id not in tile_owners");
assert!(tiles.contains(&idx),
"grid[{}] = {} but tile_owners[{}] doesn't contain {}", idx, id, id, idx);
}
}
}
#[test]
fn test_inverted_index_after_insert() {
let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
let sites = vec![("parent".to_string(), parent_site.clone())];
let mut grid = PointLocationGrid::new(16);
grid.build(&sites);
let parent_tiles_before = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
assert!(parent_tiles_before > 0, "Parent should own tiles after build");
let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
grid.update_insert("parent", "child", &child_site, &parent_site);
let parent_tiles_after = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
let child_tiles = grid.tile_owners.get("child").map(|v| v.len()).unwrap_or(0);
assert!(child_tiles > 0, "Child should own some tiles");
assert_eq!(parent_tiles_before, parent_tiles_after + child_tiles,
"Total tiles should be conserved: {} != {} + {}",
parent_tiles_before, parent_tiles_after, child_tiles);
for (id, tiles) in &grid.tile_owners {
for &idx in tiles {
assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()));
}
}
}
#[test]
fn test_inverted_index_after_delete() {
let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
let sites = vec![
("parent".to_string(), parent_site.clone()),
("child".to_string(), child_site.clone()),
];
let mut grid = PointLocationGrid::new(16);
grid.build(&sites);
let total_before: usize = grid.tile_owners.values().map(|v| v.len()).sum();
let child_tiles_before = grid.tile_owners.get("child").map(|v| v.len()).unwrap_or(0);
assert!(child_tiles_before > 0, "Child should own tiles");
grid.update_delete("child", "parent");
assert!(grid.tile_owners.get("child").is_none(),
"Deleted node should be removed from tile_owners");
let parent_tiles_after = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
assert_eq!(parent_tiles_after, total_before,
"Parent should absorb all tiles: {} vs {}", parent_tiles_after, total_before);
for (id, tiles) in &grid.tile_owners {
for &idx in tiles {
assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()));
}
}
}
}