use crate::axis::{AxisLookup, BinaryAxis, BucketedAxis, LinearAxis, UniformAxis};
use crate::boundary::BoundaryPolicy;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BilinearSurface<
const NX: usize,
const NY: usize,
X: AxisLookup<NX> = BinaryAxis<NX>,
Y: AxisLookup<NY> = BinaryAxis<NY>,
> {
x: X,
y: Y,
values: &'static [[i32; NX]; NY],
policy: BoundaryPolicy,
}
impl<const NX: usize, const NY: usize> BilinearSurface<NX, NY, BinaryAxis<NX>, BinaryAxis<NY>> {
#[must_use]
pub const fn new(
x_axis: &'static [u16; NX],
y_axis: &'static [u16; NY],
values: &'static [[i32; NX]; NY],
) -> Self {
assert!(NX >= 2, "x axis must declare at least two knots");
assert!(NY >= 2, "y axis must declare at least two knots");
let mut i = 1;
while i < NX {
assert!(
x_axis[i - 1] < x_axis[i],
"x axis knots must be strictly increasing"
);
i += 1;
}
let mut i = 1;
while i < NY {
assert!(
y_axis[i - 1] < y_axis[i],
"y axis knots must be strictly increasing"
);
i += 1;
}
Self {
x: BinaryAxis::new(x_axis),
y: BinaryAxis::new(y_axis),
values,
policy: BoundaryPolicy::new(),
}
}
}
impl<const NX: usize, const NY: usize, X: AxisLookup<NX>, Y: AxisLookup<NY>>
BilinearSurface<NX, NY, X, Y>
{
pub const VALUE_BYTES: usize = 4 * NX * NY;
pub const PAYLOAD_BYTES: usize =
X::KNOT_BYTES + X::INDEX_BYTES + Y::KNOT_BYTES + Y::INDEX_BYTES + Self::VALUE_BYTES;
pub const HANDLE_BYTES: usize = core::mem::size_of::<Self>();
pub const SUCCESS_INTERPOLATIONS: u32 = 3;
pub const SUCCESS_GRID_READS: u32 = 4;
#[must_use]
pub const fn from_axes(x: X, y: Y, values: &'static [[i32; NX]; NY]) -> Self {
Self {
x,
y,
values,
policy: BoundaryPolicy::new(),
}
}
#[must_use]
pub const fn with_policy(self, policy: BoundaryPolicy) -> Self {
Self {
x: self.x,
y: self.y,
values: self.values,
policy,
}
}
#[must_use]
pub const fn x(&self) -> &X {
&self.x
}
#[must_use]
pub const fn y(&self) -> &Y {
&self.y
}
#[must_use]
pub const fn values(&self) -> &'static [[i32; NX]; NY] {
self.values
}
#[must_use]
pub const fn nx(&self) -> usize {
NX
}
#[must_use]
pub const fn ny(&self) -> usize {
NY
}
#[must_use]
pub const fn policy(&self) -> BoundaryPolicy {
self.policy
}
}
macro_rules! impl_stored_x_accessors {
($axis:ident) => {
impl<const NX: usize, const NY: usize, Y: AxisLookup<NY>>
BilinearSurface<NX, NY, $axis<NX>, Y>
{
#[must_use]
pub const fn x_knot(&self, index: usize) -> u16 {
assert!(index < NX, "knot index is outside the axis");
self.x.knots()[index]
}
#[must_use]
pub const fn x_min(&self) -> u16 {
self.x.knots()[0]
}
#[must_use]
pub const fn x_max(&self) -> u16 {
self.x.knots()[NX - 1]
}
#[must_use]
pub const fn x_axis(&self) -> &'static [u16; NX] {
self.x.knots()
}
}
};
}
macro_rules! impl_stored_y_accessors {
($axis:ident) => {
impl<const NX: usize, const NY: usize, X: AxisLookup<NX>>
BilinearSurface<NX, NY, X, $axis<NY>>
{
#[must_use]
pub const fn y_knot(&self, index: usize) -> u16 {
assert!(index < NY, "knot index is outside the axis");
self.y.knots()[index]
}
#[must_use]
pub const fn y_min(&self) -> u16 {
self.y.knots()[0]
}
#[must_use]
pub const fn y_max(&self) -> u16 {
self.y.knots()[NY - 1]
}
#[must_use]
pub const fn y_axis(&self) -> &'static [u16; NY] {
self.y.knots()
}
}
};
}
impl_stored_x_accessors!(BinaryAxis);
impl_stored_x_accessors!(LinearAxis);
impl_stored_y_accessors!(BinaryAxis);
impl_stored_y_accessors!(LinearAxis);
impl<const NX: usize, const NY: usize, const B: usize, Y: AxisLookup<NY>>
BilinearSurface<NX, NY, BucketedAxis<NX, B>, Y>
{
#[must_use]
pub const fn x_knot(&self, index: usize) -> u16 {
assert!(index < NX, "knot index is outside the axis");
self.x.knots()[index]
}
#[must_use]
pub const fn x_min(&self) -> u16 {
self.x.knots()[0]
}
#[must_use]
pub const fn x_max(&self) -> u16 {
self.x.knots()[NX - 1]
}
#[must_use]
pub const fn x_axis(&self) -> &'static [u16; NX] {
self.x.knots()
}
}
impl<const NX: usize, const NY: usize, const B: usize, X: AxisLookup<NX>>
BilinearSurface<NX, NY, X, BucketedAxis<NY, B>>
{
#[must_use]
pub const fn y_knot(&self, index: usize) -> u16 {
assert!(index < NY, "knot index is outside the axis");
self.y.knots()[index]
}
#[must_use]
pub const fn y_min(&self) -> u16 {
self.y.knots()[0]
}
#[must_use]
pub const fn y_max(&self) -> u16 {
self.y.knots()[NY - 1]
}
#[must_use]
pub const fn y_axis(&self) -> &'static [u16; NY] {
self.y.knots()
}
}
impl<const NX: usize, const NY: usize, const ORIGIN: u16, const STEP: u16, Y: AxisLookup<NY>>
BilinearSurface<NX, NY, UniformAxis<NX, ORIGIN, STEP>, Y>
{
#[must_use]
pub const fn x_knot(&self, index: usize) -> u16 {
self.x.knot(index)
}
#[must_use]
pub const fn x_min(&self) -> u16 {
self.x.origin()
}
#[must_use]
pub const fn x_max(&self) -> u16 {
self.x.last()
}
}
impl<const NX: usize, const NY: usize, X: AxisLookup<NX>, const ORIGIN: u16, const STEP: u16>
BilinearSurface<NX, NY, X, UniformAxis<NY, ORIGIN, STEP>>
{
#[must_use]
pub const fn y_knot(&self, index: usize) -> u16 {
self.y.knot(index)
}
#[must_use]
pub const fn y_min(&self) -> u16 {
self.y.origin()
}
#[must_use]
pub const fn y_max(&self) -> u16 {
self.y.last()
}
}
#[cfg(test)]
mod tests {
use super::BilinearSurface;
use crate::axis::{AxisLookup, BinaryAxis, BucketedAxis, LinearAxis, UniformAxis};
use crate::boundary::{Boundary, BoundaryPolicy};
use crate::{bucket_index, max_local_comparisons};
use core::mem::{align_of, size_of, size_of_val};
static X2: [u16; 2] = [0, 10];
static Y2: [u16; 2] = [0, 20];
static V22: [[i32; 2]; 2] = [[1, 2], [3, 4]];
static SURFACE: BilinearSurface<2, 2> = BilinearSurface::new(&X2, &Y2, &V22);
const CONST_SURFACE: BilinearSurface<2, 2> = BilinearSurface::new(&X2, &Y2, &V22);
const CONST_X_AXIS: &[u16; 2] = CONST_SURFACE.x_axis();
const CONST_Y_AXIS: &[u16; 2] = CONST_SURFACE.y_axis();
const CONST_X_MIN: u16 = CONST_SURFACE.x_min();
const CONST_X_MAX: u16 = CONST_SURFACE.x_max();
const CONST_Y_MIN: u16 = CONST_SURFACE.y_min();
const CONST_Y_MAX: u16 = CONST_SURFACE.y_max();
static X3: [u16; 3] = [0, 5, 100];
static Y2B: [u16; 2] = [7, 900];
static V23: [[i32; 3]; 2] = [[10, 20, 30], [40, 50, 60]];
static WIDE: BilinearSurface<3, 2> = BilinearSurface::new(&X3, &Y2B, &V23);
static MIXED: BilinearSurface<2, 2, LinearAxis<2>, UniformAxis<2, 0, 20>> =
BilinearSurface::from_axes(LinearAxis::new(&X2), UniformAxis::new(), &V22);
const fn side_from_bit(bits: usize, shift: u32) -> Boundary {
if (bits >> shift) & 1 == 0 {
Boundary::Error
} else {
Boundary::Clamp
}
}
const fn policy_from_bits(bits: usize) -> BoundaryPolicy {
BoundaryPolicy::new()
.with_x_below(side_from_bit(bits, 0))
.with_x_above(side_from_bit(bits, 1))
.with_y_below(side_from_bit(bits, 2))
.with_y_above(side_from_bit(bits, 3))
}
const POLICIES: [BoundaryPolicy; 16] = {
let mut out = [BoundaryPolicy::new(); 16];
let mut bits = 0;
while bits < 16 {
out[bits] = policy_from_bits(bits);
bits += 1;
}
out
};
static SURFACES: [BilinearSurface<2, 2>; 16] = {
let mut out = [BilinearSurface::new(&X2, &Y2, &V22); 16];
let mut bits = 0;
while bits < 16 {
out[bits] = BilinearSurface::new(&X2, &Y2, &V22).with_policy(POLICIES[bits]);
bits += 1;
}
out
};
#[test]
fn two_by_two_surface_declares_as_a_static() {
assert_eq!(SURFACE.nx(), 2);
assert_eq!(SURFACE.ny(), 2);
assert_eq!(SURFACE.x_axis(), &[0, 10]);
assert_eq!(SURFACE.y_axis(), &[0, 20]);
assert_eq!(SURFACE.values(), &[[1, 2], [3, 4]]);
}
#[test]
fn values_are_addressed_row_major_by_y_then_x() {
assert_eq!(WIDE.nx(), 3);
assert_eq!(WIDE.ny(), 2);
assert_eq!(WIDE.values()[0][2], 30);
assert_eq!(WIDE.values()[1][0], 40);
}
#[test]
fn new_defaults_every_side_to_error() {
let policy = SURFACE.policy();
assert_eq!(policy, BoundaryPolicy::new());
assert_eq!(policy.x_below(), Boundary::Error);
assert_eq!(policy.x_above(), Boundary::Error);
assert_eq!(policy.y_below(), Boundary::Error);
assert_eq!(policy.y_above(), Boundary::Error);
}
#[test]
fn from_axes_defaults_every_side_to_error_as_well() {
assert_eq!(MIXED.policy(), BoundaryPolicy::new());
assert_eq!(MIXED.policy().x_below(), Boundary::Error);
assert_eq!(MIXED.policy().y_above(), Boundary::Error);
}
#[test]
fn endpoint_accessors_report_declared_extremes() {
assert_eq!(WIDE.x_min(), 0);
assert_eq!(WIDE.x_max(), 100);
assert_eq!(WIDE.y_min(), 7);
assert_eq!(WIDE.y_max(), 900);
}
#[test]
fn default_axis_accessors_remain_const_compatible() {
assert_eq!(CONST_X_AXIS, &X2);
assert_eq!(CONST_Y_AXIS, &Y2);
assert_eq!((CONST_X_MIN, CONST_X_MAX), (0, 10));
assert_eq!((CONST_Y_MIN, CONST_Y_MAX), (0, 20));
}
#[test]
fn endpoint_and_knot_accessors_do_not_depend_on_a_stored_knot_array() {
assert_eq!(MIXED.y_min(), 0);
assert_eq!(MIXED.y_max(), 20);
assert_eq!(MIXED.y_knot(0), 0);
assert_eq!(MIXED.y_knot(1), 20);
assert_eq!(MIXED.x_knot(0), MIXED.x_axis()[0]);
assert_eq!(MIXED.x_knot(1), MIXED.x_axis()[1]);
assert_eq!(SURFACE.x_knot(1), 10);
assert_eq!(SURFACE.y_knot(1), 20);
}
#[test]
fn accessors_return_the_declared_tables_without_copying() {
assert!(core::ptr::eq(SURFACE.x_axis(), &X2));
assert!(core::ptr::eq(SURFACE.y_axis(), &Y2));
assert!(core::ptr::eq(SURFACE.values(), &V22));
}
#[test]
fn handle_size_is_independent_of_table_size() {
assert_eq!(
size_of::<BilinearSurface<2, 2>>(),
size_of::<BilinearSurface<64, 64>>()
);
}
const fn documented_payload(nx: usize, ny: usize) -> usize {
2 * nx + 2 * ny + 4 * nx * ny
}
const fn referenced_payload<const NX: usize, const NY: usize>() -> usize {
size_of::<[u16; NX]>() + size_of::<[u16; NY]>() + size_of::<[[i32; NX]; NY]>()
}
#[test]
fn cost_constants_match_the_declared_tables() {
assert_eq!(
BilinearSurface::<3, 2>::VALUE_BYTES,
size_of::<[[i32; 3]; 2]>()
);
assert_eq!(
BilinearSurface::<3, 2>::PAYLOAD_BYTES,
size_of::<[u16; 3]>() + size_of::<[u16; 2]>() + size_of::<[[i32; 3]; 2]>()
);
assert_eq!(<LinearAxis<3>>::KNOT_BYTES, size_of::<[u16; 3]>());
assert_eq!(<LinearAxis<3>>::INDEX_BYTES, 0);
assert_eq!(<BucketedAxis<5, 8>>::KNOT_BYTES, size_of::<[u16; 5]>());
assert_eq!(<BucketedAxis<5, 8>>::INDEX_BYTES, size_of::<[u16; 8]>());
assert_eq!(<UniformAxis<3, 0, 50>>::KNOT_BYTES, 0);
assert_eq!(<UniformAxis<3, 0, 50>>::INDEX_BYTES, 0);
}
#[test]
fn uniform_uniform_payload_is_only_the_grid() {
type UniformUniform = BilinearSurface<2, 2, UniformAxis<2, 0, 10>, UniformAxis<2, 0, 20>>;
assert_eq!(UniformUniform::PAYLOAD_BYTES, UniformUniform::VALUE_BYTES);
assert_eq!(UniformUniform::PAYLOAD_BYTES, size_of::<[[i32; 2]; 2]>());
assert_eq!(UniformUniform::VALUE_BYTES, 16);
}
#[test]
fn handle_bytes_matches_size_of_self() {
assert_eq!(
BilinearSurface::<2, 2>::HANDLE_BYTES,
size_of::<BilinearSurface<2, 2>>()
);
assert_eq!(
BilinearSurface::<64, 64>::HANDLE_BYTES,
size_of::<BilinearSurface<64, 64>>()
);
type Mixed = BilinearSurface<2, 2, LinearAxis<2>, UniformAxis<2, 0, 20>>;
assert_eq!(Mixed::HANDLE_BYTES, size_of::<Mixed>());
}
#[test]
fn success_work_constants_are_three_interpolations_and_four_reads() {
assert_eq!(BilinearSurface::<2, 2>::SUCCESS_INTERPOLATIONS, 3);
assert_eq!(BilinearSurface::<2, 2>::SUCCESS_GRID_READS, 4);
type Mixed = BilinearSurface<5, 3, BucketedAxis<5, 8>, UniformAxis<3, 0, 50>>;
assert_eq!(Mixed::SUCCESS_INTERPOLATIONS, 3);
assert_eq!(Mixed::SUCCESS_GRID_READS, 4);
}
#[test]
fn default_binary_payload_matches_the_documented_formula() {
assert_eq!(
BilinearSurface::<5, 4>::PAYLOAD_BYTES,
2 * 5 + 2 * 4 + 4 * 5 * 4
);
assert_eq!(BilinearSurface::<5, 4>::PAYLOAD_BYTES, 98);
assert_eq!(BilinearSurface::<5, 4>::VALUE_BYTES, 80);
assert_eq!(
BilinearSurface::<2, 2>::PAYLOAD_BYTES,
documented_payload(2, 2)
);
}
#[test]
fn mixed_bucketed_uniform_payload_drops_the_uniform_knots() {
static X: [u16; 17] = [
0, 100, 210, 300, 405, 500, 610, 700, 805, 900, 1_010, 1_100, 1_205, 1_300, 1_410,
1_500, 1_600,
];
static X_INDEX: [u16; 8] = bucket_index(&X);
type Mixed = BilinearSurface<17, 9, BucketedAxis<17, 8>, UniformAxis<9, 0, 200>>;
type AllBinary = BilinearSurface<17, 9>;
assert_eq!(Mixed::VALUE_BYTES, 612);
assert_eq!(Mixed::PAYLOAD_BYTES, 34 + 16 + 612);
assert_eq!(Mixed::PAYLOAD_BYTES, 662);
assert_eq!(AllBinary::PAYLOAD_BYTES, 664);
assert_eq!(max_local_comparisons(&X, &X_INDEX), 3);
assert_eq!(<BinaryAxis<17>>::MAX_SEARCH_COMPARISONS, 5);
}
#[test]
fn referenced_payload_matches_the_documented_formula() {
assert_eq!(referenced_payload::<2, 2>(), documented_payload(2, 2));
assert_eq!(referenced_payload::<2, 2>(), 24);
assert_eq!(referenced_payload::<3, 2>(), documented_payload(3, 2));
assert_eq!(referenced_payload::<3, 2>(), 34);
assert_eq!(referenced_payload::<2, 3>(), documented_payload(2, 3));
assert_eq!(referenced_payload::<16, 8>(), documented_payload(16, 8));
assert_eq!(referenced_payload::<64, 64>(), documented_payload(64, 64));
assert_eq!(referenced_payload::<64, 64>(), 16_640);
let measured = size_of_val(SURFACE.x_axis())
+ size_of_val(SURFACE.y_axis())
+ size_of_val(SURFACE.values());
assert_eq!(measured, documented_payload(SURFACE.nx(), SURFACE.ny()));
let measured =
size_of_val(WIDE.x_axis()) + size_of_val(WIDE.y_axis()) + size_of_val(WIDE.values());
assert_eq!(measured, documented_payload(WIDE.nx(), WIDE.ny()));
}
#[test]
fn default_handle_is_three_references_plus_four_policy_bytes_and_padding() {
let reference = size_of::<&'static [u16; 2]>();
assert_eq!(
reference,
size_of::<usize>(),
"a reference to a sized array is thin"
);
assert_eq!(size_of::<&'static [[i32; 2]; 2]>(), reference);
let fields = 3 * reference + size_of::<BoundaryPolicy>();
let handle = size_of::<BilinearSurface<2, 2>>();
let align = align_of::<BilinearSurface<2, 2>>();
assert_eq!(size_of::<BoundaryPolicy>(), 4);
assert!(
handle >= fields,
"handle {handle} smaller than its fields {fields}"
);
assert!(
handle < fields + align,
"handle {handle} carries more than alignment padding over {fields}"
);
assert_eq!(handle % align, 0);
}
fn assert_handle_layout<T>(references: usize) {
let fields = references * size_of::<usize>() + size_of::<BoundaryPolicy>();
let handle = size_of::<T>();
let align = align_of::<T>();
assert!(
handle >= fields,
"handle {handle} smaller than its fields {fields}"
);
assert!(
handle < fields + align,
"handle {handle} carries more than alignment padding over {fields}"
);
assert_eq!(handle % align, 0);
}
#[test]
fn handle_reference_count_follows_the_selected_strategies() {
type UniformUniform = BilinearSurface<2, 2, UniformAxis<2, 0, 10>, UniformAxis<2, 0, 20>>;
type LinearUniform = BilinearSurface<2, 2, LinearAxis<2>, UniformAxis<2, 0, 20>>;
type BinaryBinary = BilinearSurface<2, 2, BinaryAxis<2>, BinaryAxis<2>>;
type BucketedBucketed = BilinearSurface<2, 2, BucketedAxis<2, 1>, BucketedAxis<2, 1>>;
assert_handle_layout::<UniformUniform>(1);
assert_handle_layout::<LinearUniform>(2);
assert_handle_layout::<BinaryBinary>(3);
assert_handle_layout::<BucketedBucketed>(5);
}
#[test]
fn selecting_strategies_adds_no_discriminant_to_the_handle() {
assert_eq!(
size_of::<BilinearSurface<2, 2>>(),
size_of::<BilinearSurface<2, 2, BinaryAxis<2>, BinaryAxis<2>>>()
);
assert!(
size_of::<BilinearSurface<2, 2, LinearAxis<2>, UniformAxis<2, 0, 20>>>()
< size_of::<BilinearSurface<2, 2>>()
);
}
#[test]
fn with_policy_keeps_the_declared_tables() {
let clamped = SURFACE.with_policy(BoundaryPolicy::new().with_x_below(Boundary::Clamp));
assert!(core::ptr::eq(clamped.x_axis(), &X2));
assert!(core::ptr::eq(clamped.y_axis(), &Y2));
assert!(core::ptr::eq(clamped.values(), &V22));
assert_eq!(clamped.policy().x_below(), Boundary::Clamp);
}
#[test]
fn with_policy_keeps_the_declared_strategies() {
let clamped = MIXED.with_policy(policy_from_bits(0b1111));
assert!(core::ptr::eq(clamped.x_axis(), &X2));
assert_eq!(clamped.y_max(), 20);
assert_eq!(clamped.policy(), policy_from_bits(0b1111));
}
#[test]
fn all_sixteen_boundary_combinations_are_representable() {
for (bits, surface) in SURFACES.iter().enumerate() {
let policy = surface.policy();
assert_eq!(policy.x_below(), side_from_bit(bits, 0));
assert_eq!(policy.x_above(), side_from_bit(bits, 1));
assert_eq!(policy.y_below(), side_from_bit(bits, 2));
assert_eq!(policy.y_above(), side_from_bit(bits, 3));
assert!(core::ptr::eq(surface.values(), &V22));
assert!(core::ptr::eq(surface.x_axis(), &X2));
assert!(core::ptr::eq(surface.y_axis(), &Y2));
}
}
#[test]
fn the_sixteen_combinations_are_pairwise_distinct() {
for (i, left) in POLICIES.iter().enumerate() {
for (j, right) in POLICIES.iter().enumerate() {
if i == j {
assert_eq!(left, right);
} else {
assert_ne!(left, right);
}
}
}
}
#[test]
#[should_panic(expected = "x axis must declare at least two knots")]
fn zero_knot_x_axis_is_rejected() {
static X0: [u16; 0] = [];
static V20: [[i32; 0]; 2] = [[], []];
let _ = BilinearSurface::new(&X0, &Y2, &V20);
}
#[test]
#[should_panic(expected = "x axis must declare at least two knots")]
fn one_knot_x_axis_is_rejected() {
static X1: [u16; 1] = [3];
static V21: [[i32; 1]; 2] = [[0], [1]];
let _ = BilinearSurface::new(&X1, &Y2, &V21);
}
#[test]
#[should_panic(expected = "y axis must declare at least two knots")]
fn zero_knot_y_axis_is_rejected() {
static Y0: [u16; 0] = [];
static V02: [[i32; 2]; 0] = [];
let _ = BilinearSurface::new(&X2, &Y0, &V02);
}
#[test]
#[should_panic(expected = "y axis must declare at least two knots")]
fn one_knot_y_axis_is_rejected() {
static Y1: [u16; 1] = [3];
static V12: [[i32; 2]; 1] = [[0, 1]];
let _ = BilinearSurface::new(&X2, &Y1, &V12);
}
#[test]
#[should_panic(expected = "x axis knots must be strictly increasing")]
fn duplicate_x_knots_are_rejected() {
static DUPLICATE: [u16; 2] = [5, 5];
let _ = BilinearSurface::new(&DUPLICATE, &Y2, &V22);
}
#[test]
#[should_panic(expected = "x axis knots must be strictly increasing")]
fn descending_x_knots_are_rejected() {
static DESCENDING: [u16; 2] = [10, 0];
let _ = BilinearSurface::new(&DESCENDING, &Y2, &V22);
}
#[test]
#[should_panic(expected = "y axis knots must be strictly increasing")]
fn duplicate_y_knots_are_rejected() {
static DUPLICATE: [u16; 2] = [5, 5];
let _ = BilinearSurface::new(&X2, &DUPLICATE, &V22);
}
#[test]
#[should_panic(expected = "y axis knots must be strictly increasing")]
fn descending_y_knots_are_rejected() {
static DESCENDING: [u16; 2] = [20, 0];
let _ = BilinearSurface::new(&X2, &DESCENDING, &V22);
}
#[test]
#[should_panic(expected = "y axis knots must be strictly increasing")]
fn a_valid_x_axis_does_not_mask_an_invalid_y_axis() {
static DESCENDING: [u16; 2] = [900, 7];
let _ = BilinearSurface::new(&X3, &DESCENDING, &V23);
}
#[test]
#[should_panic(expected = "x axis knots must be strictly increasing")]
fn a_descending_interior_x_step_is_rejected() {
static NOT_SORTED: [u16; 3] = [0, 100, 50];
let _ = BilinearSurface::new(&NOT_SORTED, &Y2B, &V23);
}
}