mod binary;
mod bucketed;
mod linear;
mod uniform;
pub use binary::BinaryAxis;
pub use bucketed::{BucketedAxis, bucket_index, max_local_comparisons};
pub use linear::LinearAxis;
pub use uniform::UniformAxis;
mod sealed {
pub trait Sealed<const N: usize> {
fn search_in_domain(&self, coordinate: u16) -> (usize, u32);
}
}
pub trait AxisLookup<const N: usize>: sealed::Sealed<N> + Copy {
const KNOT_BYTES: usize;
const INDEX_BYTES: usize;
const MAX_SEARCH_COMPARISONS: u32;
fn first(&self) -> u16;
fn last(&self) -> u16;
fn knot(&self, index: usize) -> u16;
fn search(&self, coordinate: u16) -> (usize, u32) {
crate::lookup::search(self, coordinate)
}
}
#[inline(always)]
pub(crate) fn search_in_domain<const N: usize, A: AxisLookup<N>>(
axis: &A,
coordinate: u16,
) -> (usize, u32) {
<A as sealed::Sealed<N>>::search_in_domain(axis, coordinate)
}
pub trait KnotArray<const N: usize>: AxisLookup<N> {
fn knots(&self) -> &'static [u16; N];
}
pub(crate) const fn probe_bound(len: usize) -> u32 {
debug_assert!(len >= 2, "an axis declares at least two knots");
usize::BITS - (len - 1).leading_zeros()
}
const fn assert_valid_knots<const N: usize>(knots: &[u16; N]) {
assert!(N >= 2, "an axis must declare at least two knots");
let mut i = 1;
while i < N {
assert!(
knots[i - 1] < knots[i],
"axis knots must be strictly increasing"
);
i += 1;
}
}
#[cfg(test)]
pub(crate) fn probes<const N: usize>(
knots: &'static [u16; N],
stride: usize,
) -> impl Iterator<Item = u16> {
knots
.iter()
.flat_map(|&knot| [knot.saturating_sub(1), knot, knot.saturating_add(1)])
.chain((knots[0]..=knots[N - 1]).step_by(stride))
.filter(|&coordinate| coordinate >= knots[0] && coordinate <= knots[N - 1])
}
#[cfg(test)]
mod tests {
use super::{
AxisLookup, BinaryAxis, BucketedAxis, LinearAxis, UniformAxis, bucket_index, probes,
};
use crate::boundary::{Boundary, BoundaryPolicy};
use crate::error::SurfaceError;
use crate::surface::BilinearSurface;
use core::mem::size_of;
static UNIFORM_KNOTS: [u16; 9] = [100, 150, 200, 250, 300, 350, 400, 450, 500];
static UNIFORM_BUCKETS: [u16; 4] = bucket_index(&UNIFORM_KNOTS);
static Y_KNOTS: [u16; 3] = [10, 20, 30];
static Y_BUCKETS: [u16; 2] = bucket_index(&Y_KNOTS);
static VALUES: [[i32; 9]; 3] = [
[0, 1, 4, 9, 16, 25, 36, 49, 64],
[100, 102, 108, 118, 132, 150, 172, 198, 228],
[-50, -49, -46, -41, -34, -25, -14, -1, 14],
];
const LINEAR_X: LinearAxis<9> = LinearAxis::new(&UNIFORM_KNOTS);
const BINARY_X: BinaryAxis<9> = BinaryAxis::new(&UNIFORM_KNOTS);
const UNIFORM_X: UniformAxis<9, 100, 50> = UniformAxis::new();
const BUCKETED_X: BucketedAxis<9, 4> = BucketedAxis::new(&UNIFORM_KNOTS, &UNIFORM_BUCKETS);
const LINEAR_Y: LinearAxis<3> = LinearAxis::new(&Y_KNOTS);
const BINARY_Y: BinaryAxis<3> = BinaryAxis::new(&Y_KNOTS);
const UNIFORM_Y: UniformAxis<3, 10, 10> = UniformAxis::new();
const BUCKETED_Y: BucketedAxis<3, 2> = BucketedAxis::new(&Y_KNOTS, &Y_BUCKETS);
static IRREGULAR_KNOTS: [u16; 7] = [3, 4, 5, 1_000, 40_000, 65_000, 65_535];
static IRREGULAR_BUCKETS: [u16; 8] = bucket_index(&IRREGULAR_KNOTS);
static IRREGULAR_VALUES: [[i32; 7]; 3] = [
[0, 1, 4, 9, 16, 25, 36],
[100, 102, 108, 118, 132, 150, 172],
[-50, -49, -46, -41, -34, -25, -14],
];
const IRREGULAR_LINEAR: LinearAxis<7> = LinearAxis::new(&IRREGULAR_KNOTS);
const IRREGULAR_BINARY: BinaryAxis<7> = BinaryAxis::new(&IRREGULAR_KNOTS);
const IRREGULAR_BUCKETED: BucketedAxis<7, 8> =
BucketedAxis::new(&IRREGULAR_KNOTS, &IRREGULAR_BUCKETS);
fn uniform_probes() -> impl Iterator<Item = u16> {
(99u16..=501).chain([0, 50, 502, 1_000, u16::MAX])
}
#[test]
fn every_strategy_locates_the_same_cell_on_an_equivalent_axis() {
for coordinate in 100u16..=500 {
let expected = BINARY_X.search(coordinate).0;
assert_eq!(LINEAR_X.search(coordinate).0, expected, "at {coordinate}");
assert_eq!(UNIFORM_X.search(coordinate).0, expected, "at {coordinate}");
assert_eq!(BUCKETED_X.search(coordinate).0, expected, "at {coordinate}");
}
}
#[test]
fn every_strategy_reports_the_same_domain_and_knots() {
for (index, &expected) in UNIFORM_KNOTS.iter().enumerate() {
assert_eq!(LINEAR_X.knot(index), expected);
assert_eq!(BINARY_X.knot(index), expected);
assert_eq!(UNIFORM_X.knot(index), expected);
assert_eq!(BUCKETED_X.knot(index), expected);
}
for (first, last) in [
(LINEAR_X.first(), LINEAR_X.last()),
(BINARY_X.first(), BINARY_X.last()),
(UNIFORM_X.first(), UNIFORM_X.last()),
(BUCKETED_X.first(), BUCKETED_X.last()),
] {
assert_eq!((first, last), (100, 500));
}
}
#[test]
fn the_three_stored_knot_strategies_agree_on_an_irregular_axis() {
for coordinate in probes(&IRREGULAR_KNOTS, 211) {
let expected = IRREGULAR_BINARY.search(coordinate).0;
assert_eq!(
IRREGULAR_LINEAR.search(coordinate).0,
expected,
"at {coordinate}"
);
assert_eq!(
IRREGULAR_BUCKETED.search(coordinate).0,
expected,
"at {coordinate}"
);
}
}
fn policy_from_bits(bits: usize) -> BoundaryPolicy {
let side = |shift: u32| {
if (bits >> shift) & 1 == 0 {
Boundary::Error
} else {
Boundary::Clamp
}
};
BoundaryPolicy::new()
.with_x_below(side(0))
.with_x_above(side(1))
.with_y_below(side(2))
.with_y_above(side(3))
}
#[test]
fn every_pairing_evaluates_identically_under_every_policy() {
for bits in 0..16 {
let policy = policy_from_bits(bits);
let baseline =
BilinearSurface::from_axes(BINARY_X, BINARY_Y, &VALUES).with_policy(policy);
macro_rules! agrees {
($x:expr, $y:expr) => {
let surface = BilinearSurface::from_axes($x, $y, &VALUES).with_policy(policy);
for x in uniform_probes() {
for y in [0u16, 9, 10, 11, 20, 25, 30, 31, 100, u16::MAX] {
assert_eq!(
surface.evaluate(x, y),
baseline.evaluate(x, y),
"bits {bits} at ({x}, {y})"
);
}
}
};
}
agrees!(LINEAR_X, LINEAR_Y);
agrees!(LINEAR_X, BINARY_Y);
agrees!(LINEAR_X, UNIFORM_Y);
agrees!(LINEAR_X, BUCKETED_Y);
agrees!(BINARY_X, LINEAR_Y);
agrees!(BINARY_X, BINARY_Y);
agrees!(BINARY_X, UNIFORM_Y);
agrees!(BINARY_X, BUCKETED_Y);
agrees!(UNIFORM_X, LINEAR_Y);
agrees!(UNIFORM_X, BINARY_Y);
agrees!(UNIFORM_X, UNIFORM_Y);
agrees!(UNIFORM_X, BUCKETED_Y);
agrees!(BUCKETED_X, LINEAR_Y);
agrees!(BUCKETED_X, BINARY_Y);
agrees!(BUCKETED_X, UNIFORM_Y);
agrees!(BUCKETED_X, BUCKETED_Y);
}
}
#[test]
fn a_mixed_pairing_reproduces_the_default_surface_on_an_irregular_axis() {
let baseline = BilinearSurface::new(&IRREGULAR_KNOTS, &Y_KNOTS, &IRREGULAR_VALUES);
let linear_bucketed =
BilinearSurface::from_axes(IRREGULAR_LINEAR, BUCKETED_Y, &IRREGULAR_VALUES);
let bucketed_uniform =
BilinearSurface::from_axes(IRREGULAR_BUCKETED, UNIFORM_Y, &IRREGULAR_VALUES);
let mut x = 3u16;
loop {
for y in [10u16, 15, 20, 25, 30] {
let expected = baseline.evaluate(x, y);
assert_eq!(linear_bucketed.evaluate(x, y), expected, "({x}, {y})");
assert_eq!(bucketed_uniform.evaluate(x, y), expected, "({x}, {y})");
}
if x == u16::MAX {
break;
}
x = x.saturating_add(197);
}
}
#[test]
fn the_four_error_variants_are_invariant_across_pairings() {
let cases: [(u16, u16, SurfaceError); 4] = [
(
99,
20,
SurfaceError::XBelow {
coordinate: 99,
bound: 100,
},
),
(
501,
20,
SurfaceError::XAbove {
coordinate: 501,
bound: 500,
},
),
(
200,
9,
SurfaceError::YBelow {
coordinate: 9,
bound: 10,
},
),
(
200,
31,
SurfaceError::YAbove {
coordinate: 31,
bound: 30,
},
),
];
for (x, y, expected) in cases {
assert_eq!(
BilinearSurface::from_axes(UNIFORM_X, BUCKETED_Y, &VALUES).evaluate(x, y),
Err(expected)
);
assert_eq!(
BilinearSurface::from_axes(BUCKETED_X, LINEAR_Y, &VALUES).evaluate(x, y),
Err(expected)
);
assert_eq!(
BilinearSurface::from_axes(LINEAR_X, UNIFORM_Y, &VALUES).evaluate(x, y),
Err(expected)
);
}
}
#[test]
fn x_before_y_precedence_is_invariant_across_pairings() {
let expected = Err(SurfaceError::XBelow {
coordinate: 99,
bound: 100,
});
assert_eq!(
BilinearSurface::from_axes(UNIFORM_X, UNIFORM_Y, &VALUES).evaluate(99, 9),
expected
);
assert_eq!(
BilinearSurface::from_axes(BUCKETED_X, LINEAR_Y, &VALUES).evaluate(99, 9),
expected
);
let clamped_x = BoundaryPolicy::new()
.with_x_below(Boundary::Clamp)
.with_x_above(Boundary::Clamp);
assert_eq!(
BilinearSurface::from_axes(LINEAR_X, BUCKETED_Y, &VALUES)
.with_policy(clamped_x)
.evaluate(99, 9),
Err(SurfaceError::YBelow {
coordinate: 9,
bound: 10,
})
);
}
#[test]
fn clamping_never_extrapolates_under_any_strategy() {
let all_clamp = policy_from_bits(0b1111);
let hull = VALUES
.iter()
.flat_map(|row| row.iter().copied())
.fold((i32::MAX, i32::MIN), |(lo, hi), v| (lo.min(v), hi.max(v)));
macro_rules! clamps_into_the_hull {
($x:expr, $y:expr) => {
let surface = BilinearSurface::from_axes($x, $y, &VALUES).with_policy(all_clamp);
for x in [0u16, 1, 99, 501, u16::MAX] {
for y in [0u16, 9, 31, u16::MAX] {
let value = surface.evaluate(x, y).expect("every side clamps");
assert!(
(hull.0..=hull.1).contains(&value),
"({x}, {y}) extrapolated to {value}"
);
}
}
assert_eq!(surface.evaluate(0, 20), surface.evaluate(100, 20));
assert_eq!(surface.evaluate(u16::MAX, 20), surface.evaluate(500, 20));
assert_eq!(surface.evaluate(200, 0), surface.evaluate(200, 10));
assert_eq!(surface.evaluate(200, u16::MAX), surface.evaluate(200, 30));
};
}
clamps_into_the_hull!(LINEAR_X, UNIFORM_Y);
clamps_into_the_hull!(BINARY_X, BUCKETED_Y);
clamps_into_the_hull!(UNIFORM_X, LINEAR_Y);
clamps_into_the_hull!(BUCKETED_X, BINARY_Y);
}
#[test]
fn every_declared_knot_returns_its_stored_value_under_every_pairing() {
macro_rules! knots_are_exact {
($x:expr, $y:expr) => {
let surface = BilinearSurface::from_axes($x, $y, &VALUES);
for (row, &y) in Y_KNOTS.iter().enumerate() {
for (column, &x) in UNIFORM_KNOTS.iter().enumerate() {
assert_eq!(surface.evaluate(x, y), Ok(VALUES[row][column]));
}
}
};
}
knots_are_exact!(LINEAR_X, LINEAR_Y);
knots_are_exact!(BINARY_X, UNIFORM_Y);
knots_are_exact!(UNIFORM_X, BUCKETED_Y);
knots_are_exact!(BUCKETED_X, BINARY_Y);
}
static ORDER_KNOTS: [u16; 2] = [0, 2];
static ORDER_VALUES: [[i32; 2]; 2] = [[0, 0], [1, 3]];
static ORDER_BUCKETS: [u16; 2] = bucket_index(&ORDER_KNOTS);
const ORDER_LINEAR: LinearAxis<2> = LinearAxis::new(&ORDER_KNOTS);
const ORDER_BINARY: BinaryAxis<2> = BinaryAxis::new(&ORDER_KNOTS);
const ORDER_UNIFORM: UniformAxis<2, 0, 2> = UniformAxis::new();
const ORDER_BUCKETED: BucketedAxis<2, 2> = BucketedAxis::new(&ORDER_KNOTS, &ORDER_BUCKETS);
#[test]
fn the_locked_order_fixture_still_distinguishes_x_then_y_under_every_pairing() {
macro_rules! keeps_the_order {
($x:expr, $y:expr) => {
let surface = BilinearSurface::from_axes($x, $y, &ORDER_VALUES);
assert_eq!(surface.evaluate(1, 1), Ok(1));
assert_ne!(surface.evaluate(1, 1), Ok(2));
};
}
keeps_the_order!(ORDER_LINEAR, ORDER_LINEAR);
keeps_the_order!(ORDER_LINEAR, ORDER_BINARY);
keeps_the_order!(ORDER_LINEAR, ORDER_UNIFORM);
keeps_the_order!(ORDER_LINEAR, ORDER_BUCKETED);
keeps_the_order!(ORDER_BINARY, ORDER_LINEAR);
keeps_the_order!(ORDER_BINARY, ORDER_BINARY);
keeps_the_order!(ORDER_BINARY, ORDER_UNIFORM);
keeps_the_order!(ORDER_BINARY, ORDER_BUCKETED);
keeps_the_order!(ORDER_UNIFORM, ORDER_LINEAR);
keeps_the_order!(ORDER_UNIFORM, ORDER_BINARY);
keeps_the_order!(ORDER_UNIFORM, ORDER_UNIFORM);
keeps_the_order!(ORDER_UNIFORM, ORDER_BUCKETED);
keeps_the_order!(ORDER_BUCKETED, ORDER_LINEAR);
keeps_the_order!(ORDER_BUCKETED, ORDER_BINARY);
keeps_the_order!(ORDER_BUCKETED, ORDER_UNIFORM);
keeps_the_order!(ORDER_BUCKETED, ORDER_BUCKETED);
}
#[test]
fn no_strategy_exceeds_its_declared_search_bound() {
for coordinate in 100u16..=500 {
assert!(LINEAR_X.search(coordinate).1 <= <LinearAxis<9>>::MAX_SEARCH_COMPARISONS);
assert!(BINARY_X.search(coordinate).1 <= <BinaryAxis<9>>::MAX_SEARCH_COMPARISONS);
assert!(
BUCKETED_X.search(coordinate).1 <= <BucketedAxis<9, 4>>::MAX_SEARCH_COMPARISONS
);
assert_eq!(UNIFORM_X.search(coordinate).1, 0);
}
assert_eq!(<LinearAxis<9>>::MAX_SEARCH_COMPARISONS, 8);
assert_eq!(<BinaryAxis<9>>::MAX_SEARCH_COMPARISONS, 4);
assert_eq!(<UniformAxis<9, 100, 50>>::MAX_SEARCH_COMPARISONS, 0);
assert_eq!(<BucketedAxis<9, 4>>::MAX_SEARCH_COMPARISONS, 8);
}
#[test]
fn stored_bytes_are_exactly_what_each_strategy_declares() {
assert_eq!(<LinearAxis<9>>::KNOT_BYTES, 18);
assert_eq!(<LinearAxis<9>>::INDEX_BYTES, 0);
assert_eq!(<BinaryAxis<9>>::KNOT_BYTES, 18);
assert_eq!(<BinaryAxis<9>>::INDEX_BYTES, 0);
assert_eq!(<UniformAxis<9, 100, 50>>::KNOT_BYTES, 0);
assert_eq!(<UniformAxis<9, 100, 50>>::INDEX_BYTES, 0);
assert_eq!(<BucketedAxis<9, 4>>::KNOT_BYTES, 18);
assert_eq!(<BucketedAxis<9, 4>>::INDEX_BYTES, 8);
assert_eq!(<LinearAxis<9>>::KNOT_BYTES, size_of::<[u16; 9]>());
assert_eq!(<BucketedAxis<9, 4>>::INDEX_BYTES, size_of::<[u16; 4]>());
}
#[test]
fn a_uniform_axis_occupies_no_storage_and_the_others_are_thin_handles() {
assert_eq!(size_of::<UniformAxis<9, 100, 50>>(), 0);
assert_eq!(size_of::<LinearAxis<9>>(), size_of::<usize>());
assert_eq!(size_of::<BinaryAxis<9>>(), size_of::<usize>());
assert_eq!(size_of::<BucketedAxis<9, 4>>(), 2 * size_of::<usize>());
}
#[test]
fn a_strategy_is_a_type_and_never_a_runtime_discriminant() {
assert_eq!(size_of::<LinearAxis<9>>(), size_of::<&'static [u16; 9]>());
assert_eq!(size_of::<BinaryAxis<9>>(), size_of::<&'static [u16; 9]>());
assert_eq!(
size_of::<BucketedAxis<9, 4>>(),
size_of::<&'static [u16; 9]>() + size_of::<&'static [u16; 4]>()
);
assert_eq!(
size_of::<BilinearSurface<9, 3>>(),
size_of::<BilinearSurface<9, 3, BinaryAxis<9>, BinaryAxis<3>>>()
);
}
#[test]
fn the_default_surface_is_the_binary_pairing() {
let defaulted: BilinearSurface<9, 3> =
BilinearSurface::new(&UNIFORM_KNOTS, &Y_KNOTS, &VALUES);
let explicit: BilinearSurface<9, 3, BinaryAxis<9>, BinaryAxis<3>> =
BilinearSurface::from_axes(BINARY_X, BINARY_Y, &VALUES);
assert_eq!(defaulted, explicit);
}
}