libreda_interp/
interp1d.rs

1// Copyright (c) 2021-2021 Thomas Kramer.
2// SPDX-FileCopyrightText: 2022 Thomas Kramer <code@tkramer.ch>
3//
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6//! One-dimensional linear interpolation.
7//!
8//! # Examples
9//! ```
10//! use interp::interp1d::Interp1D;
11//!
12//! let xs = vec![0.0, 1.0, 2.0];
13//! let zs = vec![0.0f64, 1.0, 0.0];
14//!
15//! let interp = Interp1D::new(xs, zs);
16//!
17//! assert!((interp.eval_no_extrapolation(1.0).unwrap() - 1.0).abs() < 1e-6);
18//! ```
19
20use crate::{find_closest_neighbours_indices, is_monotonic};
21use std::ops::{Add, Div, Mul, Sub};
22
23///
24/// * `C`: Coordinate type.
25/// * `Z`: Value type.
26#[derive(Debug, Clone)]
27pub struct Interp1D<C, Z> {
28    /// Index.
29    x: Vec<C>,
30    /// Samples.
31    z: Vec<Z>,
32}
33
34/// Interpolate between two values `x0` and `x1`.
35/// `alpha` should range from `0.0` to `1.0` for interpolation, otherwise
36/// the value is *extrapolated*.
37fn interpolate1d<C, Z>(z0: Z, z1: Z, alpha: C) -> Z
38where
39    C: Copy + Sub<Output = C>,
40    Z: Copy + Mul<C, Output = Z> + Add<Output = Z> + Sub<Output = Z>,
41{
42    z0 - z0 * alpha + z1 * alpha
43}
44
45#[test]
46fn test_interpolate1d() {
47    assert!((interpolate1d(1.0f64, 2., 0.) - 1.).abs() < 1e-6);
48    assert!((interpolate1d(1.0f64, 2., 1.) - 2.).abs() < 1e-6);
49    assert!((interpolate1d(1.0f64, 2., 0.5) - 1.5).abs() < 1e-6);
50}
51
52/// Find the value of `f(x)`
53/// given two sample values `vi = f(xi)` for all `i in [0, 1]`.
54fn interp1d<C, Z>(x: C, (x0, x1): (C, C), (v0, v1): (Z, Z)) -> Z
55where
56    C: Copy + Sub<Output = C> + Div,
57    Z: Copy + Mul<<C as Div>::Output, Output = Z> + Add<Output = Z> + Sub<Output = Z>,
58    <C as Div>::Output: Copy + Add<Output = <C as Div>::Output> + Sub<Output = <C as Div>::Output>,
59{
60    let dx = x1 - x0;
61
62    let alpha = (x - x0) / dx;
63
64    interpolate1d(v0, v1, alpha)
65}
66
67impl<C, Z> Interp1D<C, Z> {
68    /// Create a new interpolation engine.
69    ///
70    /// Interpolates values which are sampled on a rectangular grid.
71    ///
72    /// # Parameters
73    /// * `x`: The x-coordinates. Must be monotonic.
74    /// * `z`: The values `z(x)` for each grid point defined by the `x` coordinates.
75    ///
76    /// # Panics
77    /// Panics when
78    /// * dimensions of the indices and z-values don't match.
79    /// * one axis is empty.
80    /// * x values are not monotonic.
81    pub fn new(x: Vec<C>, z: Vec<Z>) -> Self
82    where
83        C: PartialOrd,
84    {
85        assert_eq!(z.len(), x.len(), "x-axis length mismatch.");
86        assert!(!x.is_empty());
87
88        assert!(is_monotonic(&x), "x values must be monotonic.");
89
90        Self { x, z }
91    }
92
93    /// Get the boundaries of the sample range
94    /// as `(x0, x1)` tuple.
95    pub fn bounds(&self) -> (C, C)
96    where
97        C: Copy,
98    {
99        (self.x[0], self.x[self.x.len() - 1])
100    }
101
102    /// Check if the `x` coordinate lies within the defined sample range.
103    pub fn is_within_bounds(&self, x: C) -> bool
104    where
105        C: PartialOrd + Copy,
106    {
107        let (x0, x1) = self.bounds();
108        x0 <= x && x <= x1
109    }
110
111    /// Get the x-coordinate values.
112    pub fn xs(&self) -> &Vec<C> {
113        &self.x
114    }
115
116    /// Get the raw z values.
117    pub fn z(&self) -> &Vec<Z> {
118        &self.z
119    }
120
121    /// Create a new interpolated table by applying a function elementwise to the values.
122    pub fn map_values<Z2>(&self, f: impl Fn(&Z) -> Z2) -> Interp1D<C, Z2>
123    where
124        C: PartialOrd + Clone,
125    {
126        Interp1D {
127            x: self.x.clone(),
128            z: self.z.iter().map(f).collect(),
129        }
130    }
131
132    /// Apply a function to the coordinates values of the axis.
133    /// Panics if the new coordinates are not monotonically increasing.
134    pub fn map_axis<Xnew>(self, f: impl Fn(C) -> Xnew) -> Interp1D<Xnew, Z>
135    where
136        Xnew: PartialOrd,
137    {
138        let xnew = self.x.into_iter().map(f).collect();
139        assert!(is_monotonic(&xnew));
140        Interp1D { x: xnew, z: self.z }
141    }
142}
143
144impl<C, Z> Interp1D<C, Z>
145where
146    C: Copy + Sub<Output = C> + Div + PartialOrd,
147    Z: Copy + Mul<<C as Div>::Output, Output = Z> + Add<Output = Z> + Sub<Output = Z>,
148    <C as Div>::Output: Copy + Add<Output = <C as Div>::Output> + Sub<Output = <C as Div>::Output>,
149{
150    /// Evaluate the sampled function by interpolation at `x`.
151    ///
152    /// If `x` lies out of the sampled range then the function is silently *extrapolated*.
153    /// The boundaries of the sample range can be queried with `bounds()`.
154    pub fn eval(&self, x: C) -> Z {
155        // Find closest grid points.
156
157        let (x0, x1) = find_closest_neighbours_indices(&self.x, x);
158
159        interp1d(x, (self.x[x0], self.x[x1]), (self.z[x0], self.z[x1]))
160    }
161
162    /// Returns the same value as `eval()` as long as `x` is within the
163    /// range of the samples. Otherwise `None` is returned instead of an extrapolation.
164    pub fn eval_no_extrapolation(&self, x: C) -> Option<Z> {
165        if self.is_within_bounds(x) {
166            Some(self.eval(x))
167        } else {
168            None
169        }
170    }
171}
172
173#[test]
174fn test_interp1d() {
175    let xs = vec![0.0f64, 1.0, 2.0];
176    let zs = vec![0.0, 1.0, 0.0];
177
178    let interp = Interp1D::new(xs, zs);
179
180    assert!((interp.eval_no_extrapolation(1.0).unwrap() - 1.0).abs() < 1e-6);
181    assert!((interp.eval_no_extrapolation(2.0).unwrap() - 0.0).abs() < 1e-6);
182    assert!((interp.eval_no_extrapolation(1.5).unwrap() - 0.5).abs() < 1e-6);
183}
184
185#[test]
186fn test_interp1d_map_values() {
187    let xs = vec![0.0f64, 1.0, 2.0];
188    let zs = vec![0.0, 1.0, 0.0];
189
190    let interp = Interp1D::new(xs, zs);
191    let interp = interp.map_values(|x| 2. * x);
192
193    assert!((interp.eval_no_extrapolation(1.0).unwrap() - 2.0).abs() < 1e-6);
194}
195
196#[test]
197fn test_interp1d_map_axis() {
198    let xs = vec![0.0f64, 1.0, 2.0];
199    let zs = vec![0.0, 1.0, 0.0];
200
201    let interp = Interp1D::new(xs, zs);
202    let interp = interp.map_axis(|x| 2. * x);
203
204    assert!((interp.eval_no_extrapolation(2.0).unwrap() - 1.0).abs() < 1e-6);
205}