ndarray_interp/
vector_extensions.rs

1use ndarray::{ArrayBase, Data, Ix1};
2use std::fmt::Debug;
3
4/// Helper methods for one dimensional numeric arrays
5pub trait VectorExtensions<T> {
6    /// get the monotonic property of the vector
7    fn monotonic_prop(&self) -> Monotonic;
8
9    /// Get the index of the next lower value inside the vector.
10    /// This is not guaranteed to return the index of an exact match.
11    ///
12    /// This will never return the last index of the vector.
13    /// when x is out of bounds it will either return `0` or `self.len() - 2`
14    /// depending on which side it is out of bounds
15    ///
16    /// # Warning
17    /// this method requires the [`monotonic_prop`](VectorExtensions::monotonic_prop) to be
18    /// `Monotonic::Rising { strict: true }`
19    /// otherwise the behaviour is undefined
20    fn get_lower_index(&self, x: T) -> usize;
21}
22
23/// Describes the monotonic property of a vector
24#[derive(Debug)]
25pub enum Monotonic {
26    Rising { strict: bool },
27    Falling { strict: bool },
28    NotMonotonic,
29}
30use num_traits::{cast, Num, NumCast};
31use Monotonic::*;
32
33use crate::interp1d::Linear;
34
35impl<S> VectorExtensions<S::Elem> for ArrayBase<S, Ix1>
36where
37    S: Data,
38    S::Elem: Debug + PartialOrd + Num + NumCast + Copy,
39{
40    fn monotonic_prop(&self) -> Monotonic {
41        if self.len() <= 1 {
42            return NotMonotonic;
43        };
44
45        self.windows(2)
46            .into_iter()
47            .try_fold(MonotonicState::start(), |state, items| {
48                let a = items[0];
49                let b = items[1];
50                state.update(a, b).short_circuit()
51            })
52            .map_or_else(|mon| mon, |state| state.finish())
53    }
54
55    fn get_lower_index(&self, x: S::Elem) -> usize {
56        // the vector should be strictly monotonic rising, otherwise we will
57        // produce grabage
58
59        // check in range, otherwise return the first or second last index
60        // this allows for extrapolation
61        if x <= self[0] {
62            return 0;
63        }
64        if x >= self[self.len() - 1] {
65            return self.len() - 2;
66        }
67
68        // We assume that the spacing is even. So we can calculate the index
69        // and check it. This finishes in O(1) for even spaced axis.
70        let mut range = (0usize, self.len() - 1);
71        let p1 = (
72            self[range.0],
73            cast(range.0)
74                .unwrap_or_else(|| unimplemented!("casting from usize should always work!")),
75        );
76        let p2 = (
77            self[range.1],
78            cast(range.1)
79                .unwrap_or_else(|| unimplemented!("casting from usize should always work!")),
80        );
81
82        let mid = Linear::calc_frac(p1, p2, x);
83        let mid_idx: usize =
84            cast(mid).unwrap_or_else(|| unimplemented!("failed to convert {mid:?} to usize"));
85
86        let mid_x = self[mid_idx];
87
88        if mid_x <= x && x < self[mid_idx + 1] {
89            return mid_idx;
90        }
91        if mid_x <= x {
92            // why is this faster than `mid_x < x`???
93            range.0 = mid_idx;
94        } else {
95            range.1 = mid_idx;
96        }
97
98        // the spacing was apparently not even, do binary search
99        // O(log n)
100        while range.0 + 1 < range.1 {
101            let mid_idx = (range.1 - range.0) / 2 + range.0;
102            let mid_x = self[mid_idx];
103
104            if mid_x <= x {
105                range.0 = mid_idx;
106            } else {
107                range.1 = mid_idx;
108            }
109        }
110        range.0
111    }
112}
113
114/// A State Machine for finding the monotonic property of an array
115#[derive(Debug)]
116enum MonotonicState {
117    Init,
118    NotStrict,
119    Likely(Monotonic),
120}
121
122impl MonotonicState {
123    /// start the state machine
124    fn start() -> Self {
125        Self::Init
126    }
127
128    /// update the state machine with two consecutive values from the vector
129    fn update<T>(self, a: T, b: T) -> Self
130    where
131        T: PartialOrd,
132    {
133        use MonotonicState::*;
134        match self {
135            Init => {
136                if a < b {
137                    Likely(Rising { strict: true })
138                } else if a == b {
139                    NotStrict
140                } else {
141                    Likely(Falling { strict: true })
142                }
143            }
144            NotStrict => {
145                if a < b {
146                    Likely(Rising { strict: false })
147                } else if a == b {
148                    NotStrict
149                } else {
150                    Likely(Falling { strict: false })
151                }
152            }
153            Likely(Rising { strict }) => {
154                if a == b {
155                    Likely(Rising { strict: false })
156                } else if a < b {
157                    Likely(Rising { strict })
158                } else {
159                    Likely(NotMonotonic)
160                }
161            }
162            Likely(Falling { strict }) => {
163                if a == b {
164                    Likely(Falling { strict: false })
165                } else if a > b {
166                    Likely(Falling { strict })
167                } else {
168                    Likely(NotMonotonic)
169                }
170            }
171            Likely(NotMonotonic) => Likely(NotMonotonic),
172        }
173    }
174
175    /// return `Err(Monotonic)` when the state machine can no longer change its state
176    /// otherwise retruns `Ok(self)`
177    ///
178    /// This can be used with the [`Iterator::try_fold()`] method short-circuiting
179    /// the iterator.
180    fn short_circuit(self) -> Result<Self, Monotonic> {
181        match self {
182            MonotonicState::Likely(NotMonotonic) => Err(NotMonotonic),
183            _ => Ok(self),
184        }
185    }
186
187    /// get the final value of the state machine
188    ///
189    /// # panics
190    /// when the state machine is still in the `Init` State
191    fn finish(self) -> Monotonic {
192        match self {
193            MonotonicState::Init => panic!("`MonotonicState::update` was never called"),
194            MonotonicState::NotStrict => NotMonotonic,
195            MonotonicState::Likely(mon) => mon,
196        }
197    }
198}
199
200#[cfg(test)]
201mod test {
202    use ndarray::{array, s, Array, Array1};
203
204    use super::{Monotonic, VectorExtensions};
205
206    macro_rules! test_index {
207        ($i:expr, $q:expr) => {
208            let data = Array::linspace(0.0, 10.0, 11);
209            assert_eq!($i, data.get_lower_index($q));
210        };
211        ($i:expr, $q:expr, exp) => {
212            let data = Array::from_iter((0..11).map(|x| 2f64.powi(x)));
213            assert_eq!($i, data.get_lower_index($q));
214        };
215        ($i:expr, $q:expr, ln) => {
216            let data = Array::from_iter((0..11).map(|x| (x as f64).ln_1p()));
217            assert_eq!($i, data.get_lower_index($q));
218        };
219    }
220
221    #[test]
222    fn test_outside_left() {
223        test_index!(0, -1.0);
224    }
225
226    #[test]
227    fn test_outside_right() {
228        test_index!(9, 25.0);
229    }
230
231    #[test]
232    fn test_left_border() {
233        test_index!(0, 0.0);
234    }
235
236    #[test]
237    fn test_right_border() {
238        test_index!(9, 10.0);
239    }
240
241    #[test]
242    fn test_exact_index() {
243        for i in 0..10 {
244            test_index!(i, i as f64);
245        }
246    }
247
248    #[test]
249    fn test_index() {
250        for mut i in 0..100usize {
251            let q = i as f64 / 10.0;
252            i /= 10;
253            test_index!(i, q);
254        }
255    }
256
257    #[test]
258    fn test_pos_inf_index() {
259        test_index!(9, f64::INFINITY);
260    }
261
262    #[test]
263    fn test_neg_inf_index() {
264        test_index!(0, f64::NEG_INFINITY);
265    }
266
267    #[test]
268    #[should_panic(expected = "not implemented: failed to convert NaN to usize")]
269    fn test_nan() {
270        test_index!(0, f64::NAN);
271    }
272
273    #[test]
274    fn test_exponential_exact_index() {
275        for (i, q) in (0..10).map(|x| (x as usize, 2f64.powi(x))) {
276            test_index!(i, q, exp);
277        }
278    }
279
280    #[test]
281    fn test_exponential_index() {
282        for (i, q) in (0..100usize).map(|x| (x / 10, 2f64.powf(x as f64 / 10.0))) {
283            test_index!(i, q, exp);
284        }
285    }
286
287    #[test]
288    fn test_exponential_right_border() {
289        test_index!(9, 1024.0, exp);
290    }
291
292    #[test]
293    fn test_exponential_left_border() {
294        test_index!(0, 1.0, exp);
295    }
296
297    #[test]
298    fn test_log() {
299        for (i, q) in (0..100usize).map(|x| (x / 10, (x as f64 / 10.0).ln_1p())) {
300            test_index!(i, q, ln);
301        }
302    }
303
304    macro_rules! test_monotonic {
305        ($d:ident, $expected:pat) => {
306            match $d.monotonic_prop() {
307                $expected => (),
308                value => panic!("{}", format!("got {value:?}")),
309            };
310            match $d.slice(s![..;1]).monotonic_prop() {
311                $expected => (),
312                _ => panic!(),
313            };
314        };
315    }
316
317    // test with f64
318    #[test]
319    fn test_strict_monotonic_rising_f64() {
320        let data: Array1<f64> = array![1.1, 2.0, 3.123, 4.5];
321        test_monotonic!(data, Monotonic::Rising { strict: true });
322    }
323
324    #[test]
325    fn test_monotonic_rising_f64() {
326        let data: Array1<f64> = array![1.1, 2.0, 3.123, 3.123, 4.5];
327        test_monotonic!(data, Monotonic::Rising { strict: false });
328    }
329
330    #[test]
331    fn test_strict_monotonic_falling_f64() {
332        let data: Array1<f64> = array![5.8, 4.123, 3.1, 2.0, 1.0];
333        test_monotonic!(data, Monotonic::Falling { strict: true });
334    }
335
336    #[test]
337    fn test_monotonic_falling_f64() {
338        let data: Array1<f64> = array![5.8, 4.123, 3.1, 3.1, 2.0, 1.0];
339        test_monotonic!(data, Monotonic::Falling { strict: false });
340    }
341
342    #[test]
343    fn test_not_monotonic_f64() {
344        let data: Array1<f64> = array![1.1, 2.0, 3.123, 3.120, 4.5];
345        test_monotonic!(data, Monotonic::NotMonotonic);
346    }
347
348    // test with i32
349    #[test]
350    fn test_strict_monotonic_rising_i32() {
351        let data: Array1<i32> = array![1, 2, 3, 4, 5];
352        test_monotonic!(data, Monotonic::Rising { strict: true });
353    }
354
355    #[test]
356    fn test_monotonic_rising_i32() {
357        let data: Array1<i32> = array![1, 2, 3, 3, 4, 5];
358        test_monotonic!(data, Monotonic::Rising { strict: false });
359    }
360
361    #[test]
362    fn test_strict_monotonic_falling_i32() {
363        let data: Array1<i32> = array![5, 4, 3, 2, 1];
364        test_monotonic!(data, Monotonic::Falling { strict: true });
365    }
366
367    #[test]
368    fn test_monotonic_falling_i32() {
369        let data: Array1<i32> = array![5, 4, 3, 3, 2, 1];
370        test_monotonic!(data, Monotonic::Falling { strict: false });
371    }
372
373    #[test]
374    fn test_not_monotonic_i32() {
375        let data: Array1<i32> = array![1, 2, 3, 2, 4, 5];
376        test_monotonic!(data, Monotonic::NotMonotonic);
377    }
378
379    #[test]
380    fn test_ordered_view_on_unordred_array() {
381        let data: Array1<i32> = array![5, 4, 3, 2, 1];
382        let ordered = data.slice(s![..;-1]);
383        test_monotonic!(ordered, Monotonic::Rising { strict: true });
384    }
385
386    #[test]
387    fn test_starting_flat() {
388        let data: Array1<i32> = array![1, 1, 2, 3, 4, 5];
389        test_monotonic!(data, Monotonic::Rising { strict: false });
390    }
391
392    #[test]
393    fn test_flat() {
394        let data: Array1<i32> = array![1, 1, 1];
395        test_monotonic!(data, Monotonic::NotMonotonic);
396    }
397
398    #[test]
399    fn test_one_element_array() {
400        let data: Array1<i32> = array![1];
401        test_monotonic!(data, Monotonic::NotMonotonic);
402    }
403}