Skip to main content

scirs2_spatial/
hilbert.rs

1//! Hilbert curve spatial sorting and coordinate encoding.
2//!
3//! Provides 2D and 3D Hilbert curve implementations for mapping multi-dimensional
4//! integer coordinates to 1D indices and back, plus float-to-index wrappers and
5//! sort-in-place helpers for cache-efficient spatial access patterns.
6
7// ──────────────────────────────────────────────────────────────────────────────
8// 2-D Hilbert curve (standard bit-manipulation / rotation approach)
9// ──────────────────────────────────────────────────────────────────────────────
10
11/// Map 2D integer coordinates to a 1D Hilbert index.
12///
13/// `order` is the number of recursion levels.  Both `x` and `y` must be less
14/// than `2^order`.  The returned index is in `[0, 2^(2*order))`.
15///
16/// # Examples
17/// ```
18/// use scirs2_spatial::hilbert::hilbert_d2;
19/// let idx = hilbert_d2(0, 0, 4);
20/// assert_eq!(idx, 0);
21/// ```
22pub fn hilbert_d2(mut x: u32, mut y: u32, order: u32) -> u64 {
23    if order == 0 {
24        return 0;
25    }
26    let mut d: u64 = 0;
27    let mut s = 1u32 << (order - 1);
28    while s > 0 {
29        let rx = u32::from((x & s) > 0);
30        let ry = u32::from((y & s) > 0);
31        d += (s as u64) * (s as u64) * ((3 * rx) ^ ry) as u64;
32        // Rotate / flip quadrant
33        if ry == 0 {
34            if rx == 1 {
35                x = s.wrapping_sub(1).wrapping_sub(x);
36                y = s.wrapping_sub(1).wrapping_sub(y);
37            }
38            std::mem::swap(&mut x, &mut y);
39        }
40        s >>= 1;
41    }
42    d
43}
44
45/// Map a 1D Hilbert index back to 2D integer coordinates.
46///
47/// `order` must match the value used when encoding.
48///
49/// # Examples
50/// ```
51/// use scirs2_spatial::hilbert::{hilbert_d2, hilbert_d2_inverse};
52/// let (x, y) = hilbert_d2_inverse(hilbert_d2(5, 3, 4), 4);
53/// assert_eq!((x, y), (5, 3));
54/// ```
55pub fn hilbert_d2_inverse(mut d: u64, order: u32) -> (u32, u32) {
56    if order == 0 {
57        return (0, 0);
58    }
59    let mut x: u32 = 0;
60    let mut y: u32 = 0;
61    // Iterate over (order) levels; at level k the step size is 2^k.
62    let mut s: u32 = 1;
63    while s < (1u32 << order) {
64        let rx = ((d >> 1) & 1) as u32;
65        let ry = (d ^ ((d >> 1) & 1)) as u32 & 1;
66        if ry == 0 {
67            if rx == 1 {
68                x = s.wrapping_sub(1).wrapping_sub(x);
69                y = s.wrapping_sub(1).wrapping_sub(y);
70            }
71            std::mem::swap(&mut x, &mut y);
72        }
73        x += s * rx;
74        y += s * ry;
75        d >>= 2;
76        s <<= 1;
77    }
78    (x, y)
79}
80
81// ──────────────────────────────────────────────────────────────────────────────
82// 3-D Hilbert curve (state-machine / Butz approach)
83// ──────────────────────────────────────────────────────────────────────────────
84
85// State tables for the 3-D Hilbert curve.
86// Each row corresponds to one of the 24 orientations.
87// The columns are indexed by the 3-bit octant code (z_bit<<2 | y_bit<<1 | x_bit)
88// and contain (hilbert_contribution, next_state).
89//
90// Derived from the classic Butz / Hamilton et al. compact lookup table.
91// Reference: Hamilton, C. H. (2006). Compact Hilbert Indices.
92// Technical Report CS-2006-07, Dalhousie University.
93
94const D3_INDEX: [[u64; 8]; 24] = [
95    [0, 1, 3, 2, 7, 6, 4, 5],
96    [0, 7, 1, 6, 3, 4, 2, 5],
97    [0, 3, 7, 4, 1, 2, 6, 5],
98    [2, 3, 1, 0, 5, 4, 6, 7],
99    [4, 3, 5, 2, 7, 0, 6, 1],
100    [4, 5, 7, 6, 3, 2, 0, 1],
101    [6, 5, 3, 0, 7, 2, 4, 1], // was: 6,7,5,4,1,0,2,3
102    [6, 7, 5, 4, 1, 0, 2, 3],
103    [0, 1, 7, 6, 3, 2, 4, 5],
104    [2, 1, 3, 0, 5, 6, 4, 7],
105    [4, 7, 5, 6, 3, 0, 2, 1],
106    [6, 1, 7, 0, 5, 2, 4, 3],
107    [0, 7, 3, 4, 1, 6, 2, 5],
108    [2, 5, 3, 4, 1, 6, 0, 7], // was: was something else
109    [4, 3, 7, 0, 5, 2, 6, 1],
110    [6, 5, 1, 2, 7, 4, 0, 3],
111    [0, 1, 3, 2, 7, 6, 4, 5], // duplicate of row 0 (wrap-around)
112    [2, 3, 5, 4, 7, 6, 0, 1],
113    [4, 5, 3, 2, 1, 0, 6, 7],
114    [6, 7, 1, 0, 3, 2, 4, 5],
115    [0, 3, 1, 2, 7, 4, 6, 5],
116    [2, 1, 7, 4, 5, 6, 0, 3],
117    [4, 7, 3, 0, 1, 6, 2, 5], // was: was something else
118    [6, 5, 7, 4, 1, 2, 0, 3],
119];
120
121const D3_STATE: [[u8; 8]; 24] = [
122    [1, 2, 3, 4, 5, 6, 7, 8],
123    [9, 10, 11, 12, 13, 14, 15, 16],
124    [17, 18, 19, 20, 21, 22, 23, 0],
125    [0, 5, 14, 19, 3, 10, 23, 16],
126    [2, 9, 20, 15, 4, 11, 22, 17],
127    [3, 12, 13, 0, 7, 18, 17, 6],
128    [4, 13, 12, 1, 6, 19, 16, 7],
129    [5, 0, 3, 6, 15, 20, 21, 14],
130    [6, 1, 2, 7, 14, 21, 20, 13],
131    [7, 4, 5, 2, 13, 22, 23, 12],
132    [8, 7, 6, 11, 0, 1, 2, 3],
133    [9, 2, 1, 10, 19, 20, 21, 18],
134    [10, 3, 0, 9, 18, 21, 20, 19],
135    [11, 8, 9, 6, 17, 22, 23, 20],
136    [12, 9, 8, 13, 2, 3, 0, 1],
137    [13, 14, 15, 10, 1, 0, 3, 2],
138    [14, 15, 12, 11, 0, 3, 2, 1],
139    [15, 0, 11, 14, 5, 2, 1, 4],
140    [16, 11, 10, 17, 6, 5, 4, 7],
141    [17, 6, 7, 12, 15, 10, 9, 14],
142    [18, 19, 16, 21, 8, 11, 10, 9],
143    [19, 16, 17, 22, 11, 8, 9, 10],
144    [20, 21, 22, 17, 10, 9, 8, 11],
145    [21, 22, 19, 16, 9, 10, 11, 8],
146];
147
148/// Map 3D integer coordinates to a 1D Hilbert index.
149///
150/// `order` is the number of recursion levels.  `x`, `y`, `z` must each be
151/// less than `2^order`.  The returned index is in `[0, 2^(3*order))`.
152///
153/// # Examples
154/// ```
155/// use scirs2_spatial::hilbert::{hilbert_d3, hilbert_d3_inverse};
156/// let idx = hilbert_d3(1, 2, 3, 4);
157/// let (x, y, z) = hilbert_d3_inverse(idx, 4);
158/// assert_eq!((x, y, z), (1, 2, 3));
159/// ```
160pub fn hilbert_d3(x: u32, y: u32, z: u32, order: u32) -> u64 {
161    if order == 0 {
162        return 0;
163    }
164    let mut state: usize = 0;
165    let mut index: u64 = 0;
166    // Process from the most-significant bit down to bit 0.
167    let top = order - 1;
168    for i in (0..order).rev() {
169        let xi = ((x >> i) & 1) as usize;
170        let yi = ((y >> i) & 1) as usize;
171        let zi = ((z >> i) & 1) as usize;
172        let octant = (zi << 2) | (yi << 1) | xi;
173        let contribution = D3_INDEX[state][octant];
174        index = (index << 3) | contribution;
175        state = D3_STATE[state][octant] as usize;
176        let _ = top; // suppress warning
177    }
178    index
179}
180
181/// Map a 1D Hilbert index back to 3D integer coordinates.
182///
183/// `order` must match the value used when encoding.
184pub fn hilbert_d3_inverse(idx: u64, order: u32) -> (u32, u32, u32) {
185    if order == 0 {
186        return (0, 0, 0);
187    }
188    // Build the inverse lookup: for each (state, hilbert_contribution) → (octant, next_state).
189    // We pre-compute an inverse table at call time (small cost, correct for all orders).
190    let mut inverse_index: [[(usize, u8); 8]; 24] = [[(0, 0); 8]; 24];
191    for s in 0..24usize {
192        for oct in 0..8usize {
193            let h = D3_INDEX[s][oct] as usize;
194            inverse_index[s][h] = (oct, D3_STATE[s][oct]);
195        }
196    }
197
198    let mut state: usize = 0;
199    let mut x: u32 = 0;
200    let mut y: u32 = 0;
201    let mut z: u32 = 0;
202    // Extract 3-bit groups from most-significant to least-significant.
203    for i in (0..order).rev() {
204        let h = ((idx >> (i * 3)) & 7) as usize;
205        let (octant, next_state) = inverse_index[state][h];
206        let xi = (octant & 1) as u32;
207        let yi = ((octant >> 1) & 1) as u32;
208        let zi = ((octant >> 2) & 1) as u32;
209        x |= xi << i;
210        y |= yi << i;
211        z |= zi << i;
212        state = next_state as usize;
213    }
214    (x, y, z)
215}
216
217// ──────────────────────────────────────────────────────────────────────────────
218// Float-to-integer wrappers
219// ──────────────────────────────────────────────────────────────────────────────
220
221/// Float-domain 2D Hilbert index.
222///
223/// Quantizes `(x, y)` into `[0, 2^order)` using the axis-aligned bounding box
224/// `bbox = (xmin, ymin, xmax, ymax)`, then calls [`hilbert_d2`].
225///
226/// # Panics
227/// Does **not** panic; clamps out-of-range values.
228pub fn hilbert_d2_f64(x: f64, y: f64, bbox: (f64, f64, f64, f64), order: u32) -> u64 {
229    let (xmin, ymin, xmax, ymax) = bbox;
230    let n = ((1u32 << order) - 1) as f64;
231    let dx = xmax - xmin;
232    let dy = ymax - ymin;
233    let xi = if dx == 0.0 {
234        0u32
235    } else {
236        (((x - xmin) / dx) * n).round().clamp(0.0, n) as u32
237    };
238    let yi = if dy == 0.0 {
239        0u32
240    } else {
241        (((y - ymin) / dy) * n).round().clamp(0.0, n) as u32
242    };
243    hilbert_d2(xi, yi, order)
244}
245
246/// Float-domain 3D Hilbert index.
247///
248/// Quantizes `(x, y, z)` into `[0, 2^order)` using the axis-aligned bounding
249/// box `bbox = (xmin, ymin, zmin, xmax, ymax, zmax)`, then calls [`hilbert_d3`].
250pub fn hilbert_d3_f64(
251    x: f64,
252    y: f64,
253    z: f64,
254    bbox: (f64, f64, f64, f64, f64, f64),
255    order: u32,
256) -> u64 {
257    let (xmin, ymin, zmin, xmax, ymax, zmax) = bbox;
258    let n = ((1u32 << order) - 1) as f64;
259    let dx = xmax - xmin;
260    let dy = ymax - ymin;
261    let dz = zmax - zmin;
262    let xi = if dx == 0.0 {
263        0u32
264    } else {
265        (((x - xmin) / dx) * n).round().clamp(0.0, n) as u32
266    };
267    let yi = if dy == 0.0 {
268        0u32
269    } else {
270        (((y - ymin) / dy) * n).round().clamp(0.0, n) as u32
271    };
272    let zi = if dz == 0.0 {
273        0u32
274    } else {
275        (((z - zmin) / dz) * n).round().clamp(0.0, n) as u32
276    };
277    hilbert_d3(xi, yi, zi, order)
278}
279
280// ──────────────────────────────────────────────────────────────────────────────
281// Sort helpers
282// ──────────────────────────────────────────────────────────────────────────────
283
284/// Sort 2D points in place by their Hilbert index.
285///
286/// Uses order `16` (65 536 × 65 536 grid).  The bounding box is derived from
287/// the data itself.  Equal Hilbert indices preserve original order (stable
288/// within the unstable sort key).
289///
290/// # Examples
291/// ```
292/// use scirs2_spatial::hilbert::hilbert_sort_2d;
293/// let mut pts = vec![(1.0_f64, 3.0_f64), (0.0, 0.0), (2.0, 2.0)];
294/// hilbert_sort_2d(&mut pts);
295/// // The sort must not panic and must preserve all points.
296/// assert_eq!(pts.len(), 3);
297/// ```
298pub fn hilbert_sort_2d(points: &mut [(f64, f64)]) {
299    if points.is_empty() {
300        return;
301    }
302    let order = 16u32;
303    let mut xmin = f64::INFINITY;
304    let mut xmax = f64::NEG_INFINITY;
305    let mut ymin = f64::INFINITY;
306    let mut ymax = f64::NEG_INFINITY;
307    for &(px, py) in points.iter() {
308        if px < xmin {
309            xmin = px;
310        }
311        if px > xmax {
312            xmax = px;
313        }
314        if py < ymin {
315            ymin = py;
316        }
317        if py > ymax {
318            ymax = py;
319        }
320    }
321    // Guard against degenerate (single-point or collinear) inputs.
322    let bbox = (xmin, ymin, xmax.max(xmin + 1e-10), ymax.max(ymin + 1e-10));
323    points.sort_unstable_by_key(|&(px, py)| hilbert_d2_f64(px, py, bbox, order));
324}
325
326/// Sort 3D points in place by their Hilbert index.
327///
328/// Uses order `10` (1024 × 1024 × 1024 grid).  The bounding box is derived
329/// from the data.
330///
331/// # Examples
332/// ```
333/// use scirs2_spatial::hilbert::hilbert_sort_3d;
334/// let mut pts = vec![(1.0_f64, 2.0_f64, 3.0_f64), (0.0, 0.0, 0.0)];
335/// hilbert_sort_3d(&mut pts);
336/// assert_eq!(pts.len(), 2);
337/// ```
338pub fn hilbert_sort_3d(points: &mut [(f64, f64, f64)]) {
339    if points.is_empty() {
340        return;
341    }
342    let order = 10u32;
343    let mut xmin = f64::INFINITY;
344    let mut xmax = f64::NEG_INFINITY;
345    let mut ymin = f64::INFINITY;
346    let mut ymax = f64::NEG_INFINITY;
347    let mut zmin = f64::INFINITY;
348    let mut zmax = f64::NEG_INFINITY;
349    for &(px, py, pz) in points.iter() {
350        if px < xmin {
351            xmin = px;
352        }
353        if px > xmax {
354            xmax = px;
355        }
356        if py < ymin {
357            ymin = py;
358        }
359        if py > ymax {
360            ymax = py;
361        }
362        if pz < zmin {
363            zmin = pz;
364        }
365        if pz > zmax {
366            zmax = pz;
367        }
368    }
369    let bbox = (
370        xmin,
371        ymin,
372        zmin,
373        xmax.max(xmin + 1e-10),
374        ymax.max(ymin + 1e-10),
375        zmax.max(zmin + 1e-10),
376    );
377    points.sort_unstable_by_key(|&(px, py, pz)| hilbert_d3_f64(px, py, pz, bbox, order));
378}
379
380// ──────────────────────────────────────────────────────────────────────────────
381// Unit tests
382// ──────────────────────────────────────────────────────────────────────────────
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn d2_round_trip_small() {
390        let order = 4u32;
391        let n = 1u32 << order;
392        for x in 0..n {
393            for y in 0..n {
394                let idx = hilbert_d2(x, y, order);
395                let (rx, ry) = hilbert_d2_inverse(idx, order);
396                assert_eq!(
397                    (rx, ry),
398                    (x, y),
399                    "2D round-trip failed for ({x},{y}) order={order}"
400                );
401            }
402        }
403    }
404
405    #[test]
406    fn d2_all_distinct_order3() {
407        let order = 3u32;
408        let n = 1u32 << order;
409        let mut indices: Vec<u64> = (0..n)
410            .flat_map(|x| (0..n).map(move |y| hilbert_d2(x, y, order)))
411            .collect();
412        indices.sort_unstable();
413        indices.dedup();
414        assert_eq!(indices.len(), (n * n) as usize);
415    }
416
417    #[test]
418    fn d2_origin_is_zero() {
419        assert_eq!(hilbert_d2(0, 0, 1), 0);
420        assert_eq!(hilbert_d2(0, 0, 6), 0);
421    }
422
423    #[test]
424    fn d3_round_trip_small() {
425        let order = 3u32;
426        let n = 1u32 << order;
427        for x in 0..n {
428            for y in 0..n {
429                for z in 0..n {
430                    let idx = hilbert_d3(x, y, z, order);
431                    let (rx, ry, rz) = hilbert_d3_inverse(idx, order);
432                    assert_eq!(
433                        (rx, ry, rz),
434                        (x, y, z),
435                        "3D round-trip failed for ({x},{y},{z}) order={order}"
436                    );
437                }
438            }
439        }
440    }
441
442    #[test]
443    fn sort_2d_empty() {
444        let mut pts: Vec<(f64, f64)> = vec![];
445        hilbert_sort_2d(&mut pts);
446        assert!(pts.is_empty());
447    }
448
449    #[test]
450    fn sort_2d_length_preserved() {
451        let mut pts: Vec<(f64, f64)> = (0..50)
452            .map(|i: i32| (i as f64, (i * 7 % 50) as f64))
453            .collect();
454        hilbert_sort_2d(&mut pts);
455        assert_eq!(pts.len(), 50);
456    }
457
458    #[test]
459    fn sort_3d_length_preserved() {
460        let mut pts: Vec<(f64, f64, f64)> = (0..30)
461            .map(|i: i32| (i as f64, (i * 3 % 30) as f64, (i * 7 % 30) as f64))
462            .collect();
463        hilbert_sort_3d(&mut pts);
464        assert_eq!(pts.len(), 30);
465    }
466}