ph_surfaces/axis/
linear.rs1use super::{AxisLookup, KnotArray, assert_valid_knots, sealed};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub struct LinearAxis<const N: usize> {
52 knots: &'static [u16; N],
53}
54
55impl<const N: usize> LinearAxis<N> {
56 #[must_use]
63 pub const fn new(knots: &'static [u16; N]) -> Self {
64 assert_valid_knots(knots);
65
66 Self { knots }
67 }
68
69 #[must_use]
73 pub const fn knots(&self) -> &'static [u16; N] {
74 self.knots
75 }
76}
77
78impl<const N: usize> sealed::Sealed<N> for LinearAxis<N> {
79 #[inline(always)]
80 fn search_in_domain(&self, coordinate: u16) -> (usize, u32) {
81 debug_assert!(
82 self.knots[0] <= coordinate && coordinate <= self.knots[N - 1],
83 "the sealed search is only called on an in-domain coordinate"
84 );
85
86 let mut index = 0;
87 let mut comparisons = 0;
88
89 while index + 1 < N {
93 comparisons += 1;
94 if self.knots[index + 1] > coordinate {
95 break;
96 }
97 index += 1;
98 }
99
100 debug_assert!(
101 comparisons <= <Self as AxisLookup<N>>::MAX_SEARCH_COMPARISONS,
102 "the scan must stay inside the documented bound"
103 );
104 debug_assert!(
105 self.knots[index] <= coordinate,
106 "the located knot must not sit above the coordinate"
107 );
108
109 (index, comparisons)
110 }
111}
112
113impl<const N: usize> KnotArray<N> for LinearAxis<N> {
114 fn knots(&self) -> &'static [u16; N] {
115 self.knots
116 }
117}
118
119impl<const N: usize> AxisLookup<N> for LinearAxis<N> {
120 const KNOT_BYTES: usize = 2 * N;
121 const INDEX_BYTES: usize = 0;
122 const MAX_SEARCH_COMPARISONS: u32 = (N - 1) as u32;
125
126 fn first(&self) -> u16 {
127 self.knots[0]
128 }
129
130 fn last(&self) -> u16 {
131 self.knots[N - 1]
132 }
133
134 fn knot(&self, index: usize) -> u16 {
135 self.knots[index]
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::LinearAxis;
142 use crate::axis::{AxisLookup, BinaryAxis, KnotArray, probes};
143
144 static X_TINY: [u16; 2] = [7, 9];
145 static X_MAIN: [u16; 5] = [10, 20, 30, 40, 50];
146 static X_SPARSE: [u16; 6] = [3, 4, 5, 1_000, 40_000, 65_000];
147 static X_FULL: [u16; 4] = [0, 1, 32_768, 65_535];
148
149 const TINY: LinearAxis<2> = LinearAxis::new(&X_TINY);
150 const MAIN: LinearAxis<5> = LinearAxis::new(&X_MAIN);
151 const SPARSE: LinearAxis<6> = LinearAxis::new(&X_SPARSE);
152 const FULL: LinearAxis<4> = LinearAxis::new(&X_FULL);
153
154 #[test]
155 fn the_scan_locates_the_same_index_as_the_binary_search() {
156 macro_rules! agrees_with_binary {
157 ($linear:expr, $knots:expr, $stride:expr) => {
158 let linear = $linear;
159 let binary = BinaryAxis::new($knots);
160
161 for coordinate in probes($knots, $stride) {
162 assert_eq!(
163 linear.search(coordinate).0,
164 binary.search(coordinate).0,
165 "at {coordinate}"
166 );
167 }
168 };
169 }
170
171 agrees_with_binary!(TINY, &X_TINY, 1);
172 agrees_with_binary!(MAIN, &X_MAIN, 1);
173 agrees_with_binary!(SPARSE, &X_SPARSE, 97);
174 agrees_with_binary!(FULL, &X_FULL, 97);
175 }
176
177 #[test]
178 fn every_knot_locates_itself() {
179 for (index, &knot) in X_MAIN.iter().enumerate() {
180 assert_eq!(MAIN.search(knot).0, index, "at knot {knot}");
181 }
182 for (index, &knot) in X_SPARSE.iter().enumerate() {
183 assert_eq!(SPARSE.search(knot).0, index, "at knot {knot}");
184 }
185 }
186
187 #[test]
188 fn the_scan_never_exceeds_its_declared_bound() {
189 assert_eq!(<LinearAxis<2>>::MAX_SEARCH_COMPARISONS, 1);
190 assert_eq!(<LinearAxis<5>>::MAX_SEARCH_COMPARISONS, 4);
191 assert_eq!(<LinearAxis<6>>::MAX_SEARCH_COMPARISONS, 5);
192
193 for coordinate in 10u16..=50 {
194 assert!(MAIN.search(coordinate).1 <= <LinearAxis<5>>::MAX_SEARCH_COMPARISONS);
195 }
196
197 assert_eq!(MAIN.search(50).1, 4);
200 assert_eq!(MAIN.search(10).1, 1);
201 assert_eq!(TINY.search(9).1, 1);
202 }
203
204 #[test]
205 fn the_knot_array_is_referenced_and_never_copied() {
206 assert!(core::ptr::eq(MAIN.knots(), &X_MAIN));
207 assert!(core::ptr::eq(KnotArray::knots(&MAIN), &X_MAIN));
208 assert_eq!(<LinearAxis<5>>::KNOT_BYTES, 10);
209 assert_eq!(<LinearAxis<5>>::INDEX_BYTES, 0);
210 }
211
212 #[test]
213 #[should_panic(expected = "an axis must declare at least two knots")]
214 fn a_one_knot_axis_is_rejected() {
215 static ONE: [u16; 1] = [3];
216
217 let _ = LinearAxis::new(&ONE);
218 }
219
220 #[test]
221 #[should_panic(expected = "axis knots must be strictly increasing")]
222 fn a_descending_knot_is_rejected() {
223 static DESCENDING: [u16; 3] = [0, 100, 50];
224
225 let _ = LinearAxis::new(&DESCENDING);
226 }
227}