#![forbid(unsafe_code)]
use crate::geometry::coordinate_range::{
CoordinateRange, CoordinateRangeError, CoordinateRangeOrdering,
};
use core::fmt;
use num_traits::ToPrimitive;
use std::num::NonZeroU32;
pub const MAX_HILBERT_BITS: u32 = 31;
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum HilbertError {
#[error("bits parameter {bits} is out of valid range [1, 31]")]
InvalidBitsParameter {
bits: u32,
},
#[error(
"Hilbert index would overflow u128: dimension {dimension} * bits {bits} = {total_bits} > 128"
)]
IndexOverflow {
dimension: usize,
bits: u32,
total_bits: u128,
},
#[error("dimension {dimension} is too large (exceeds u32::MAX)")]
DimensionTooLarge {
dimension: usize,
},
#[error(
"Hilbert quantization bounds must be finite: lower bound finite = {lower_bound_finite}, upper bound finite = {upper_bound_finite}"
)]
NonFiniteBounds {
lower_bound_finite: bool,
upper_bound_finite: bool,
},
#[error("Hilbert quantization bounds must satisfy min < max ({ordering})")]
NonIncreasingBounds {
ordering: CoordinateRangeOrdering,
},
#[error("Hilbert quantization bounds produced a non-finite extent")]
NonFiniteBoundsExtent {},
#[error("Hilbert coordinate at index {coordinate_index} must be finite")]
NonFiniteCoordinate {
coordinate_index: usize,
},
#[error(
"Hilbert coordinate at index {coordinate_index} produced a non-finite normalized value"
)]
NonFiniteNormalizedCoordinate {
coordinate_index: usize,
},
#[error(
"Hilbert quantized coordinate at index {coordinate_index} for {bits} bits and grid maximum {max_grid_value} cannot be represented as u32"
)]
QuantizedCoordinateConversionFailed {
bits: u32,
max_grid_value: u32,
coordinate_index: usize,
},
#[error(
"pre-quantized Hilbert coordinate at point {point_index}, coordinate {coordinate_index} has value {coordinate}, which exceeds the maximum {max_grid_value} for {bits} bits"
)]
PrequantizedCoordinateOutOfRange {
bits: u32,
max_grid_value: u32,
point_index: usize,
coordinate_index: usize,
coordinate: u32,
},
#[error(
"Hilbert sort permutation length mismatch: item count {item_count}, permutation count {permutation_count}"
)]
InvalidSortPermutationLength {
item_count: usize,
permutation_count: usize,
},
#[error(
"Hilbert sort permutation index {permutation_index} has value {item_index}, which is outside item count {item_count}"
)]
InvalidSortPermutationIndex {
permutation_index: usize,
item_index: usize,
item_count: usize,
},
#[error("Hilbert sort permutation index {permutation_index} repeats item index {item_index}")]
InvalidSortPermutationDuplicate {
permutation_index: usize,
item_index: usize,
},
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[must_use]
pub struct HilbertBitDepth(NonZeroU32);
impl HilbertBitDepth {
pub const fn try_new(bits: u32) -> Result<Self, HilbertError> {
let Some(bits) = NonZeroU32::new(bits) else {
return Err(HilbertError::InvalidBitsParameter { bits: 0 });
};
if bits.get() > MAX_HILBERT_BITS {
return Err(HilbertError::InvalidBitsParameter { bits: bits.get() });
}
Ok(Self(bits))
}
#[must_use]
pub const fn get(self) -> u32 {
self.0.get()
}
}
impl TryFrom<u32> for HilbertBitDepth {
type Error = HilbertError;
fn try_from(bits: u32) -> Result<Self, Self::Error> {
Self::try_new(bits)
}
}
impl fmt::Display for HilbertBitDepth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
#[derive(Clone, Copy, Debug)]
#[must_use]
pub struct HilbertQuantizedBatch<'a, const D: usize> {
quantized: &'a [[u32; D]],
index_mode: HilbertIndexMode<D>,
}
impl<'a, const D: usize> HilbertQuantizedBatch<'a, D> {
pub fn try_new(quantized: &'a [[u32; D]], bits: HilbertBitDepth) -> Result<Self, HilbertError> {
let index_mode = HilbertIndexMode::try_new(bits)?;
if D != 0 {
validate_prequantized_coordinates(quantized, bits)?;
}
Ok(Self {
quantized,
index_mode,
})
}
#[must_use]
pub const fn coordinates(self) -> &'a [[u32; D]] {
self.quantized
}
pub const fn bits(self) -> HilbertBitDepth {
self.index_mode.bits()
}
#[must_use]
pub fn indices(self) -> Vec<u128> {
hilbert_indices_for_quantized_batch(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use]
pub struct HilbertQuantizedVec<const D: usize> {
quantized: Vec<[u32; D]>,
index_mode: HilbertIndexMode<D>,
}
impl<const D: usize> HilbertQuantizedVec<D> {
#[must_use]
pub fn coordinates(&self) -> &[[u32; D]] {
&self.quantized
}
pub const fn bits(&self) -> HilbertBitDepth {
self.index_mode.bits()
}
#[must_use]
pub const fn len(&self) -> usize {
self.quantized.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.quantized.is_empty()
}
#[must_use]
pub fn indices(&self) -> Vec<u128> {
indices_for_mode(&self.quantized, self.index_mode)
}
#[must_use]
pub fn into_indices_and_coordinates(self) -> (Vec<u128>, Vec<[u32; D]>) {
let indices = self.indices();
(indices, self.quantized)
}
#[must_use]
pub fn into_coordinates(self) -> Vec<[u32; D]> {
self.quantized
}
}
pub fn hilbert_quantize_batch_in_range<Item, const D: usize>(
items: &[Item],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
mut coords_of: impl FnMut(&Item) -> [f64; D],
) -> Result<HilbertQuantizedVec<D>, HilbertError> {
let index_mode = HilbertIndexMode::try_new(bits)?;
if D == 0 {
return Ok(HilbertQuantizedVec {
quantized: vec![[0_u32; D]; items.len()],
index_mode,
});
}
let (max_val_u32, max_val_t) = quantization_scale(bits);
let quantized = items
.iter()
.map(|item| {
let coords = coords_of(item);
quantize_with_scale(&coords, bounds, bits, max_val_u32, max_val_t)
})
.collect::<Result<Vec<[u32; D]>, HilbertError>>()?;
Ok(HilbertQuantizedVec {
quantized,
index_mode,
})
}
fn quantization_scale(bits: HilbertBitDepth) -> (u32, f64) {
let max_grid_value = max_quantized_coordinate(bits);
(max_grid_value, f64::from(max_grid_value))
}
const fn max_quantized_coordinate(bits: HilbertBitDepth) -> u32 {
(1_u32 << bits.get()) - 1
}
fn total_bits<const D: usize>(bits: HilbertBitDepth) -> Result<u128, HilbertError> {
let d_u32 = u32::try_from(D).map_err(|_| HilbertError::DimensionTooLarge { dimension: D })?;
Ok(u128::from(d_u32) * u128::from(bits.get()))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct HilbertIndexParams<const D: usize> {
bits: HilbertBitDepth,
}
impl<const D: usize> HilbertIndexParams<D> {
const fn bits(self) -> u32 {
self.bits.get()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HilbertIndexMode<const D: usize> {
ZeroDimensional { bits: HilbertBitDepth },
Positive(HilbertIndexParams<D>),
}
impl<const D: usize> HilbertIndexMode<D> {
fn try_new(bits: HilbertBitDepth) -> Result<Self, HilbertError> {
validate_index_width::<D>(bits)?;
if D == 0 {
Ok(Self::ZeroDimensional { bits })
} else {
Ok(Self::Positive(HilbertIndexParams { bits }))
}
}
const fn bits(self) -> HilbertBitDepth {
match self {
Self::ZeroDimensional { bits } => bits,
Self::Positive(params) => params.bits,
}
}
}
fn validate_index_width<const D: usize>(bits: HilbertBitDepth) -> Result<(), HilbertError> {
let total_bits = total_bits::<D>(bits)?;
if total_bits > 128 {
return Err(HilbertError::IndexOverflow {
dimension: D,
bits: bits.get(),
total_bits,
});
}
Ok(())
}
fn parse_hilbert_bounds(bounds: (f64, f64)) -> Result<CoordinateRange<f64>, HilbertError> {
let lower_bound_finite = bounds.0.is_finite();
let upper_bound_finite = bounds.1.is_finite();
CoordinateRange::try_from(bounds).map_err(|error| match error {
CoordinateRangeError::NonFiniteBound { .. } => HilbertError::NonFiniteBounds {
lower_bound_finite,
upper_bound_finite,
},
CoordinateRangeError::NonIncreasing { ordering, .. } => {
HilbertError::NonIncreasingBounds { ordering }
}
})
}
pub fn try_hilbert_quantize<const D: usize>(
coords: &[f64; D],
bounds: (f64, f64),
bits: HilbertBitDepth,
) -> Result<[u32; D], HilbertError> {
let bounds = parse_hilbert_bounds(bounds)?;
if D == 0 {
return Ok([0_u32; D]);
}
let (max_val_u32, max_val_t) = quantization_scale(bits);
quantize_with_scale(coords, bounds, bits, max_val_u32, max_val_t)
}
pub fn hilbert_quantize_in_range<const D: usize>(
coords: &[f64; D],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
) -> Result<[u32; D], HilbertError> {
if D == 0 {
return Ok([0_u32; D]);
}
let (max_val_u32, max_val_t) = quantization_scale(bits);
quantize_with_scale(coords, bounds, bits, max_val_u32, max_val_t)
}
#[inline]
fn quantize_with_scale<const D: usize>(
coords: &[f64; D],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
max_val_u32: u32,
max_val_t: f64,
) -> Result<[u32; D], HilbertError> {
let min = bounds.min();
let max = bounds.max();
let extent = max - min;
if !extent.is_finite() {
return Err(HilbertError::NonFiniteBoundsExtent {});
}
let mut quantized = [0_u32; D];
for (i, &coord) in coords.iter().enumerate() {
if !coord.is_finite() {
return Err(HilbertError::NonFiniteCoordinate {
coordinate_index: i,
});
}
let t = (coord - min) / extent;
let normalized = if t.is_finite() {
t.clamp(0.0, 1.0)
} else {
return Err(HilbertError::NonFiniteNormalizedCoordinate {
coordinate_index: i,
});
};
let scaled = normalized * max_val_t;
let Some(value) = scaled.round().to_u32() else {
return Err(HilbertError::QuantizedCoordinateConversionFailed {
bits: bits.get(),
max_grid_value: max_val_u32,
coordinate_index: i,
});
};
let q = value.min(max_val_u32);
quantized[i] = q;
}
Ok(quantized)
}
fn apply_order<Item>(
items: &mut [Item],
order: impl ExactSizeIterator<Item = usize>,
) -> Result<(), HilbertError> {
let item_len = items.len();
let permutation_count = order.len();
if item_len != permutation_count {
return Err(HilbertError::InvalidSortPermutationLength {
item_count: item_len,
permutation_count,
});
}
let mut ranks = vec![usize::MAX; item_len];
let mut observed_count = 0_usize;
for (new_index, old_index) in order.into_iter().enumerate() {
observed_count = new_index + 1;
if new_index >= item_len {
return Err(HilbertError::InvalidSortPermutationLength {
item_count: item_len,
permutation_count: observed_count,
});
}
if old_index >= item_len {
return Err(HilbertError::InvalidSortPermutationIndex {
permutation_index: new_index,
item_index: old_index,
item_count: item_len,
});
}
if ranks[old_index] != usize::MAX {
return Err(HilbertError::InvalidSortPermutationDuplicate {
permutation_index: new_index,
item_index: old_index,
});
}
ranks[old_index] = new_index;
}
if observed_count != item_len {
return Err(HilbertError::InvalidSortPermutationLength {
item_count: item_len,
permutation_count: observed_count,
});
}
for index in 0..item_len {
while ranks[index] != index {
let target = ranks[index];
items.swap(index, target);
ranks.swap(index, target);
}
}
Ok(())
}
pub fn try_hilbert_index<const D: usize>(
coords: &[f64; D],
bounds: (f64, f64),
bits: HilbertBitDepth,
) -> Result<u128, HilbertError> {
let bounds = parse_hilbert_bounds(bounds)?;
hilbert_index_in_range(coords, bounds, bits)
}
pub fn hilbert_index_in_range<const D: usize>(
coords: &[f64; D],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
) -> Result<u128, HilbertError> {
let index_mode = HilbertIndexMode::try_new(bits)?;
let HilbertIndexMode::Positive(index_params) = index_mode else {
return Ok(0);
};
let q = hilbert_quantize_in_range(coords, bounds, bits)?;
Ok(index_from_quantized(&q, index_params))
}
#[must_use]
fn index_from_quantized<const D: usize>(coords: &[u32; D], params: HilbertIndexParams<D>) -> u128 {
let bits = params.bits();
let mut transposed = *coords;
let highest_bit_mask = max_quantized_coordinate(params.bits).isolate_highest_one();
let mut bit_mask: u32 = highest_bit_mask;
while bit_mask > 1 {
let mask_minus_one = bit_mask - 1;
if (transposed[0] & bit_mask) != 0 {
transposed[0] ^= mask_minus_one;
}
let (first, rest) = transposed.split_at_mut(1);
let first_coord = &mut first[0];
for coord in rest {
if (*coord & bit_mask) != 0 {
*first_coord ^= mask_minus_one;
} else {
let toggle = (*first_coord ^ *coord) & mask_minus_one;
*first_coord ^= toggle;
*coord ^= toggle;
}
}
bit_mask >>= 1;
}
let mut prev = transposed[0];
for coord in transposed.iter_mut().skip(1) {
*coord ^= prev;
prev = *coord;
}
let mut gray_mask: u32 = 0;
bit_mask = highest_bit_mask;
while bit_mask > 1 {
if (transposed[D - 1] & bit_mask) != 0 {
gray_mask ^= bit_mask - 1;
}
bit_mask >>= 1;
}
for coord in &mut transposed {
*coord ^= gray_mask;
}
let mut index: u128 = 0;
for bit_pos in (0..bits).rev() {
for &coord in &transposed {
let bit_value = (coord >> bit_pos) & 1;
index = (index << 1) | u128::from(bit_value);
}
}
index
}
pub fn try_hilbert_sort_by_stable<Item, const D: usize>(
items: &mut [Item],
bounds: (f64, f64),
bits: HilbertBitDepth,
coords_of: impl FnMut(&Item) -> [f64; D],
) -> Result<(), HilbertError> {
let bounds = parse_hilbert_bounds(bounds)?;
hilbert_sort_by_stable_in_range(items, bounds, bits, coords_of)
}
pub fn hilbert_sort_by_stable_in_range<Item, const D: usize>(
items: &mut [Item],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
mut coords_of: impl FnMut(&Item) -> [f64; D],
) -> Result<(), HilbertError> {
let index_mode = HilbertIndexMode::try_new(bits)?;
let HilbertIndexMode::Positive(index_params) = index_mode else {
return Ok(());
};
let (max_val_u32, max_val_t) = quantization_scale(bits);
let mut keyed: Vec<((u128, [u32; D]), usize)> = items
.iter()
.enumerate()
.map(|(i, item)| {
let c = coords_of(item);
let q = quantize_with_scale(&c, bounds, bits, max_val_u32, max_val_t)?;
let idx = index_from_quantized(&q, index_params);
Ok(((idx, q), i))
})
.collect::<Result<_, HilbertError>>()?;
keyed.sort_by_key(|(key, _)| *key);
apply_order(items, keyed.into_iter().map(|(_, i)| i))?;
Ok(())
}
pub fn try_hilbert_sort_by_unstable<Item, const D: usize>(
items: &mut [Item],
bounds: (f64, f64),
bits: HilbertBitDepth,
coords_of: impl FnMut(&Item) -> [f64; D],
) -> Result<(), HilbertError> {
let bounds = parse_hilbert_bounds(bounds)?;
hilbert_sort_by_unstable_in_range(items, bounds, bits, coords_of)
}
pub fn hilbert_sort_by_unstable_in_range<Item, const D: usize>(
items: &mut [Item],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
mut coords_of: impl FnMut(&Item) -> [f64; D],
) -> Result<(), HilbertError> {
let index_mode = HilbertIndexMode::try_new(bits)?;
let HilbertIndexMode::Positive(index_params) = index_mode else {
return Ok(());
};
let (max_val_u32, max_val_t) = quantization_scale(bits);
let mut keyed: Vec<((u128, [u32; D]), usize)> = items
.iter()
.enumerate()
.map(|(i, item)| {
let c = coords_of(item);
let q = quantize_with_scale(&c, bounds, bits, max_val_u32, max_val_t)?;
let idx = index_from_quantized(&q, index_params);
Ok(((idx, q), i))
})
.collect::<Result<_, HilbertError>>()?;
keyed.sort_unstable_by_key(|(key, _)| *key);
apply_order(items, keyed.into_iter().map(|(_, i)| i))?;
Ok(())
}
fn validate_prequantized_coordinates<const D: usize>(
quantized: &[[u32; D]],
bits: HilbertBitDepth,
) -> Result<(), HilbertError> {
let max_grid_value = max_quantized_coordinate(bits);
for (point_index, point) in quantized.iter().enumerate() {
for (coordinate_index, &coordinate) in point.iter().enumerate() {
if coordinate > max_grid_value {
return Err(HilbertError::PrequantizedCoordinateOutOfRange {
bits: bits.get(),
max_grid_value,
point_index,
coordinate_index,
coordinate,
});
}
}
}
Ok(())
}
pub fn hilbert_indices_prequantized<const D: usize>(
quantized: &[[u32; D]],
bits: HilbertBitDepth,
) -> Result<Vec<u128>, HilbertError> {
Ok(HilbertQuantizedBatch::try_new(quantized, bits)?.indices())
}
#[must_use]
pub fn hilbert_indices_for_quantized_batch<const D: usize>(
batch: HilbertQuantizedBatch<'_, D>,
) -> Vec<u128> {
indices_for_mode(batch.quantized, batch.index_mode)
}
fn indices_for_mode<const D: usize>(
quantized: &[[u32; D]],
index_mode: HilbertIndexMode<D>,
) -> Vec<u128> {
match index_mode {
HilbertIndexMode::ZeroDimensional { .. } => vec![0_u128; quantized.len()],
HilbertIndexMode::Positive(index_params) => quantized
.iter()
.map(|q| index_from_quantized(q, index_params))
.collect(),
}
}
pub fn try_hilbert_sorted_indices<const D: usize>(
coords: &[[f64; D]],
bounds: (f64, f64),
bits: HilbertBitDepth,
) -> Result<Vec<usize>, HilbertError> {
let bounds = parse_hilbert_bounds(bounds)?;
hilbert_sorted_indices_in_range(coords, bounds, bits)
}
pub fn hilbert_sorted_indices_in_range<const D: usize>(
coords: &[[f64; D]],
bounds: CoordinateRange<f64>,
bits: HilbertBitDepth,
) -> Result<Vec<usize>, HilbertError> {
let index_mode = HilbertIndexMode::try_new(bits)?;
let HilbertIndexMode::Positive(index_params) = index_mode else {
return Ok((0..coords.len()).collect());
};
let (max_val_u32, max_val_t) = quantization_scale(bits);
let mut keyed: Vec<((u128, [u32; D]), usize)> = coords
.iter()
.enumerate()
.map(|(i, c)| {
let q = quantize_with_scale(c, bounds, bits, max_val_u32, max_val_t)?;
let idx = index_from_quantized(&q, index_params);
Ok(((idx, q), i))
})
.collect::<Result<_, HilbertError>>()?;
keyed.sort_by(|(ka, ia), (kb, ib)| ka.cmp(kb).then_with(|| ia.cmp(ib)));
Ok(keyed.into_iter().map(|(_, i)| i).collect())
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
use crate::geometry::point::Point;
fn bit_depth(value: u32) -> HilbertBitDepth {
HilbertBitDepth::try_new(value).expect("test bit depth must be valid")
}
fn positive_index_params<const D: usize>(bits: HilbertBitDepth) -> HilbertIndexParams<D> {
let HilbertIndexMode::Positive(params) =
HilbertIndexMode::<D>::try_new(bits).expect("test index parameters must be valid")
else {
panic!("test index parameters must be positive-dimensional");
};
params
}
struct LyingOrder {
values: std::vec::IntoIter<usize>,
reported_len: usize,
}
impl LyingOrder {
fn new(values: Vec<usize>, reported_len: usize) -> Self {
Self {
values: values.into_iter(),
reported_len,
}
}
}
impl Iterator for LyingOrder {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
self.values.next()
}
}
impl ExactSizeIterator for LyingOrder {
fn len(&self) -> usize {
self.reported_len
}
}
fn assert_prequantized_matches_hilbert_index<const D: usize>(
coords: &[[f64; D]],
bounds: (f64, f64),
bits: HilbertBitDepth,
) {
let quantized: Vec<[u32; D]> = coords
.iter()
.map(|c| try_hilbert_quantize(c, bounds, bits).unwrap())
.collect();
let indices_bulk =
hilbert_indices_prequantized(&quantized, bits).expect("valid quantized points");
let indices_individual: Vec<u128> = coords
.iter()
.map(|c| try_hilbert_index(c, bounds, bits).unwrap())
.collect();
assert_eq!(indices_bulk, indices_individual);
}
#[test]
fn test_hilbert_bit_depth_boundaries_and_traits() {
let min_bits = HilbertBitDepth::try_new(1).expect("minimum bit depth should be valid");
let max_bits =
HilbertBitDepth::try_new(MAX_HILBERT_BITS).expect("maximum bit depth should be valid");
assert_eq!(min_bits.get(), 1);
assert_eq!(max_bits.get(), MAX_HILBERT_BITS);
assert_eq!(max_bits.to_string(), MAX_HILBERT_BITS.to_string());
assert_eq!(HilbertBitDepth::try_from(8), HilbertBitDepth::try_new(8));
assert_matches!(
HilbertBitDepth::try_new(0),
Err(HilbertError::InvalidBitsParameter { bits: 0 })
);
assert_matches!(
HilbertBitDepth::try_new(32),
Err(HilbertError::InvalidBitsParameter { bits: 32 })
);
assert_matches!(
HilbertBitDepth::try_from(0),
Err(HilbertError::InvalidBitsParameter { bits: 0 })
);
assert_matches!(
HilbertBitDepth::try_from(MAX_HILBERT_BITS + 1),
Err(HilbertError::InvalidBitsParameter { bits }) if bits == MAX_HILBERT_BITS + 1
);
}
#[test]
fn test_hilbert_index_2d() {
let bits = bit_depth(4);
let origin = try_hilbert_index(&[0.0_f64, 0.0], (0.0, 1.0), bits).unwrap();
let corner = try_hilbert_index(&[1.0_f64, 1.0], (0.0, 1.0), bits).unwrap();
let center = try_hilbert_index(&[0.5_f64, 0.5], (0.0, 1.0), bits).unwrap();
assert_eq!(origin, 0);
assert_ne!(origin, center);
assert_ne!(center, corner);
}
#[test]
fn test_hilbert_index_3d() {
let bits = bit_depth(8);
let origin = try_hilbert_index(&[0.0_f64, 0.0, 0.0], (-1.0, 1.0), bits).unwrap();
let corner = try_hilbert_index(&[1.0_f64, 1.0, 1.0], (-1.0, 1.0), bits).unwrap();
assert_ne!(origin, corner);
}
macro_rules! gen_prequantized_matches_hilbert_index_tests {
($dim:literal, $coords:expr, $bounds:expr, $bits:expr) => {
pastey::paste! {
#[test]
fn [<test_hilbert_indices_prequantized_matches_hilbert_index_ $dim d>]() {
let coords: [[f64; $dim]; 4] = $coords;
assert_prequantized_matches_hilbert_index(
&coords,
$bounds,
bit_depth($bits),
);
}
}
};
}
gen_prequantized_matches_hilbert_index_tests!(
2,
[[-2.0, -1.0], [-1.5, 0.25], [0.1, -0.7], [3.0, 3.0]],
(-2.0_f64, 3.0_f64),
8
);
gen_prequantized_matches_hilbert_index_tests!(
3,
[
[-2.0, -1.0, 0.0],
[-1.5, 0.25, 1.75],
[0.1, -0.7, 2.2],
[3.0, 3.0, -2.0],
],
(-2.0_f64, 3.0_f64),
8
);
gen_prequantized_matches_hilbert_index_tests!(
4,
[
[-2.0, -1.0, 0.0, 1.0],
[-1.5, 0.25, 1.75, 2.5],
[0.1, -0.7, 2.2, -1.8],
[3.0, 3.0, -2.0, -2.0],
],
(-2.0_f64, 3.0_f64),
8
);
gen_prequantized_matches_hilbert_index_tests!(
5,
[
[-2.0, -1.0, 0.0, 1.0, 2.0],
[-1.5, 0.25, 1.75, 2.5, -0.5],
[0.1, -0.7, 2.2, -1.8, 1.4],
[3.0, 3.0, -2.0, -2.0, 0.5],
],
(-2.0_f64, 3.0_f64),
8
);
#[test]
fn test_hilbert_sorted_indices_and_sort_helpers() {
let coords: Vec<[f64; 2]> =
vec![[0.9, 0.9], [0.1, 0.1], [0.5, 0.5], [0.1, 0.9], [0.9, 0.1]];
let bits = bit_depth(16);
let order = try_hilbert_sorted_indices(&coords, (0.0, 1.0), bits).unwrap();
assert_eq!(order.len(), coords.len());
let mut payload: Vec<usize> = (0..coords.len()).collect();
try_hilbert_sort_by_stable(&mut payload, (0.0_f64, 1.0), bits, |&i| coords[i]).unwrap();
let mut payload2: Vec<usize> = (0..coords.len()).collect();
try_hilbert_sort_by_stable(&mut payload2, (0.0_f64, 1.0), bits, |&i| coords[i]).unwrap();
assert_eq!(payload, order);
assert_eq!(payload, payload2);
}
#[test]
fn test_sort_helpers_accept_stateful_coordinate_closures() {
let coords: Vec<[f64; 2]> = vec![[0.9, 0.9], [0.1, 0.1], [0.5, 0.5]];
let bits = bit_depth(8);
let expected_order = try_hilbert_sorted_indices(&coords, (0.0, 1.0), bits).unwrap();
let mut stable_calls = 0_usize;
let mut stable_payload: Vec<usize> = (0..coords.len()).collect();
try_hilbert_sort_by_stable(&mut stable_payload, (0.0_f64, 1.0), bits, |&i| {
stable_calls += 1;
coords[i]
})
.unwrap();
assert_eq!(stable_payload, expected_order);
assert_eq!(stable_calls, coords.len());
let mut unstable_calls = 0_usize;
let mut unstable_payload: Vec<usize> = (0..coords.len()).collect();
try_hilbert_sort_by_unstable(&mut unstable_payload, (0.0_f64, 1.0), bits, |&i| {
unstable_calls += 1;
coords[i]
})
.unwrap();
assert_eq!(unstable_payload, expected_order);
assert_eq!(unstable_calls, coords.len());
}
#[test]
fn test_unstable_sort_orders_by_key() {
let coords: Vec<[f64; 2]> =
vec![[0.9, 0.9], [0.1, 0.1], [0.5, 0.5], [0.1, 0.9], [0.9, 0.1]];
let bits = bit_depth(16);
let expected_order = try_hilbert_sorted_indices(&coords, (0.0, 1.0), bits).unwrap();
let mut payload: Vec<usize> = (0..coords.len()).collect();
try_hilbert_sort_by_unstable(&mut payload, (0.0_f64, 1.0), bits, |&i| coords[i]).unwrap();
assert_eq!(payload, expected_order);
}
#[test]
fn test_zero_dim_sort_helpers_noop() {
let coords: Vec<[f64; 0]> = vec![[], [], []];
let bits = bit_depth(8);
let order = try_hilbert_sorted_indices(&coords, (0.0, 1.0), bits).unwrap();
assert_eq!(order, vec![0, 1, 2]);
let mut stable_payload = vec![3, 2, 1];
try_hilbert_sort_by_stable(&mut stable_payload, (0.0_f64, 1.0), bits, |_| []).unwrap();
assert_eq!(stable_payload, vec![3, 2, 1]);
let mut unstable_payload = vec![3, 2, 1];
try_hilbert_sort_by_unstable(&mut unstable_payload, (0.0_f64, 1.0), bits, |_| []).unwrap();
assert_eq!(unstable_payload, vec![3, 2, 1]);
}
#[test]
fn test_zero_dim_raw_bound_apis_reject_invalid_bounds() {
let bits = bit_depth(8);
let coords = [0.0_f64; 0];
let coordinate_batch = vec![coords; 3];
let mut payload = vec![3, 2, 1];
assert_matches!(
try_hilbert_quantize(&coords, (f64::NAN, 1.0), bits),
Err(HilbertError::NonFiniteBounds {
lower_bound_finite: false,
upper_bound_finite: true
})
);
assert_matches!(
try_hilbert_index(&coords, (1.0, 1.0), bits),
Err(HilbertError::NonIncreasingBounds {
ordering: CoordinateRangeOrdering::Equal
})
);
assert_matches!(
try_hilbert_sorted_indices(&coordinate_batch, (1.0, 0.0), bits),
Err(HilbertError::NonIncreasingBounds {
ordering: CoordinateRangeOrdering::Decreasing
})
);
assert_matches!(
try_hilbert_sort_by_stable(&mut payload, (f64::NEG_INFINITY, 1.0), bits, |_| coords),
Err(HilbertError::NonFiniteBounds {
lower_bound_finite: false,
upper_bound_finite: true
})
);
}
#[test]
fn test_scaled_quantize_reports_conversion_error() {
let result = quantize_with_scale(
&[1.0_f64],
CoordinateRange::try_new(0.0, 1.0).unwrap(),
bit_depth(31),
u32::MAX,
f64::INFINITY,
);
assert_matches!(
result,
Err(HilbertError::QuantizedCoordinateConversionFailed {
bits: 31,
max_grid_value: u32::MAX,
coordinate_index: 0
})
);
}
macro_rules! gen_in_range_quantization_tests {
($dim:literal, $points:expr, $sample:expr) => {
pastey::paste! {
#[test]
fn [<test_quantize_in_range_matches_tuple_boundary_ $dim d>]() {
let bits = bit_depth(8);
let range = CoordinateRange::try_new(-2.0_f64, 3.0).unwrap();
let coords: [f64; $dim] = $sample;
let parsed = try_hilbert_quantize(&coords, range.bounds(), bits).unwrap();
let prevalidated = hilbert_quantize_in_range(&coords, range, bits).unwrap();
assert_eq!(prevalidated, parsed);
}
#[test]
fn [<test_quantized_batch_carries_prequantized_validation_ $dim d>]() {
let bits = bit_depth(8);
let range = CoordinateRange::try_new(-2.0_f64, 3.0).unwrap();
let points: [[f64; $dim]; 4] = $points;
let quantized: Vec<[u32; $dim]> = points
.iter()
.map(|point| hilbert_quantize_in_range(point, range, bits).unwrap())
.collect();
let batch = HilbertQuantizedBatch::try_new(&quantized, bits).unwrap();
assert_eq!(batch.coordinates(), quantized.as_slice());
assert_eq!(batch.bits(), bits);
let checked = hilbert_indices_prequantized(&quantized, bits).unwrap();
assert_eq!(batch.indices(), checked);
assert_eq!(hilbert_indices_for_quantized_batch(batch), checked);
}
#[test]
fn [<test_quantize_batch_in_range_matches_two_step_path_ $dim d>]() {
let bits = bit_depth(8);
let bounds = CoordinateRange::try_new(-2.0_f64, 3.0).unwrap();
let points: [[f64; $dim]; 4] = $points;
let two_step: Vec<[u32; $dim]> = points
.iter()
.map(|point| hilbert_quantize_in_range(point, bounds, bits).unwrap())
.collect();
let two_step_indices = hilbert_indices_prequantized(&two_step, bits).unwrap();
let batch = hilbert_quantize_batch_in_range(&points, bounds, bits, |point| *point)
.unwrap();
assert_eq!(batch.coordinates(), two_step.as_slice());
assert_eq!(batch.bits(), bits);
assert_eq!(batch.len(), points.len());
assert!(!batch.is_empty());
let (indices, quantized) = batch.into_indices_and_coordinates();
assert_eq!(quantized, two_step);
assert_eq!(indices, two_step_indices);
}
}
};
}
gen_in_range_quantization_tests!(
2,
[[-2.0_f64, -1.0], [-1.5, 0.25], [0.1, -0.7], [3.0, 3.0]],
[0.25_f64, 0.75]
);
gen_in_range_quantization_tests!(
3,
[
[-2.0_f64, -1.0, 0.0],
[-1.5, 0.25, 1.75],
[0.1, -0.7, 2.2],
[3.0, 3.0, -2.0]
],
[0.25_f64, 0.75, -1.0]
);
gen_in_range_quantization_tests!(
4,
[
[-2.0_f64, -1.0, 0.0, 1.0],
[-1.5, 0.25, 1.75, 2.5],
[0.1, -0.7, 2.2, -1.8],
[3.0, 3.0, -2.0, -2.0],
],
[0.25_f64, 0.75, -1.0, 2.5]
);
gen_in_range_quantization_tests!(
5,
[
[-2.0_f64, -1.0, 0.0, 1.0, 2.0],
[-1.5, 0.25, 1.75, 2.5, -0.5],
[0.1, -0.7, 2.2, -1.8, 1.4],
[3.0, 3.0, -2.0, -2.0, 0.5],
],
[0.25_f64, 0.75, -1.0, 2.5, 0.0]
);
#[test]
fn test_quantized_batch_rejects_out_of_range_coordinate() {
let bits = bit_depth(2);
let quantized = [[0_u32, 0], [4, 1]];
let result = HilbertQuantizedBatch::try_new(&quantized, bits);
assert_matches!(
result,
Err(HilbertError::PrequantizedCoordinateOutOfRange {
bits: 2,
max_grid_value: 3,
point_index: 1,
coordinate_index: 0,
coordinate: 4
})
);
}
#[test]
fn test_quantized_batch_rejects_index_overflow() {
let quantized = [[1_u32, 2, 3, 4, 5]];
let result = HilbertQuantizedBatch::try_new(&quantized, bit_depth(26));
assert_matches!(
result,
Err(HilbertError::IndexOverflow {
dimension: 5,
bits: 26,
total_bits: 130
})
);
}
#[test]
fn test_quantized_batch_handles_zero_dimension() {
let bits = bit_depth(8);
let quantized = [[], [], []];
let batch = HilbertQuantizedBatch::try_new(&quantized, bits).unwrap();
assert_eq!(batch.coordinates(), quantized.as_slice());
assert_eq!(batch.bits(), bits);
assert_eq!(batch.indices(), vec![0_u128, 0_u128, 0_u128]);
assert_eq!(
hilbert_indices_for_quantized_batch(batch),
vec![0_u128, 0_u128, 0_u128]
);
}
#[test]
fn test_quantize_batch_in_range_handles_zero_dimension() {
let bits = bit_depth(8);
let bounds = CoordinateRange::try_new(0.0_f64, 1.0).unwrap();
let items = [(), (), ()];
let batch =
hilbert_quantize_batch_in_range(&items, bounds, bits, |()| [0.0_f64; 0]).unwrap();
assert_eq!(batch.len(), 3);
assert!(!batch.is_empty());
assert_eq!(batch.indices(), vec![0_u128, 0_u128, 0_u128]);
assert_eq!(batch.into_coordinates(), vec![[0_u32; 0]; 3]);
}
#[test]
fn test_quantize_in_range_handles_zero_dimension() {
let bits = bit_depth(8);
let bounds = CoordinateRange::try_new(0.0_f64, 1.0).unwrap();
let coords = [0.0_f64; 0];
assert_eq!(
hilbert_quantize_in_range(&coords, bounds, bits).unwrap(),
[0_u32; 0]
);
}
#[test]
fn test_quantize_batch_in_range_rejects_index_overflow() {
let bits = bit_depth(26);
let bounds = CoordinateRange::try_new(0.0_f64, 1.0).unwrap();
let items = [[0.0_f64; 5]];
assert_matches!(
hilbert_quantize_batch_in_range(&items, bounds, bits, |p| *p),
Err(HilbertError::IndexOverflow {
dimension: 5,
bits: 26,
total_bits: 130
})
);
}
#[test]
fn test_quantize_rejects_nonfinite_bounds() {
let result = try_hilbert_quantize(&[0.5_f64], (f64::NAN, 1.0), bit_depth(8));
assert_eq!(
result,
Err(HilbertError::NonFiniteBounds {
lower_bound_finite: false,
upper_bound_finite: true
})
);
let both_non_finite =
try_hilbert_quantize(&[0.5_f64], (f64::NAN, f64::INFINITY), bit_depth(8));
assert_eq!(
both_non_finite,
Err(HilbertError::NonFiniteBounds {
lower_bound_finite: false,
upper_bound_finite: false
})
);
}
#[test]
fn test_quantize_rejects_nonfinite_extent() {
let result = try_hilbert_quantize(&[0.0_f64], (-f64::MAX, f64::MAX), bit_depth(8));
assert_eq!(result, Err(HilbertError::NonFiniteBoundsExtent {}));
}
#[test]
fn test_quantize_rejects_nonfinite_coordinate() {
let result = try_hilbert_quantize(&[0.25_f64, f64::INFINITY], (0.0, 1.0), bit_depth(8));
assert_eq!(
result,
Err(HilbertError::NonFiniteCoordinate {
coordinate_index: 1
})
);
}
#[test]
fn test_quantize_rejects_nonfinite_normalized() {
let result =
try_hilbert_quantize(&[f64::MAX], (-f64::MAX / 2.0, f64::MAX / 2.0), bit_depth(8));
assert_eq!(
result,
Err(HilbertError::NonFiniteNormalizedCoordinate {
coordinate_index: 0
})
);
}
#[test]
fn test_sort_error_keeps_order() {
let coords = [[0.5_f64], [f64::NAN], [0.25]];
let mut payload = vec![0_usize, 1, 2];
let result =
try_hilbert_sort_by_stable(&mut payload, (0.0, 1.0), bit_depth(8), |&i| coords[i]);
assert_eq!(
result,
Err(HilbertError::NonFiniteCoordinate {
coordinate_index: 0
})
);
assert_eq!(payload, vec![0, 1, 2]);
}
#[test]
fn test_apply_order_rejects_length_mismatch_without_reordering() {
let mut payload = vec![10, 20, 30];
let result = apply_order(&mut payload, [0_usize, 1].into_iter());
assert_eq!(payload, vec![10, 20, 30]);
assert_matches!(
result,
Err(HilbertError::InvalidSortPermutationLength {
item_count: 3,
permutation_count: 2
})
);
}
#[test]
fn test_apply_order_rejects_out_of_range_index_without_reordering() {
let mut payload = vec![10, 20, 30];
let result = apply_order(&mut payload, [0_usize, 3, 1].into_iter());
assert_eq!(payload, vec![10, 20, 30]);
assert_matches!(
result,
Err(HilbertError::InvalidSortPermutationIndex {
permutation_index: 1,
item_index: 3,
item_count: 3
})
);
}
#[test]
fn test_apply_order_rejects_duplicate_index_without_reordering() {
let mut payload = vec![10, 20, 30];
let result = apply_order(&mut payload, [0_usize, 1, 1].into_iter());
assert_eq!(payload, vec![10, 20, 30]);
assert_matches!(
result,
Err(HilbertError::InvalidSortPermutationDuplicate {
permutation_index: 2,
item_index: 1
})
);
}
#[test]
fn test_apply_order_rejects_short_iterator_length_lie_without_reordering() {
let mut payload = vec![10, 20, 30];
let result = apply_order(&mut payload, LyingOrder::new(vec![0, 1], 3));
assert_eq!(payload, vec![10, 20, 30]);
assert_matches!(
result,
Err(HilbertError::InvalidSortPermutationLength {
item_count: 3,
permutation_count: 2
})
);
}
#[test]
fn test_apply_order_rejects_long_iterator_length_lie_without_reordering() {
let mut payload = vec![10, 20, 30];
let result = apply_order(&mut payload, LyingOrder::new(vec![0, 1, 2, 0], 3));
assert_eq!(payload, vec![10, 20, 30]);
assert_matches!(
result,
Err(HilbertError::InvalidSortPermutationLength {
item_count: 3,
permutation_count: 4
})
);
}
#[test]
fn test_quantize_clamps_f64_endpoint() {
let q = try_hilbert_quantize(&[1.0], (0.0, 1.0), bit_depth(31)).unwrap();
assert_eq!(q, [(1_u32 << 31) - 1]);
}
#[test]
fn test_hilbert_curve_is_continuous_on_2d_grid() {
let bits: u32 = 4;
let n: u32 = 1_u32 << bits;
let mut points: Vec<([u32; 2], u128)> = Vec::with_capacity((n * n) as usize);
let params = positive_index_params::<2>(bit_depth(bits));
for x in 0..n {
for y in 0..n {
let q = [x, y];
let idx = index_from_quantized(&q, params);
points.push((q, idx));
}
}
points.sort_by_key(|(_, idx)| *idx);
for (i, (_, idx)) in points.iter().enumerate() {
let i_u128 = u128::from(u32::try_from(i).expect("grid size should fit in u32"));
assert_eq!(*idx, i_u128);
}
for window in points.windows(2) {
let a = window[0].0;
let b = window[1].0;
let dx = a[0].abs_diff(b[0]);
let dy = a[1].abs_diff(b[1]);
assert_eq!(dx + dy, 1, "Non-adjacent step: a={a:?}, b={b:?}");
}
}
#[test]
fn test_hilbert_curve_is_continuous_on_4d_grid() {
let bits: u32 = 2;
let n: u32 = 1_u32 << bits;
let mut points: Vec<([u32; 4], u128)> = Vec::with_capacity((n * n * n * n) as usize);
let params = positive_index_params::<4>(bit_depth(bits));
for x in 0..n {
for y in 0..n {
for z in 0..n {
for w in 0..n {
let q = [x, y, z, w];
let idx = index_from_quantized(&q, params);
points.push((q, idx));
}
}
}
}
points.sort_by_key(|(_, idx)| *idx);
for (i, (_, idx)) in points.iter().enumerate() {
let i_u128 = u128::from(u32::try_from(i).expect("grid size should fit in u32"));
assert_eq!(*idx, i_u128);
}
for window in points.windows(2) {
let a = window[0].0;
let b = window[1].0;
let dx = a[0].abs_diff(b[0]);
let dy = a[1].abs_diff(b[1]);
let dz = a[2].abs_diff(b[2]);
let dw = a[3].abs_diff(b[3]);
assert_eq!(dx + dy + dz + dw, 1, "Non-adjacent step: a={a:?}, b={b:?}");
}
}
#[test]
fn test_point_coords_work_with_hilbert() {
let p: Point<2> = Point::try_new([0.25, 0.75]).expect("finite point coordinates");
let idx = try_hilbert_index(p.coords(), (0.0, 1.0), bit_depth(16)).unwrap();
assert!(idx > 0);
}
#[test]
fn test_hilbert_bits_boundaries() {
let coarsest_bits = bit_depth(1);
let origin = try_hilbert_index(&[0.0_f64, 0.0], (0.0, 1.0), coarsest_bits).unwrap();
let corner = try_hilbert_index(&[1.0_f64, 1.0], (0.0, 1.0), coarsest_bits).unwrap();
tracing::debug!(origin, corner, "bits=1 boundaries");
assert_eq!(origin, 0, "bits=1 origin should map to 0");
assert_ne!(origin, corner, "bits=1 should distinguish corners");
let finest_bits = bit_depth(31);
let origin_31 = try_hilbert_index(&[0.0_f64, 0.0], (0.0, 1.0), finest_bits).unwrap();
let corner_31 = try_hilbert_index(&[1.0_f64, 1.0], (0.0, 1.0), finest_bits).unwrap();
tracing::debug!(origin_31, corner_31, "bits=31 boundaries");
assert_eq!(origin_31, 0, "bits=31 origin should map to 0");
assert_ne!(origin_31, corner_31, "bits=31 should distinguish corners");
}
#[test]
fn test_hilbert_index_1d_monotonic() {
let bounds = (0.0_f64, 1.0_f64);
let bits = bit_depth(8);
let a = try_hilbert_index(&[0.0_f64], bounds, bits).unwrap();
let b = try_hilbert_index(&[0.25_f64], bounds, bits).unwrap();
let c = try_hilbert_index(&[0.5_f64], bounds, bits).unwrap();
let d = try_hilbert_index(&[1.0_f64], bounds, bits).unwrap();
tracing::debug!(a, b, c, d, "1d indices");
assert!(
a < b && b < c && c < d,
"1D Hilbert indices should be monotonic"
);
}
#[test]
fn test_hilbert_rejects_degenerate_bounds() {
let bounds = (1.0_f64, 1.0_f64);
let coords = [2.0_f64, -2.0_f64];
let bits = bit_depth(8);
assert_matches!(
try_hilbert_quantize(&coords, bounds, bits),
Err(HilbertError::NonIncreasingBounds {
ordering: CoordinateRangeOrdering::Equal
})
);
assert_matches!(
try_hilbert_index(&coords, bounds, bits),
Err(HilbertError::NonIncreasingBounds {
ordering: CoordinateRangeOrdering::Equal
})
);
}
#[test]
fn test_hilbert_rejects_decreasing_bounds() {
let bounds = (1.0_f64, 0.0_f64);
let coords = [0.5_f64, 0.25_f64];
let bits = bit_depth(8);
assert_matches!(
try_hilbert_quantize(&coords, bounds, bits),
Err(HilbertError::NonIncreasingBounds {
ordering: CoordinateRangeOrdering::Decreasing
})
);
}
#[test]
fn test_hilbert_quantize_clamps_out_of_range() {
let bounds = (0.0_f64, 1.0_f64);
let bits = bit_depth(4);
let coords = [-1.0_f64, 2.0_f64];
let q = try_hilbert_quantize(&coords, bounds, bits).unwrap();
let max_val = (1_u32 << bits.get()) - 1;
tracing::debug!(?q, max_val, "clamp quantize");
assert_eq!(
q,
[0, max_val],
"out-of-range coords should clamp to bounds"
);
let idx = try_hilbert_index(&coords, bounds, bits).unwrap();
let idx_clamped = try_hilbert_index(&[0.0_f64, 1.0_f64], bounds, bits).unwrap();
tracing::debug!(idx, idx_clamped, "clamp index");
assert_eq!(
idx, idx_clamped,
"clamped coords should match clamped index"
);
}
#[test]
fn test_hilbert_indices_prequantized_matches_individual_calls() {
let coords = [
[0.1_f64, 0.2, 0.3],
[0.5, 0.5, 0.5],
[0.9, 0.8, 0.7],
[0.0, 0.0, 0.0],
[1.0, 1.0, 1.0],
];
let bounds = (0.0_f64, 1.0_f64);
let bits = bit_depth(8);
let quantized: Vec<[u32; 3]> = coords
.iter()
.map(|c| try_hilbert_quantize(c, bounds, bits).unwrap())
.collect();
let indices_bulk = hilbert_indices_prequantized(&quantized, bits)
.expect("valid parameters should succeed");
let indices_individual: Vec<u128> = coords
.iter()
.map(|c| try_hilbert_index(c, bounds, bits).unwrap())
.collect();
assert_eq!(indices_bulk.len(), coords.len());
assert_eq!(indices_bulk, indices_individual);
}
#[test]
fn test_hilbert_indices_prequantized_empty_input() {
let empty: Vec<[u32; 2]> = vec![];
let bits = bit_depth(4);
let indices =
hilbert_indices_prequantized(&empty, bits).expect("valid parameters should succeed");
assert_eq!(indices.len(), 0);
}
#[test]
fn test_hilbert_indices_prequantized_validates_overflow() {
let quantized = vec![[1_u32, 2, 3, 4, 5]];
let result = hilbert_indices_prequantized(&quantized, bit_depth(26));
assert_matches!(
result,
Err(HilbertError::IndexOverflow {
dimension: 5,
bits: 26,
total_bits: 130
})
);
}
#[test]
fn test_hilbert_indices_prequantized_validates_coordinate_range() {
let bits = bit_depth(2);
let quantized = vec![[0_u32, 3], [4, 1]];
let result = hilbert_indices_prequantized(&quantized, bits);
assert_matches!(
result,
Err(HilbertError::PrequantizedCoordinateOutOfRange {
bits: 2,
max_grid_value: 3,
point_index: 1,
coordinate_index: 0,
coordinate: 4
})
);
}
#[test]
fn test_hilbert_indices_prequantized_handles_zero_dimension() {
let quantized: Vec<[u32; 0]> = vec![[], [], []];
let bits = bit_depth(8);
let indices = hilbert_indices_prequantized(&quantized, bits).expect("D=0 should succeed");
assert_eq!(indices.len(), 3);
assert_eq!(indices, vec![0_u128, 0_u128, 0_u128]);
}
#[test]
fn test_hilbert_quantize_uses_rounding_not_truncation() {
let bounds = (0.0_f64, 1.0_f64);
let bits = bit_depth(2);
let test_cases = [
(0.0, 0), (0.1, 0), (0.17, 1), (0.3, 1), (0.5, 2), (0.7, 2), (0.85, 3), (1.0, 3), ];
for (coord, expected_cell) in test_cases {
let q = try_hilbert_quantize(&[coord], bounds, bits).unwrap();
assert_eq!(
q[0], expected_cell,
"coordinate {coord} should quantize to cell {expected_cell}, got {}",
q[0]
);
}
let samples = 1000;
let mut cell_counts = [0_usize; 4];
for i in 0..samples {
let coord = f64::from(i) / f64::from(samples);
let q = try_hilbert_quantize(&[coord], bounds, bits).unwrap();
cell_counts[q[0] as usize] += 1;
}
tracing::debug!(?cell_counts, "cell distribution for {samples} samples");
assert!(
cell_counts[0] >= 100 && cell_counts[0] <= 217,
"cell 0 should have ~167 samples with rounding, got {}",
cell_counts[0]
);
assert!(
cell_counts[1] >= 283 && cell_counts[1] <= 383,
"cell 1 should have ~333 samples with rounding, got {}",
cell_counts[1]
);
assert!(
cell_counts[2] >= 283 && cell_counts[2] <= 383,
"cell 2 should have ~333 samples with rounding, got {}",
cell_counts[2]
);
assert!(
cell_counts[3] >= 100 && cell_counts[3] <= 217,
"cell 3 should have ~167 samples with rounding, got {}",
cell_counts[3]
);
}
}