use crate::bounding_box::BoundingBox;
const MAX_HILBERT_ORDER: u32 = 32;
pub fn hilbert_index(x: f64, y: f64, order: u32) -> u64 {
debug_assert!((0.0..=1.0).contains(&x), "x must be in [0,1]");
debug_assert!((0.0..=1.0).contains(&y), "y must be in [0,1]");
debug_assert!(order > 0 && order <= MAX_HILBERT_ORDER, "order must be 1-32");
let n = 1u64 << order; let mut xi = (x * (n as f64 - 0.5)) as u64;
let mut yi = (y * (n as f64 - 0.5)) as u64;
xi = xi.min(n - 1);
yi = yi.min(n - 1);
xy2d(n, xi, yi)
}
pub fn hilbert_index_bounded(x: f64, y: f64, bounds: &BoundingBox, order: u32) -> u64 {
let x_range = bounds.max_x - bounds.min_x;
let y_range = bounds.max_y - bounds.min_y;
let x_norm = if x_range > 0.0 {
((x - bounds.min_x) / x_range).clamp(0.0, 1.0)
} else {
0.5
};
let y_norm = if y_range > 0.0 {
((y - bounds.min_y) / y_range).clamp(0.0, 1.0)
} else {
0.5
};
hilbert_index(x_norm, y_norm, order)
}
fn xy2d(n: u64, x: u64, y: u64) -> u64 {
let mut d = 0u64;
let mut x = x;
let mut y = y;
let mut s = n / 2;
while s > 0 {
let rx = ((x & s) > 0) as u64;
let ry = ((y & s) > 0) as u64;
d += s * s * ((3 * rx) ^ ry);
rotate(s, &mut x, &mut y, rx, ry);
s /= 2;
}
d
}
fn rotate(n: u64, x: &mut u64, y: &mut u64, rx: u64, ry: u64) {
if ry == 0 {
if rx == 1 {
*x = n.wrapping_sub(1).wrapping_sub(*x);
*y = n.wrapping_sub(1).wrapping_sub(*y);
}
std::mem::swap(x, y);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hilbert_index_corners() {
let tl = hilbert_index(0.0, 1.0, 8);
let tr = hilbert_index(1.0, 1.0, 8);
let bl = hilbert_index(0.0, 0.0, 8);
let br = hilbert_index(1.0, 0.0, 8);
let mut indices = vec![tl, tr, bl, br];
indices.sort_unstable();
indices.dedup();
assert_eq!(indices.len(), 4, "Corner indices should be unique");
}
#[test]
fn test_hilbert_index_spatial_locality() {
let center = hilbert_index(0.5, 0.5, 8);
let nearby = hilbert_index(0.50001, 0.50001, 8);
let diff = center.abs_diff(nearby);
assert!(diff < 1000, "Nearby points should have close indices");
}
#[test]
fn test_hilbert_index_bounded() {
let bounds = BoundingBox {
min_x: 0.0,
min_y: 0.0,
max_x: 100.0,
max_y: 100.0,
};
let idx1 = hilbert_index_bounded(50.0, 50.0, &bounds, 8);
let idx2 = hilbert_index_bounded(50.0, 50.0, &bounds, 8);
assert_eq!(idx1, idx2, "Same coordinates should produce same index");
}
#[test]
fn test_hilbert_index_order_affects_precision() {
let idx_8 = hilbert_index(0.5, 0.5, 8);
let idx_16 = hilbert_index(0.5, 0.5, 16);
assert!(idx_16 >= idx_8, "Higher order should produce larger indices");
}
#[test]
fn test_hilbert_index_boundary_values() {
let min_idx = hilbert_index(0.0, 0.0, 8);
assert_eq!(min_idx, 0, "Origin should have index 0");
let max_idx = hilbert_index(0.999, 0.999, 8);
assert!(max_idx > 0, "Near-max coordinates should have positive index");
let mid_idx = hilbert_index(0.5, 0.5, 8);
assert!(mid_idx > 0, "Midpoint should have positive index");
}
#[test]
fn test_hilbert_index_different_orders() {
for order in [1, 2, 4, 8, 16, 20, 24] {
let idx = hilbert_index(0.5, 0.5, order);
let max_possible = 1u64 << (2 * order);
assert!(idx < max_possible, "Index for order {} should be less than {}", order, max_possible);
}
}
#[test]
fn test_hilbert_index_symmetry() {
let idx_quarter = hilbert_index(0.25, 0.25, 8);
let idx_three_quarter = hilbert_index(0.75, 0.75, 8);
assert!(idx_quarter >= 0);
assert!(idx_three_quarter > 0);
assert_ne!(idx_quarter, idx_three_quarter);
}
#[test]
fn test_hilbert_index_bounded_normalization() {
let bounds = BoundingBox {
min_x: -100.0,
min_y: -100.0,
max_x: 100.0,
max_y: 100.0,
};
let idx_bounded = hilbert_index_bounded(0.0, 0.0, &bounds, 8);
let idx_direct = hilbert_index(0.5, 0.5, 8);
assert_eq!(idx_bounded, idx_direct, "Bounded center should match direct center");
}
#[test]
fn test_hilbert_index_bounded_outside_bounds_clamped() {
let bounds = BoundingBox {
min_x: 0.0,
min_y: 0.0,
max_x: 100.0,
max_y: 100.0,
};
let idx_outside = hilbert_index_bounded(150.0, 150.0, &bounds, 8);
let idx_max = hilbert_index_bounded(100.0, 100.0, &bounds, 8);
assert!(idx_outside >= 0);
assert!(idx_max >= 0);
}
#[test]
fn test_hilbert_index_bounded_zero_range() {
let point_bounds = BoundingBox {
min_x: 50.0,
min_y: 50.0,
max_x: 50.0,
max_y: 50.0,
};
let idx = hilbert_index_bounded(50.0, 50.0, &point_bounds, 8);
let expected = hilbert_index(0.5, 0.5, 8);
assert_eq!(idx, expected, "Zero-range bounds should map to center");
}
#[test]
fn test_hilbert_index_bounded_negative_coords() {
let bounds = BoundingBox {
min_x: -180.0,
min_y: -90.0,
max_x: 180.0,
max_y: 90.0,
};
let idx_neg = hilbert_index_bounded(-90.0, -45.0, &bounds, 8);
let idx_pos = hilbert_index_bounded(90.0, 45.0, &bounds, 8);
assert!(idx_neg >= 0);
assert!(idx_pos > 0);
assert_ne!(idx_neg, idx_pos);
}
#[test]
fn test_xy2d_basic() {
let d = xy2d(2, 0, 0);
assert_eq!(d, 0, "Origin in 2x2 grid should be 0");
let d = xy2d(2, 1, 1);
assert!(d > 0, "Non-origin in 2x2 grid should be positive");
}
#[test]
fn test_hilbert_index_grid_coverage() {
let order = 3; let n = 1u64 << order;
let mut indices = Vec::new();
for xi in 0..n {
for yi in 0..n {
let x = (xi as f64 + 0.5) / n as f64;
let y = (yi as f64 + 0.5) / n as f64;
indices.push(hilbert_index(x, y, order));
}
}
indices.sort_unstable();
indices.dedup();
assert!(indices.len() >= (n * n / 2) as usize, "Should cover most grid cells");
}
}