ph_surfaces/axis/
binary.rs1use super::{AxisLookup, KnotArray, assert_valid_knots, probe_bound, sealed};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct BinaryAxis<const N: usize> {
62 knots: &'static [u16; N],
63}
64
65impl<const N: usize> BinaryAxis<N> {
66 #[must_use]
74 pub const fn new(knots: &'static [u16; N]) -> Self {
75 assert_valid_knots(knots);
76
77 Self { knots }
78 }
79
80 #[must_use]
84 pub const fn knots(&self) -> &'static [u16; N] {
85 self.knots
86 }
87}
88
89impl<const N: usize> sealed::Sealed<N> for BinaryAxis<N> {
90 #[inline(always)]
91 fn search_in_domain(&self, coordinate: u16) -> (usize, u32) {
92 debug_assert!(
93 self.knots[0] <= coordinate && coordinate <= self.knots[N - 1],
94 "the sealed search is only called on an in-domain coordinate"
95 );
96
97 let mut base = 0;
98 let mut size = N;
99 let mut probes = 0;
100
101 while size > 1 {
102 let half = size / 2;
103 let mid = base + half;
104
105 if self.knots[mid] <= coordinate {
109 base = mid;
110 }
111
112 size -= half;
113 probes += 1;
114 }
115
116 debug_assert_eq!(
117 probes,
118 <Self as AxisLookup<N>>::MAX_SEARCH_COMPARISONS,
119 "the probe count must match the documented bound exactly"
120 );
121 debug_assert!(
122 self.knots[base] <= coordinate,
123 "the located knot must not sit above the coordinate"
124 );
125
126 (base, probes)
127 }
128}
129
130impl<const N: usize> KnotArray<N> for BinaryAxis<N> {
131 fn knots(&self) -> &'static [u16; N] {
132 self.knots
133 }
134}
135
136impl<const N: usize> AxisLookup<N> for BinaryAxis<N> {
137 const KNOT_BYTES: usize = 2 * N;
138 const INDEX_BYTES: usize = 0;
139 const MAX_SEARCH_COMPARISONS: u32 = probe_bound(N);
140
141 fn first(&self) -> u16 {
142 self.knots[0]
143 }
144
145 fn last(&self) -> u16 {
146 self.knots[N - 1]
147 }
148
149 fn knot(&self, index: usize) -> u16 {
150 self.knots[index]
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::BinaryAxis;
157 use crate::axis::{AxisLookup, KnotArray, probe_bound};
158
159 static X_MAIN: [u16; 5] = [10, 20, 30, 40, 50];
160 static X_SPARSE: [u16; 6] = [3, 4, 5, 1_000, 40_000, 65_000];
161 static X_FULL: [u16; 4] = [0, 1, 32_768, 65_535];
162 static X_TINY: [u16; 2] = [7, 9];
163
164 static X_BIG: [u16; 1024] = {
167 let mut axis = [0u16; 1024];
168 let mut i = 0;
169 let mut knot = 0u16;
170
171 while i < 1024 {
172 axis[i] = knot;
173 i += 1;
174
175 if i < 1024 {
178 knot += 64;
179 }
180 }
181
182 axis
183 };
184
185 const MAIN: BinaryAxis<5> = BinaryAxis::new(&X_MAIN);
186 const SPARSE: BinaryAxis<6> = BinaryAxis::new(&X_SPARSE);
187 const FULL: BinaryAxis<4> = BinaryAxis::new(&X_FULL);
188 const TINY: BinaryAxis<2> = BinaryAxis::new(&X_TINY);
189 const BIG: BinaryAxis<1024> = BinaryAxis::new(&X_BIG);
190
191 fn ceil_log2_reference(len: usize) -> u32 {
195 let mut remaining = len;
196 let mut steps = 0;
197
198 while remaining > 1 {
199 remaining = remaining.div_ceil(2);
200 steps += 1;
201 }
202
203 steps
204 }
205
206 #[test]
207 fn the_probe_bound_matches_the_documented_formula() {
208 for len in [2usize, 3, 4, 5, 6, 7, 8, 9, 16, 17, 1_024, 65_535, 65_536] {
209 assert_eq!(probe_bound(len), ceil_log2_reference(len), "length {len}");
210 }
211
212 assert_eq!(probe_bound(1_024), 10);
213 assert_eq!(<BinaryAxis<1024>>::MAX_SEARCH_COMPARISONS, 10);
214 assert_eq!(<BinaryAxis<5>>::MAX_SEARCH_COMPARISONS, 3);
215 assert_eq!(<BinaryAxis<2>>::MAX_SEARCH_COMPARISONS, 1);
216 }
217
218 #[test]
219 fn the_search_probe_count_is_exact_and_data_independent() {
220 macro_rules! exact_on {
221 ($axis:expr) => {
222 let axis = $axis;
223 let expected = axis.knots().len();
224 let expected = probe_bound(expected);
225
226 for &knot in axis.knots() {
227 assert_eq!(axis.search(knot).1, expected, "at knot {knot}");
228
229 if knot > axis.first() {
230 assert_eq!(axis.search(knot - 1).1, expected);
231 }
232 if knot < axis.last() {
233 assert_eq!(axis.search(knot + 1).1, expected);
234 }
235 }
236 };
237 }
238
239 exact_on!(MAIN);
240 exact_on!(SPARSE);
241 exact_on!(FULL);
242 exact_on!(TINY);
243 exact_on!(BIG);
244
245 let mut coordinate = 0u16;
248 while coordinate < 65_472 {
249 assert_eq!(BIG.search(coordinate).1, 10);
250 coordinate += 397;
251 }
252 }
253
254 #[test]
255 fn binary_lookup_costs_far_fewer_comparisons_than_a_scan() {
256 let (_, probes) = BIG.search(40_000);
257 let scan = u32::try_from(X_BIG.len()).expect("the axis length fits in u32");
258
259 assert_eq!(probes, 10);
260 assert!(
261 probes * 100 < scan,
262 "{probes} probes is not two orders of magnitude below {scan}"
263 );
264 }
265
266 #[test]
267 fn the_located_index_is_the_greatest_knot_at_or_below_the_coordinate() {
268 for (index, &knot) in X_SPARSE.iter().enumerate() {
269 assert_eq!(SPARSE.search(knot).0, index, "at knot {knot}");
270
271 if knot > SPARSE.first() {
272 assert_eq!(SPARSE.search(knot - 1).0, index - 1);
273 }
274 }
275
276 assert_eq!(SPARSE.search(999).0, 2);
277 assert_eq!(SPARSE.search(1_000).0, 3);
278 assert_eq!(SPARSE.search(39_999).0, 3);
279 assert_eq!(FULL.search(65_535).0, 3);
280 assert_eq!(TINY.search(8).0, 0);
281 }
282
283 #[test]
284 fn the_knot_array_is_referenced_and_never_copied() {
285 assert!(core::ptr::eq(MAIN.knots(), &X_MAIN));
286 assert!(core::ptr::eq(KnotArray::knots(&MAIN), &X_MAIN));
287 assert_eq!(<BinaryAxis<5>>::KNOT_BYTES, 10);
288 assert_eq!(<BinaryAxis<5>>::INDEX_BYTES, 0);
289 }
290
291 #[test]
292 #[should_panic(expected = "an axis must declare at least two knots")]
293 fn a_one_knot_axis_is_rejected() {
294 static ONE: [u16; 1] = [3];
295
296 let _ = BinaryAxis::new(&ONE);
297 }
298
299 #[test]
300 #[should_panic(expected = "axis knots must be strictly increasing")]
301 fn a_duplicated_knot_is_rejected() {
302 static DUPLICATE: [u16; 2] = [5, 5];
303
304 let _ = BinaryAxis::new(&DUPLICATE);
305 }
306
307 #[test]
308 #[should_panic(expected = "axis knots must be strictly increasing")]
309 fn a_descending_knot_is_rejected() {
310 static DESCENDING: [u16; 3] = [0, 100, 50];
311
312 let _ = BinaryAxis::new(&DESCENDING);
313 }
314}