hurray_core/layout/hilbert.rs
1//! Hilbert curve layout descriptor.
2//!
3//! Tag `0x40`. Tier 2. Carries curve order and rank.
4//! See `docs/spec/layouts/hilbert.md`.
5
6use crate::{Error, Result};
7
8/// Descriptor for the Hilbert space-filling curve layout.
9///
10/// Elements are ordered according to a Hilbert curve, which provides better
11/// spatial locality than Morton (Z-order): consecutive Hilbert indices always
12/// differ by exactly 1 in exactly one coordinate (L∞ distance = 1).
13///
14/// Constraints (enforced by [`HilbertLayout::new`]):
15/// - `hilbert_order > 0`
16/// - `hilbert_rank >= 2`
17/// - Every tensor dimension must equal `2^hilbert_order`
18/// (validated at shape-binding time via
19/// [`LayoutDescriptor::validate_against_shape`])
20///
21/// The normative index mapping is the Skilling (2004) algorithm; see
22/// `docs/spec/layouts/hilbert.md § Normative Index Mapping`.
23///
24/// # Examples
25///
26/// ```
27/// use hurray_core::layout::{LayoutDescriptor, HilbertLayout};
28///
29/// // 2-D Hilbert curve, order 2 (4×4 tensor).
30/// let layout = LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap());
31/// assert_eq!(layout.tag(), 0x40);
32/// ```
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub struct HilbertLayout {
36 /// Order of the Hilbert curve. MUST be > 0.
37 /// Each tensor dimension must equal `2^hilbert_order`.
38 pub hilbert_order: u32,
39
40 /// Number of curve dimensions. MUST equal the tensor rank. MUST be >= 2.
41 pub hilbert_rank: u32,
42}
43
44impl HilbertLayout {
45 /// Creates a new [`HilbertLayout`], validating that `hilbert_order > 0`
46 /// and `hilbert_rank >= 2`.
47 ///
48 /// Shape consistency (`shape[k] == 2^hilbert_order` for all `k`) is
49 /// deferred to [`LayoutDescriptor::validate_against_shape`] because
50 /// the layout descriptor does not carry the shape.
51 ///
52 /// # Errors
53 ///
54 /// Returns [`Error::InvalidLayout`] if `hilbert_order == 0` or
55 /// `hilbert_rank < 2`.
56 ///
57 /// # Examples
58 ///
59 /// ```
60 /// use hurray_core::layout::HilbertLayout;
61 ///
62 /// let h = HilbertLayout::new(3, 2).unwrap(); // 8×8 tensor
63 /// assert_eq!(h.hilbert_order, 3);
64 /// assert_eq!(h.hilbert_rank, 2);
65 ///
66 /// assert!(HilbertLayout::new(0, 2).is_err()); // order must be > 0
67 /// assert!(HilbertLayout::new(2, 1).is_err()); // rank must be >= 2
68 /// ```
69 pub fn new(hilbert_order: u32, hilbert_rank: u32) -> Result<Self> {
70 if hilbert_order == 0 {
71 return Err(Error::InvalidLayout(
72 "hilbert_order must be > 0".to_string(),
73 ));
74 }
75 if hilbert_rank < 2 {
76 return Err(Error::InvalidLayout(format!(
77 "hilbert_rank must be >= 2, got {hilbert_rank}"
78 )));
79 }
80 Ok(Self {
81 hilbert_order,
82 hilbert_rank,
83 })
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::layout::LayoutDescriptor;
91
92 #[test]
93 fn hilbert_tag_is_0x40() {
94 let layout = LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap());
95 assert_eq!(layout.tag(), 0x40);
96 }
97
98 #[test]
99 fn hilbert_buffer_count_is_1() {
100 use std::num::NonZeroU8;
101 let layout = LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap());
102 assert_eq!(layout.buffer_count(), Some(NonZeroU8::new(1).unwrap()));
103 }
104
105 #[test]
106 fn rejects_zero_order() {
107 assert!(matches!(
108 HilbertLayout::new(0, 2),
109 Err(Error::InvalidLayout(_))
110 ));
111 }
112
113 #[test]
114 fn rejects_rank_one() {
115 assert!(matches!(
116 HilbertLayout::new(2, 1),
117 Err(Error::InvalidLayout(_))
118 ));
119 }
120
121 #[test]
122 fn rejects_rank_zero() {
123 assert!(matches!(
124 HilbertLayout::new(2, 0),
125 Err(Error::InvalidLayout(_))
126 ));
127 }
128}