bio/data_structures/interpolation_table.rs
1// Copyright 2018 Johannes Köster.
2// Licensed under the MIT license (http://opensource.org/licenses/MIT)
3// This file may not be copied, modified, or distributed
4// except according to those terms.
5
6//! Fast lookup table for arbitrary floating point functions.
7//! # Examples
8//! ## Easy:
9//! ```
10//! use bio::data_structures::interpolation_table::*;
11//!
12//! let table = InterpolationTable::new(0.0, 10.0, 5, |x| x.powf(2.0));
13//! assert_eq!(table.get(3.0), 9.0);
14//! assert_eq!(table.get(5.0), 25.0);
15//! ```
16//! ## More complicated:
17//! ```
18//! extern crate approx;
19//! fn main() {
20//! use bio::data_structures::interpolation_table::*;
21//! use approx::assert_relative_eq;
22//!
23//! let table = InterpolationTable::new(0.0, 10.0, 5, |x| x.ln_1p());
24//! for &x in &[0.02, 0.04, 0.45678686, 0.23875, 1.45345e-6] {
25//! assert_relative_eq!(table.get(x), x.ln_1p(), epsilon = 0.00001);
26//! }
27//! }
28
29#[inline]
30pub fn interpolate(a: f64, b: f64, fraction: f64) -> f64 {
31 a * (1.0 - fraction) + b * fraction
32}
33
34/// Fast lookup table for arbitrary floating point functions.
35/// This can be used to e.g., provide fast lookups of distribution values.
36/// Input values are sampled with a given precision and results are stored in a vector.
37/// During lookup, infimum and supremum of a given value are calculated and the result is
38/// interpolated.
39#[derive(Default, Clone, PartialEq, PartialOrd, Debug, Serialize, Deserialize)]
40pub struct InterpolationTable<F: Fn(f64) -> f64> {
41 inner: Vec<f64>,
42 func: F,
43 offset: usize,
44 min_x: f64,
45 max_x: f64,
46 shift: f64,
47}
48
49impl<F: Fn(f64) -> f64> InterpolationTable<F> {
50 /// Create a new `InterpolationTable`.
51 ///
52 /// # Arguments
53 ///
54 /// * `min_x` - minimum sample value
55 /// * `max_x` - maximum sample value
56 /// * `frac_digits` - number of fraction digits to store in sample
57 /// * `func` - Function to emulate.
58 ///
59 /// If given value is outside of min_x and max_x, the lookup falls back to applying the
60 /// function itself.
61 /// The table size grows with the number of fraction digits.
62 /// Space Complexity: O(m * 10^n), where `m = max_x - min_x` and `n = frac_digits`
63 pub fn new(min_x: f64, max_x: f64, frac_digits: i32, func: F) -> Self {
64 let shift = 10.0_f64.powi(frac_digits);
65 let offset = (min_x * shift) as usize;
66 let mut table = InterpolationTable {
67 inner: Vec::new(),
68 func,
69 min_x,
70 max_x,
71 shift,
72 offset,
73 };
74
75 for i in table.index(min_x)..table.index(max_x) {
76 let x = i as f64 / shift;
77 table.inner.push((table.func)(x));
78 }
79
80 table
81 }
82
83 /// Return lower bound index for given f64.
84 #[inline]
85 fn index(&self, x: f64) -> usize {
86 (x * self.shift) as usize - self.offset
87 }
88
89 /// Lookup given value in table, and interpolate the result between the sampled values if
90 /// necessary. This provides an approximation that is better the more fraction digits are
91 /// used to generate this table.
92 /// Time Complexity for lookup: O(1) if `min_x <= x < max_x` and O(func(x)) otherwise.
93 pub fn get(&self, x: f64) -> f64 {
94 if x < self.min_x || x >= self.max_x {
95 (self.func)(x)
96 } else {
97 let i = self.index(x);
98 // interpolate
99 let fraction = (x * self.shift - i as f64) / self.shift;
100 interpolate(self.inner[i], self.inner[i + 1], fraction)
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_interpolation_table() {
111 let table = InterpolationTable::new(0.0, 10.0, 5, |x| x.ln_1p());
112
113 for &x in &[0.02, 0.04, 0.45678686, 0.23875, 1.45345e-6] {
114 assert_relative_eq!(table.get(x), x.ln_1p(), epsilon = 0.00001);
115 }
116 }
117}