1mod binary;
42mod bucketed;
43mod linear;
44mod uniform;
45
46pub use binary::BinaryAxis;
47pub use bucketed::{BucketedAxis, bucket_index, max_local_comparisons};
48pub use linear::LinearAxis;
49pub use uniform::UniformAxis;
50
51mod sealed {
59 pub trait Sealed<const N: usize> {
61 fn search_in_domain(&self, coordinate: u16) -> (usize, u32);
64 }
65}
66
67pub trait AxisLookup<const N: usize>: sealed::Sealed<N> + Copy {
86 const KNOT_BYTES: usize;
93
94 const INDEX_BYTES: usize;
99
100 const MAX_SEARCH_COMPARISONS: u32;
110
111 fn first(&self) -> u16;
113
114 fn last(&self) -> u16;
116
117 fn knot(&self, index: usize) -> u16;
125
126 fn search(&self, coordinate: u16) -> (usize, u32) {
150 crate::lookup::search(self, coordinate)
151 }
152}
153
154#[inline(always)]
156pub(crate) fn search_in_domain<const N: usize, A: AxisLookup<N>>(
157 axis: &A,
158 coordinate: u16,
159) -> (usize, u32) {
160 <A as sealed::Sealed<N>>::search_in_domain(axis, coordinate)
161}
162
163pub trait KnotArray<const N: usize>: AxisLookup<N> {
173 fn knots(&self) -> &'static [u16; N];
175}
176
177pub(crate) const fn probe_bound(len: usize) -> u32 {
191 debug_assert!(len >= 2, "an axis declares at least two knots");
192
193 usize::BITS - (len - 1).leading_zeros()
194}
195
196const fn assert_valid_knots<const N: usize>(knots: &[u16; N]) {
204 assert!(N >= 2, "an axis must declare at least two knots");
205
206 let mut i = 1;
207 while i < N {
208 assert!(
209 knots[i - 1] < knots[i],
210 "axis knots must be strictly increasing"
211 );
212 i += 1;
213 }
214}
215
216#[cfg(test)]
223pub(crate) fn probes<const N: usize>(
224 knots: &'static [u16; N],
225 stride: usize,
226) -> impl Iterator<Item = u16> {
227 knots
228 .iter()
229 .flat_map(|&knot| [knot.saturating_sub(1), knot, knot.saturating_add(1)])
230 .chain((knots[0]..=knots[N - 1]).step_by(stride))
231 .filter(|&coordinate| coordinate >= knots[0] && coordinate <= knots[N - 1])
232}
233
234#[cfg(test)]
235mod tests {
236 use super::{
237 AxisLookup, BinaryAxis, BucketedAxis, LinearAxis, UniformAxis, bucket_index, probes,
238 };
239 use crate::boundary::{Boundary, BoundaryPolicy};
240 use crate::error::SurfaceError;
241 use crate::surface::BilinearSurface;
242 use core::mem::size_of;
243
244 static UNIFORM_KNOTS: [u16; 9] = [100, 150, 200, 250, 300, 350, 400, 450, 500];
247 static UNIFORM_BUCKETS: [u16; 4] = bucket_index(&UNIFORM_KNOTS);
248
249 static Y_KNOTS: [u16; 3] = [10, 20, 30];
252 static Y_BUCKETS: [u16; 2] = bucket_index(&Y_KNOTS);
253
254 static VALUES: [[i32; 9]; 3] = [
257 [0, 1, 4, 9, 16, 25, 36, 49, 64],
258 [100, 102, 108, 118, 132, 150, 172, 198, 228],
259 [-50, -49, -46, -41, -34, -25, -14, -1, 14],
260 ];
261
262 const LINEAR_X: LinearAxis<9> = LinearAxis::new(&UNIFORM_KNOTS);
263 const BINARY_X: BinaryAxis<9> = BinaryAxis::new(&UNIFORM_KNOTS);
264 const UNIFORM_X: UniformAxis<9, 100, 50> = UniformAxis::new();
265 const BUCKETED_X: BucketedAxis<9, 4> = BucketedAxis::new(&UNIFORM_KNOTS, &UNIFORM_BUCKETS);
266
267 const LINEAR_Y: LinearAxis<3> = LinearAxis::new(&Y_KNOTS);
268 const BINARY_Y: BinaryAxis<3> = BinaryAxis::new(&Y_KNOTS);
269 const UNIFORM_Y: UniformAxis<3, 10, 10> = UniformAxis::new();
270 const BUCKETED_Y: BucketedAxis<3, 2> = BucketedAxis::new(&Y_KNOTS, &Y_BUCKETS);
271
272 static IRREGULAR_KNOTS: [u16; 7] = [3, 4, 5, 1_000, 40_000, 65_000, 65_535];
274 static IRREGULAR_BUCKETS: [u16; 8] = bucket_index(&IRREGULAR_KNOTS);
275 static IRREGULAR_VALUES: [[i32; 7]; 3] = [
276 [0, 1, 4, 9, 16, 25, 36],
277 [100, 102, 108, 118, 132, 150, 172],
278 [-50, -49, -46, -41, -34, -25, -14],
279 ];
280
281 const IRREGULAR_LINEAR: LinearAxis<7> = LinearAxis::new(&IRREGULAR_KNOTS);
282 const IRREGULAR_BINARY: BinaryAxis<7> = BinaryAxis::new(&IRREGULAR_KNOTS);
283 const IRREGULAR_BUCKETED: BucketedAxis<7, 8> =
284 BucketedAxis::new(&IRREGULAR_KNOTS, &IRREGULAR_BUCKETS);
285
286 fn uniform_probes() -> impl Iterator<Item = u16> {
290 (99u16..=501).chain([0, 50, 502, 1_000, u16::MAX])
291 }
292
293 #[test]
294 fn every_strategy_locates_the_same_cell_on_an_equivalent_axis() {
295 for coordinate in 100u16..=500 {
296 let expected = BINARY_X.search(coordinate).0;
297
298 assert_eq!(LINEAR_X.search(coordinate).0, expected, "at {coordinate}");
299 assert_eq!(UNIFORM_X.search(coordinate).0, expected, "at {coordinate}");
300 assert_eq!(BUCKETED_X.search(coordinate).0, expected, "at {coordinate}");
301 }
302 }
303
304 #[test]
305 fn every_strategy_reports_the_same_domain_and_knots() {
306 for (index, &expected) in UNIFORM_KNOTS.iter().enumerate() {
307 assert_eq!(LINEAR_X.knot(index), expected);
308 assert_eq!(BINARY_X.knot(index), expected);
309 assert_eq!(UNIFORM_X.knot(index), expected);
310 assert_eq!(BUCKETED_X.knot(index), expected);
311 }
312
313 for (first, last) in [
314 (LINEAR_X.first(), LINEAR_X.last()),
315 (BINARY_X.first(), BINARY_X.last()),
316 (UNIFORM_X.first(), UNIFORM_X.last()),
317 (BUCKETED_X.first(), BUCKETED_X.last()),
318 ] {
319 assert_eq!((first, last), (100, 500));
320 }
321 }
322
323 #[test]
324 fn the_three_stored_knot_strategies_agree_on_an_irregular_axis() {
325 for coordinate in probes(&IRREGULAR_KNOTS, 211) {
328 let expected = IRREGULAR_BINARY.search(coordinate).0;
329
330 assert_eq!(
331 IRREGULAR_LINEAR.search(coordinate).0,
332 expected,
333 "at {coordinate}"
334 );
335 assert_eq!(
336 IRREGULAR_BUCKETED.search(coordinate).0,
337 expected,
338 "at {coordinate}"
339 );
340 }
341 }
342
343 fn policy_from_bits(bits: usize) -> BoundaryPolicy {
346 let side = |shift: u32| {
347 if (bits >> shift) & 1 == 0 {
348 Boundary::Error
349 } else {
350 Boundary::Clamp
351 }
352 };
353
354 BoundaryPolicy::new()
355 .with_x_below(side(0))
356 .with_x_above(side(1))
357 .with_y_below(side(2))
358 .with_y_above(side(3))
359 }
360
361 #[test]
362 fn every_pairing_evaluates_identically_under_every_policy() {
363 for bits in 0..16 {
367 let policy = policy_from_bits(bits);
368 let baseline =
369 BilinearSurface::from_axes(BINARY_X, BINARY_Y, &VALUES).with_policy(policy);
370
371 macro_rules! agrees {
372 ($x:expr, $y:expr) => {
373 let surface = BilinearSurface::from_axes($x, $y, &VALUES).with_policy(policy);
374 for x in uniform_probes() {
375 for y in [0u16, 9, 10, 11, 20, 25, 30, 31, 100, u16::MAX] {
376 assert_eq!(
377 surface.evaluate(x, y),
378 baseline.evaluate(x, y),
379 "bits {bits} at ({x}, {y})"
380 );
381 }
382 }
383 };
384 }
385
386 agrees!(LINEAR_X, LINEAR_Y);
387 agrees!(LINEAR_X, BINARY_Y);
388 agrees!(LINEAR_X, UNIFORM_Y);
389 agrees!(LINEAR_X, BUCKETED_Y);
390 agrees!(BINARY_X, LINEAR_Y);
391 agrees!(BINARY_X, BINARY_Y);
392 agrees!(BINARY_X, UNIFORM_Y);
393 agrees!(BINARY_X, BUCKETED_Y);
394 agrees!(UNIFORM_X, LINEAR_Y);
395 agrees!(UNIFORM_X, BINARY_Y);
396 agrees!(UNIFORM_X, UNIFORM_Y);
397 agrees!(UNIFORM_X, BUCKETED_Y);
398 agrees!(BUCKETED_X, LINEAR_Y);
399 agrees!(BUCKETED_X, BINARY_Y);
400 agrees!(BUCKETED_X, UNIFORM_Y);
401 agrees!(BUCKETED_X, BUCKETED_Y);
402 }
403 }
404
405 #[test]
406 fn a_mixed_pairing_reproduces_the_default_surface_on_an_irregular_axis() {
407 let baseline = BilinearSurface::new(&IRREGULAR_KNOTS, &Y_KNOTS, &IRREGULAR_VALUES);
408 let linear_bucketed =
409 BilinearSurface::from_axes(IRREGULAR_LINEAR, BUCKETED_Y, &IRREGULAR_VALUES);
410 let bucketed_uniform =
411 BilinearSurface::from_axes(IRREGULAR_BUCKETED, UNIFORM_Y, &IRREGULAR_VALUES);
412
413 let mut x = 3u16;
414 loop {
415 for y in [10u16, 15, 20, 25, 30] {
416 let expected = baseline.evaluate(x, y);
417 assert_eq!(linear_bucketed.evaluate(x, y), expected, "({x}, {y})");
418 assert_eq!(bucketed_uniform.evaluate(x, y), expected, "({x}, {y})");
419 }
420
421 if x == u16::MAX {
422 break;
423 }
424 x = x.saturating_add(197);
425 }
426 }
427
428 #[test]
429 fn the_four_error_variants_are_invariant_across_pairings() {
430 let cases: [(u16, u16, SurfaceError); 4] = [
431 (
432 99,
433 20,
434 SurfaceError::XBelow {
435 coordinate: 99,
436 bound: 100,
437 },
438 ),
439 (
440 501,
441 20,
442 SurfaceError::XAbove {
443 coordinate: 501,
444 bound: 500,
445 },
446 ),
447 (
448 200,
449 9,
450 SurfaceError::YBelow {
451 coordinate: 9,
452 bound: 10,
453 },
454 ),
455 (
456 200,
457 31,
458 SurfaceError::YAbove {
459 coordinate: 31,
460 bound: 30,
461 },
462 ),
463 ];
464
465 for (x, y, expected) in cases {
466 assert_eq!(
467 BilinearSurface::from_axes(UNIFORM_X, BUCKETED_Y, &VALUES).evaluate(x, y),
468 Err(expected)
469 );
470 assert_eq!(
471 BilinearSurface::from_axes(BUCKETED_X, LINEAR_Y, &VALUES).evaluate(x, y),
472 Err(expected)
473 );
474 assert_eq!(
475 BilinearSurface::from_axes(LINEAR_X, UNIFORM_Y, &VALUES).evaluate(x, y),
476 Err(expected)
477 );
478 }
479 }
480
481 #[test]
482 fn x_before_y_precedence_is_invariant_across_pairings() {
483 let expected = Err(SurfaceError::XBelow {
486 coordinate: 99,
487 bound: 100,
488 });
489
490 assert_eq!(
491 BilinearSurface::from_axes(UNIFORM_X, UNIFORM_Y, &VALUES).evaluate(99, 9),
492 expected
493 );
494 assert_eq!(
495 BilinearSurface::from_axes(BUCKETED_X, LINEAR_Y, &VALUES).evaluate(99, 9),
496 expected
497 );
498
499 let clamped_x = BoundaryPolicy::new()
501 .with_x_below(Boundary::Clamp)
502 .with_x_above(Boundary::Clamp);
503 assert_eq!(
504 BilinearSurface::from_axes(LINEAR_X, BUCKETED_Y, &VALUES)
505 .with_policy(clamped_x)
506 .evaluate(99, 9),
507 Err(SurfaceError::YBelow {
508 coordinate: 9,
509 bound: 10,
510 })
511 );
512 }
513
514 #[test]
515 fn clamping_never_extrapolates_under_any_strategy() {
516 let all_clamp = policy_from_bits(0b1111);
517 let hull = VALUES
518 .iter()
519 .flat_map(|row| row.iter().copied())
520 .fold((i32::MAX, i32::MIN), |(lo, hi), v| (lo.min(v), hi.max(v)));
521
522 macro_rules! clamps_into_the_hull {
523 ($x:expr, $y:expr) => {
524 let surface = BilinearSurface::from_axes($x, $y, &VALUES).with_policy(all_clamp);
525 for x in [0u16, 1, 99, 501, u16::MAX] {
526 for y in [0u16, 9, 31, u16::MAX] {
527 let value = surface.evaluate(x, y).expect("every side clamps");
528 assert!(
529 (hull.0..=hull.1).contains(&value),
530 "({x}, {y}) extrapolated to {value}"
531 );
532 }
533 }
534 assert_eq!(surface.evaluate(0, 20), surface.evaluate(100, 20));
536 assert_eq!(surface.evaluate(u16::MAX, 20), surface.evaluate(500, 20));
537 assert_eq!(surface.evaluate(200, 0), surface.evaluate(200, 10));
538 assert_eq!(surface.evaluate(200, u16::MAX), surface.evaluate(200, 30));
539 };
540 }
541
542 clamps_into_the_hull!(LINEAR_X, UNIFORM_Y);
543 clamps_into_the_hull!(BINARY_X, BUCKETED_Y);
544 clamps_into_the_hull!(UNIFORM_X, LINEAR_Y);
545 clamps_into_the_hull!(BUCKETED_X, BINARY_Y);
546 }
547
548 #[test]
549 fn every_declared_knot_returns_its_stored_value_under_every_pairing() {
550 macro_rules! knots_are_exact {
551 ($x:expr, $y:expr) => {
552 let surface = BilinearSurface::from_axes($x, $y, &VALUES);
553 for (row, &y) in Y_KNOTS.iter().enumerate() {
554 for (column, &x) in UNIFORM_KNOTS.iter().enumerate() {
555 assert_eq!(surface.evaluate(x, y), Ok(VALUES[row][column]));
556 }
557 }
558 };
559 }
560
561 knots_are_exact!(LINEAR_X, LINEAR_Y);
562 knots_are_exact!(BINARY_X, UNIFORM_Y);
563 knots_are_exact!(UNIFORM_X, BUCKETED_Y);
564 knots_are_exact!(BUCKETED_X, BINARY_Y);
565 }
566
567 static ORDER_KNOTS: [u16; 2] = [0, 2];
570 static ORDER_VALUES: [[i32; 2]; 2] = [[0, 0], [1, 3]];
571 static ORDER_BUCKETS: [u16; 2] = bucket_index(&ORDER_KNOTS);
572
573 const ORDER_LINEAR: LinearAxis<2> = LinearAxis::new(&ORDER_KNOTS);
574 const ORDER_BINARY: BinaryAxis<2> = BinaryAxis::new(&ORDER_KNOTS);
575 const ORDER_UNIFORM: UniformAxis<2, 0, 2> = UniformAxis::new();
576 const ORDER_BUCKETED: BucketedAxis<2, 2> = BucketedAxis::new(&ORDER_KNOTS, &ORDER_BUCKETS);
577
578 #[test]
579 fn the_locked_order_fixture_still_distinguishes_x_then_y_under_every_pairing() {
580 macro_rules! keeps_the_order {
583 ($x:expr, $y:expr) => {
584 let surface = BilinearSurface::from_axes($x, $y, &ORDER_VALUES);
585 assert_eq!(surface.evaluate(1, 1), Ok(1));
586 assert_ne!(surface.evaluate(1, 1), Ok(2));
587 };
588 }
589
590 keeps_the_order!(ORDER_LINEAR, ORDER_LINEAR);
591 keeps_the_order!(ORDER_LINEAR, ORDER_BINARY);
592 keeps_the_order!(ORDER_LINEAR, ORDER_UNIFORM);
593 keeps_the_order!(ORDER_LINEAR, ORDER_BUCKETED);
594 keeps_the_order!(ORDER_BINARY, ORDER_LINEAR);
595 keeps_the_order!(ORDER_BINARY, ORDER_BINARY);
596 keeps_the_order!(ORDER_BINARY, ORDER_UNIFORM);
597 keeps_the_order!(ORDER_BINARY, ORDER_BUCKETED);
598 keeps_the_order!(ORDER_UNIFORM, ORDER_LINEAR);
599 keeps_the_order!(ORDER_UNIFORM, ORDER_BINARY);
600 keeps_the_order!(ORDER_UNIFORM, ORDER_UNIFORM);
601 keeps_the_order!(ORDER_UNIFORM, ORDER_BUCKETED);
602 keeps_the_order!(ORDER_BUCKETED, ORDER_LINEAR);
603 keeps_the_order!(ORDER_BUCKETED, ORDER_BINARY);
604 keeps_the_order!(ORDER_BUCKETED, ORDER_UNIFORM);
605 keeps_the_order!(ORDER_BUCKETED, ORDER_BUCKETED);
606 }
607
608 #[test]
609 fn no_strategy_exceeds_its_declared_search_bound() {
610 for coordinate in 100u16..=500 {
611 assert!(LINEAR_X.search(coordinate).1 <= <LinearAxis<9>>::MAX_SEARCH_COMPARISONS);
612 assert!(BINARY_X.search(coordinate).1 <= <BinaryAxis<9>>::MAX_SEARCH_COMPARISONS);
613 assert!(
614 BUCKETED_X.search(coordinate).1 <= <BucketedAxis<9, 4>>::MAX_SEARCH_COMPARISONS
615 );
616 assert_eq!(UNIFORM_X.search(coordinate).1, 0);
619 }
620
621 assert_eq!(<LinearAxis<9>>::MAX_SEARCH_COMPARISONS, 8);
624 assert_eq!(<BinaryAxis<9>>::MAX_SEARCH_COMPARISONS, 4);
625 assert_eq!(<UniformAxis<9, 100, 50>>::MAX_SEARCH_COMPARISONS, 0);
626 assert_eq!(<BucketedAxis<9, 4>>::MAX_SEARCH_COMPARISONS, 8);
627 }
628
629 #[test]
630 fn stored_bytes_are_exactly_what_each_strategy_declares() {
631 assert_eq!(<LinearAxis<9>>::KNOT_BYTES, 18);
632 assert_eq!(<LinearAxis<9>>::INDEX_BYTES, 0);
633 assert_eq!(<BinaryAxis<9>>::KNOT_BYTES, 18);
634 assert_eq!(<BinaryAxis<9>>::INDEX_BYTES, 0);
635 assert_eq!(<UniformAxis<9, 100, 50>>::KNOT_BYTES, 0);
636 assert_eq!(<UniformAxis<9, 100, 50>>::INDEX_BYTES, 0);
637 assert_eq!(<BucketedAxis<9, 4>>::KNOT_BYTES, 18);
638 assert_eq!(<BucketedAxis<9, 4>>::INDEX_BYTES, 8);
639
640 assert_eq!(<LinearAxis<9>>::KNOT_BYTES, size_of::<[u16; 9]>());
642 assert_eq!(<BucketedAxis<9, 4>>::INDEX_BYTES, size_of::<[u16; 4]>());
643 }
644
645 #[test]
646 fn a_uniform_axis_occupies_no_storage_and_the_others_are_thin_handles() {
647 assert_eq!(size_of::<UniformAxis<9, 100, 50>>(), 0);
650
651 assert_eq!(size_of::<LinearAxis<9>>(), size_of::<usize>());
652 assert_eq!(size_of::<BinaryAxis<9>>(), size_of::<usize>());
653 assert_eq!(size_of::<BucketedAxis<9, 4>>(), 2 * size_of::<usize>());
654 }
655
656 #[test]
657 fn a_strategy_is_a_type_and_never_a_runtime_discriminant() {
658 assert_eq!(size_of::<LinearAxis<9>>(), size_of::<&'static [u16; 9]>());
661 assert_eq!(size_of::<BinaryAxis<9>>(), size_of::<&'static [u16; 9]>());
662 assert_eq!(
663 size_of::<BucketedAxis<9, 4>>(),
664 size_of::<&'static [u16; 9]>() + size_of::<&'static [u16; 4]>()
665 );
666
667 assert_eq!(
670 size_of::<BilinearSurface<9, 3>>(),
671 size_of::<BilinearSurface<9, 3, BinaryAxis<9>, BinaryAxis<3>>>()
672 );
673 }
674
675 #[test]
676 fn the_default_surface_is_the_binary_pairing() {
677 let defaulted: BilinearSurface<9, 3> =
681 BilinearSurface::new(&UNIFORM_KNOTS, &Y_KNOTS, &VALUES);
682 let explicit: BilinearSurface<9, 3, BinaryAxis<9>, BinaryAxis<3>> =
683 BilinearSurface::from_axes(BINARY_X, BINARY_Y, &VALUES);
684
685 assert_eq!(defaulted, explicit);
686 }
687}