use g_math::fixed_point::FixedPoint;
#[derive(Clone, Debug)]
pub struct HilbertMapper {
dims: usize,
bits: u32,
max_coord: u32, }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HilbertIndex(pub u128);
impl HilbertIndex {
pub fn value(&self) -> u128 { self.0 }
pub fn chunk_offset(&self, chunk_size: usize) -> u64 {
(self.0 as u64).wrapping_mul(chunk_size as u64)
}
}
impl HilbertMapper {
pub fn new(dims: usize, bits: u32) -> Self {
assert!(dims > 0 && dims <= 16, "dims must be 1-16");
assert!(bits > 0 && bits <= 16, "bits must be 1-16");
assert!((dims as u32) * bits <= 128, "total index bits must be ≤ 128");
Self {
dims,
bits,
max_coord: (1u32 << bits) - 1,
}
}
pub fn coords_to_index_fixed(&self, coords: &[FixedPoint]) -> HilbertIndex {
let zero = FixedPoint::from_int(0);
let one = FixedPoint::from_int(1);
let half = one / FixedPoint::from_int(2);
let scale = FixedPoint::from_int(self.max_coord as i32);
let mut axes: Vec<u32> = (0..self.dims)
.map(|i| {
let v = coords.get(i).copied().unwrap_or(zero);
let v = if v < zero { zero } else if v > one { one } else { v };
(v * scale + half).to_int() as u32
})
.collect();
self.axes_to_hilbert(&mut axes)
}
pub fn coords_to_index(&self, coords: &[f64]) -> HilbertIndex {
let mut axes: Vec<u32> = (0..self.dims)
.map(|i| {
let v = coords.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0);
(v * self.max_coord as f64).round() as u32
})
.collect();
self.axes_to_hilbert(&mut axes)
}
pub fn index_to_coords(&self, index: &HilbertIndex) -> Vec<f64> {
let axes = self.hilbert_to_axes(index);
axes.iter()
.map(|&a| a as f64 / self.max_coord as f64)
.collect()
}
pub fn resolution(&self) -> u32 { self.max_coord + 1 }
pub fn total_bits(&self) -> u32 { self.dims as u32 * self.bits }
fn axes_to_hilbert(&self, axes: &mut Vec<u32>) -> HilbertIndex {
let n = self.dims;
let b = self.bits;
let m = 1u32 << (b - 1);
let mut q = m;
while q > 1 {
let p = q - 1;
for i in 0..n {
if axes[i] & q != 0 {
axes[0] ^= p;
} else {
let t = (axes[0] ^ axes[i]) & p;
axes[0] ^= t;
axes[i] ^= t;
}
}
q >>= 1;
}
for i in 1..n {
axes[i] ^= axes[i - 1];
}
let mut t = 0u32;
let mut q = m;
while q > 1 {
if axes[n - 1] & q != 0 {
t ^= q - 1;
}
q >>= 1;
}
for i in 0..n {
axes[i] ^= t;
}
self.interleave(axes)
}
fn hilbert_to_axes(&self, index: &HilbertIndex) -> Vec<u32> {
let n = self.dims;
let b = self.bits;
let mut axes = self.deinterleave(index);
let t = axes[n - 1] >> 1;
for i in (1..n).rev() {
axes[i] ^= axes[i - 1];
}
axes[0] ^= t;
let mut q = 2u32;
while q != (1 << b) {
let p = q - 1;
let mut i = n - 1;
loop {
if axes[i] & q != 0 {
axes[0] ^= p;
} else {
let tt = (axes[0] ^ axes[i]) & p;
axes[0] ^= tt;
axes[i] ^= tt;
}
if i == 0 { break; }
i -= 1;
}
q <<= 1;
}
axes
}
fn interleave(&self, axes: &[u32]) -> HilbertIndex {
let mut index: u128 = 0;
for bit in (0..self.bits).rev() {
for dim in 0..self.dims {
index <<= 1;
if axes[dim] & (1 << bit) != 0 {
index |= 1;
}
}
}
HilbertIndex(index)
}
fn deinterleave(&self, index: &HilbertIndex) -> Vec<u32> {
let mut axes = vec![0u32; self.dims];
let mut val = index.0;
for bit in 0..self.bits {
for dim in (0..self.dims).rev() {
if val & 1 != 0 {
axes[dim] |= 1 << bit;
}
val >>= 1;
}
}
axes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_2d() {
let h = HilbertMapper::new(2, 8);
let coords = vec![0.3, 0.7];
let idx = h.coords_to_index(&coords);
let back = h.index_to_coords(&idx);
assert!((back[0] - coords[0]).abs() < 0.01);
assert!((back[1] - coords[1]).abs() < 0.01);
}
#[test]
fn roundtrip_3d() {
let h = HilbertMapper::new(3, 10);
let coords = vec![0.5, 0.25, 0.75];
let idx = h.coords_to_index(&coords);
let back = h.index_to_coords(&idx);
for i in 0..3 {
assert!((back[i] - coords[i]).abs() < 0.002,
"dim {} mismatch: {} vs {}", i, back[i], coords[i]);
}
}
#[test]
fn locality_2d() {
let h = HilbertMapper::new(2, 12);
let a = h.coords_to_index(&[0.50, 0.50]);
let b = h.coords_to_index(&[0.51, 0.50]);
let c = h.coords_to_index(&[0.90, 0.10]);
let dist_ab = (a.0 as i128 - b.0 as i128).unsigned_abs();
let dist_ac = (a.0 as i128 - c.0 as i128).unsigned_abs();
assert!(dist_ab < dist_ac, "nearby points should have closer indices: ab={} ac={}", dist_ab, dist_ac);
}
#[test]
fn origin_is_zero() {
let h = HilbertMapper::new(3, 8);
let origin = h.coords_to_index(&[0.0, 0.0, 0.0]);
assert_eq!(origin.0, 0);
}
#[test]
fn index_space_bounded() {
let h = HilbertMapper::new(3, 8);
let max_index = (1u128 << (3 * 8)) - 1;
for coords in [[1.0,1.0,1.0],[0.0,1.0,0.0],[1.0,0.0,1.0],[0.5,0.5,0.5]] {
let idx = h.coords_to_index(&coords);
assert!(idx.0 <= max_index, "index {} exceeds max {}", idx.0, max_index);
}
}
#[test]
fn chunk_offset() {
let h = HilbertMapper::new(3, 8);
let idx = h.coords_to_index(&[0.5, 0.5, 0.5]);
let offset = idx.chunk_offset(4096); assert!(offset > 0);
}
#[test]
fn deterministic() {
let h = HilbertMapper::new(4, 10);
let coords = vec![0.33, 0.66, 0.11, 0.88];
let a = h.coords_to_index(&coords);
let b = h.coords_to_index(&coords);
assert_eq!(a, b);
}
}