Skip to main content

makima_spline/
lib.rs

1/// Contains all the necessary information to sample a given point
2pub struct Spline{
3	/// Contains the positions of the data.
4	/// This is used to figure out which coefficients need to be used at a given position
5	x: Vec<f64>,
6
7	/// The spline is made of cubic polynomial segments with coefficients
8	/// A cubic Polynomial has 4 parameters and thus this is a vec containing 4 parameters for each segment
9	coefficients: Vec<(f64, f64, f64, f64)>
10}
11
12impl Spline{
13	/// Returns a Spline struct from a dataset
14	///
15	/// # Arguments
16	///
17	/// * `points` - A vec that holds each point `(f64, f64)` to be interpolated<br>
18	/// The first element in the tuple being the key that will be used for finding the right segment during sampling
19	///
20	/// # Example
21	///
22	/// ```
23	/// let spline = makima_spline::Spline::from_vec(vec![(1., 3.), (2., 5.), (3., 2.)]);
24	///  ``` 
25	///
26	/// # Panics
27	///
28	/// [`panic!`] when there is only one point in the vector. Make sure your vector contains at least 2 points to interpolate between
29	///
30	/// [`panic!`]: https://doc.rust-lang.org/std/macro.panic.html
31	pub fn from_vec(points: Vec<(f64, f64)>)->Spline{
32		assert!(points.len() > 1, "Only one point. No Interpolation possible");
33		let mut points = points;
34		points.sort_by(|(x0, _), (x1, _)| x0.partial_cmp(x1).unwrap());
35		// tangents
36		let mut m = Vec::<f64>::with_capacity(points.len()+3);
37		for i in 0..points.len()-1{
38			m.push((points[i+1].1 - points[i].1)/(points[i+1].0-points[i].0));
39		}
40		if m.len() == 1{
41			// this is just a line!
42			let p1 = points[0];
43			let p2 = points[1];
44			let coefficients =  vec![(p1.1, m[0], 0., 0.)];
45			return Spline{x: vec![p1.0, p2.0], coefficients}
46		}
47		// extrapolation at beginning
48		m.insert(0, 2.0*m[0]-m[1]);
49		m.insert(0, 2.0*m[0]-m[1]);
50		// extrapolation at end
51		m.push(2.0*m[m.len()-1]-m[m.len()-2]);
52		m.push(2.0*m[m.len()-1]-m[m.len()-2]);
53		let m = m; // make immutable
54
55		// derivatives at points 
56		let mut t= Vec::<f64>::with_capacity(points.len()-1);
57		for i in 0..points.len(){
58			let i = i + 2; // because of extrapolation
59			let w1 = (m[i+1]-m[i]).abs() + (m[i+1]+m[i]).abs()/2.;
60			let w2 = (m[i-1]-m[i-2]).abs() + (m[i-1]+m[i-2]).abs()/2.;
61			let nominator = w1*m[i-1] + w2*m[i];
62			if nominator == 0.0{
63				t.push(0.0)
64			}else{
65				t.push(nominator/(w1+w2))
66			}
67			
68		}
69		let t = t; // make immutable
70
71		// coefficients
72		let mut coefficients = Vec::<(f64, f64, f64, f64)>::with_capacity(points.len()-1);
73		for i in 0..points.len()-1{
74			let (xi, yi) = points[i];
75			let (xi1, _) = points[i+1];
76			let z = xi1-xi;
77			let a = yi;
78			let b = t[i];
79			let c = (3.*m[i+2] - 2.*t[i]-t[i+1])/z;
80			let d = (t[i]+t[i+1] - 2.*m[i+2])/z.powi(2);
81			coefficients.push((a, b, c, d));
82		}
83
84		// extract x
85		let x = points.iter().map(|p|{
86			p.0
87		}).collect();
88		Spline{x, coefficients}
89	}
90
91	///finds the segment and its parameters for interpolation
92	///
93	///the output is (a, b, c, d, xi) with xi the coordinate of the polynomial for dx
94	fn segment(&self, pos: f64) -> (f64, f64, f64, f64, f64){
95		if pos <= self.x[0]{
96			// extrapolate linear
97			let (a, b, _, _) = self.coefficients[0];
98			let xi = self.x[0];
99			return (a, b, 0.0, 0.0, xi);
100		}
101		if pos >= self.x[self.x.len()-1]{
102			// extrapolate linear
103			let (a, b, c, d) = self.coefficients[self.coefficients.len()-1];
104			let xi = self.x[self.x.len()-2];
105			let xi1 = self.x[self.x.len()-1];
106			let dx = xi1-xi; 
107			let y1 = a + b*dx + c*dx*dx + d*dx*dx*dx;
108			let m = b + 2.*c*dx + 3.*d*dx*dx;
109			return (y1, m, 0.0, 0.0, xi1);
110		}
111		//do binary search
112		let mut upper = self.x.len();
113		let mut lower = 0;
114		while upper-lower > 1{
115			let center = (upper+lower)/2;
116			if pos > self.x[center]{
117				lower = center;
118			}else{
119				upper = center;
120			}
121		}
122		let coef = self.coefficients[lower];
123		(coef.0, coef.1, coef.2, coef.3, self.x[lower])
124	}
125
126	///samples a point at a given position
127	///
128	/// # Example
129	/// 
130	/// ```
131	/// let spline = makima_spline::Spline::from_vec(vec![(1., 3.), (2., 5.), (3., 2.)]);
132	/// let sample: f64 = spline.sample(1.5);
133	/// //This even works outside the given data (extrapolation)
134	/// let sample2: f64 = spline.sample(-3.0);
135	/// ```
136	pub fn sample(&self, pos: f64)-> f64{
137		let (a, b, c, d, xi) = self.segment(pos);
138		
139		let dx = pos-xi;
140		a + b*dx + c*dx*dx + d*dx*dx*dx
141	}
142
143	/// returns the 1. order derivative at sampled point
144	pub fn derivative_1(&self, pos: f64) -> f64{
145		let (_, b, c, d, xi) = self.segment(pos);
146		let dx = pos-xi;
147		b + 2.*c*dx + 3.*d*dx*dx
148	}
149
150	/// returns the 2. order derivative at sampled point 
151	///
152	/// note, this doesn't have to be continous
153	pub fn derivative_2(&self, pos: f64) -> f64{
154		let (_, _, c, d, xi) = self.segment(pos);
155		let dx = pos-xi;
156		2.*c + 6.*d*dx
157	}
158
159	/// returns the 3. order derivative at sampled point 
160	///
161	/// note, this doesn't have to be continous
162	pub fn derivative_3(&self, pos: f64) -> f64{
163		let (_, _, _, d, _) = self.segment(pos);
164		6.0*d
165	}
166}
167
168/// converts two vecs of x & y coordinates into points (f64, f64) used to build the spline
169///
170/// # Example
171/// ```
172/// let x = vec![1., 2., 3.];
173/// let y = vec![3., 5., 2.];
174/// let points = makima_spline::vec_to_points(&x, &y);
175/// ```
176pub fn vec_to_points(x: &Vec<f64>, y: &Vec<f64>) -> Vec<(f64, f64)>{
177	assert!(x.len() == y.len());
178	let mut points = Vec::<(f64, f64)>::with_capacity(x.len());
179	for i in 0..x.len(){
180		points.push((x[i], y[i]))
181	}
182	points
183}
184
185#[cfg(feature = "n_dimensional")]
186pub mod n_dimensional;
187
188mod tests;