Skip to main content

nodedb_array/coord/
hilbert.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ND Hilbert curve encode/decode (Skilling 2004 transposed-form).
4//!
5//! Operates on `n` per-dim integer coordinates of `bits` bits each,
6//! interleaving the result into a single Hilbert index. Caller-side
7//! [`super::normalize`] maps typed coords into this integer space.
8
9use super::normalize::MAX_DIMS;
10use crate::error::{ArrayError, ArrayResult};
11
12/// Encode per-dim integer coordinates into a single Hilbert index.
13///
14/// `coords.len()` must be `<=` [`MAX_DIMS`]; `bits * coords.len()` must
15/// be `<= 64`.
16pub fn encode(coords: &[u64], bits: u32) -> ArrayResult<u64> {
17    check_shape(coords.len(), bits)?;
18    let n = coords.len();
19    if n == 0 {
20        return Ok(0);
21    }
22    let mut x: [u64; MAX_DIMS] = [0; MAX_DIMS];
23    x[..n].copy_from_slice(coords);
24
25    // Skilling: axes → transposed Hilbert.
26    let m: u64 = 1u64 << (bits - 1);
27    let mut q = m;
28    while q > 0 {
29        let p = q - 1;
30        for i in 0..n {
31            if x[i] & q != 0 {
32                x[0] ^= p;
33            } else {
34                let t = (x[0] ^ x[i]) & p;
35                x[0] ^= t;
36                x[i] ^= t;
37            }
38        }
39        q >>= 1;
40    }
41    // Gray encode.
42    for i in 1..n {
43        x[i] ^= x[i - 1];
44    }
45    let mut t: u64 = 0;
46    let mut q = m;
47    while q > 1 {
48        if x[n - 1] & q != 0 {
49            t ^= q - 1;
50        }
51        q >>= 1;
52    }
53    for xi in x.iter_mut().take(n) {
54        *xi ^= t;
55    }
56
57    // Interleave transposed Hilbert into single index, MSB first.
58    let mut idx: u64 = 0;
59    for b in (0..bits).rev() {
60        for xi in x.iter().take(n) {
61            let bit = (*xi >> b) & 1;
62            idx = (idx << 1) | bit;
63        }
64    }
65    Ok(idx)
66}
67
68/// Inverse of [`encode`]. Recovers per-dim integer coordinates from a
69/// Hilbert index.
70pub fn decode(idx: u64, n: usize, bits: u32) -> ArrayResult<Vec<u64>> {
71    check_shape(n, bits)?;
72    if n == 0 {
73        return Ok(Vec::new());
74    }
75    // De-interleave bits.
76    let mut x: [u64; MAX_DIMS] = [0; MAX_DIMS];
77    for b in (0..bits).rev() {
78        for (i, slot) in x.iter_mut().enumerate().take(n) {
79            let shift = b * n as u32 + (n as u32 - 1 - i as u32);
80            let bit = (idx >> shift) & 1;
81            *slot = (*slot << 1) | bit;
82        }
83    }
84    // Skilling TransposetoAxes: Gray decode (single-shot), then undo.
85    let t = x[n - 1] >> 1;
86    for i in (1..n).rev() {
87        x[i] ^= x[i - 1];
88    }
89    x[0] ^= t;
90    // Undo excess work: Q = 2 .. 2^bits.
91    let n_lim: u128 = 1u128 << bits;
92    let mut q: u128 = 2;
93    while q != n_lim {
94        let p = (q - 1) as u64;
95        let qb = q as u64;
96        for i in (0..n).rev() {
97            if x[i] & qb != 0 {
98                x[0] ^= p;
99            } else {
100                let t = (x[0] ^ x[i]) & p;
101                x[0] ^= t;
102                x[i] ^= t;
103            }
104        }
105        q <<= 1;
106    }
107    Ok(x[..n].to_vec())
108}
109
110fn check_shape(n: usize, bits: u32) -> ArrayResult<()> {
111    if n > MAX_DIMS {
112        return Err(ArrayError::InvalidSchema {
113            array: String::new(),
114            detail: format!("hilbert: arity {n} exceeds MAX_DIMS={MAX_DIMS}"),
115        });
116    }
117    if bits == 0 || (n as u32) * bits > 64 {
118        return Err(ArrayError::InvalidSchema {
119            array: String::new(),
120            detail: format!("hilbert: {n} dims × {bits} bits exceeds 64-bit prefix"),
121        });
122    }
123    Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn hilbert_round_trip_2d_4bit() {
132        for x in 0..16u64 {
133            for y in 0..16u64 {
134                let idx = encode(&[x, y], 4).unwrap();
135                let back = decode(idx, 2, 4).unwrap();
136                assert_eq!(back, vec![x, y], "mismatch at ({x},{y}) idx={idx}");
137            }
138        }
139    }
140
141    #[test]
142    fn hilbert_round_trip_3d_4bit() {
143        for x in 0..16u64 {
144            for y in 0..16u64 {
145                for z in 0..16u64 {
146                    let idx = encode(&[x, y, z], 4).unwrap();
147                    let back = decode(idx, 3, 4).unwrap();
148                    assert_eq!(back, vec![x, y, z]);
149                }
150            }
151        }
152    }
153
154    #[test]
155    fn hilbert_indices_are_unique_2d() {
156        use std::collections::HashSet;
157        let mut seen = HashSet::new();
158        for x in 0..8u64 {
159            for y in 0..8u64 {
160                assert!(seen.insert(encode(&[x, y], 3).unwrap()));
161            }
162        }
163        assert_eq!(seen.len(), 64);
164    }
165
166    #[test]
167    fn hilbert_index_adjacent_cells_are_spatial_neighbors() {
168        // The Hilbert guarantee: cells at consecutive Hilbert indices
169        // differ by at most 1 in every dimension (Chebyshev distance =
170        // 1). The reverse — "spatially-adjacent cells have small index
171        // gap" — is NOT a property of any space-filling curve.
172        let mut by_idx = vec![[0u64; 2]; 64];
173        for x in 0..8u64 {
174            for y in 0..8u64 {
175                let i = encode(&[x, y], 3).unwrap() as usize;
176                by_idx[i] = [x, y];
177            }
178        }
179        for w in by_idx.windows(2) {
180            let dx = (w[0][0] as i64 - w[1][0] as i64).abs();
181            let dy = (w[0][1] as i64 - w[1][1] as i64).abs();
182            assert!(
183                dx.max(dy) == 1,
184                "index-adjacent cells {:?}, {:?} not spatial neighbors",
185                w[0],
186                w[1]
187            );
188        }
189    }
190
191    #[test]
192    fn hilbert_rejects_too_many_dims() {
193        let coords = vec![0u64; MAX_DIMS + 1];
194        assert!(encode(&coords, 1).is_err());
195    }
196
197    #[test]
198    fn hilbert_rejects_overflowing_prefix() {
199        // 8 dims × 9 bits = 72 > 64.
200        let coords = vec![0u64; 8];
201        assert!(encode(&coords, 9).is_err());
202    }
203}