1struct BandMatrix {
14 dim: usize,
15 upper: [Vec<f64>; 2],
17 lower: [Vec<f64>; 2],
19}
20
21impl BandMatrix {
22 fn new(dim: usize) -> Self {
23 Self {
24 dim,
25 upper: [vec![0.0; dim], vec![0.0; dim]],
26 lower: [vec![0.0; dim], vec![0.0; dim]],
27 }
28 }
29
30 #[inline]
32 fn get(&self, i: usize, j: usize) -> f64 {
33 let k = j as isize - i as isize;
34 if k >= 0 {
35 self.upper[k as usize][i]
36 } else {
37 self.lower[(-k) as usize][i]
38 }
39 }
40
41 #[inline]
42 fn set(&mut self, i: usize, j: usize, v: f64) {
43 let k = j as isize - i as isize;
44 if k >= 0 {
45 self.upper[k as usize][i] = v;
46 } else {
47 self.lower[(-k) as usize][i] = v;
48 }
49 }
50
51 #[inline]
52 fn saved_diag(&self, i: usize) -> f64 {
53 self.lower[0][i]
54 }
55 #[inline]
56 fn set_saved_diag(&mut self, i: usize, v: f64) {
57 self.lower[0][i] = v;
58 }
59
60 fn lu_decompose(&mut self) {
62 let dim = self.dim as isize;
63 for i in 0..self.dim {
65 let diag = self.get(i, i);
66 self.set_saved_diag(i, 1.0 / diag);
67 let j_min = (i as isize - 1).max(0) as usize;
68 let j_max = (i + 1).min(self.dim - 1);
69 let s = self.saved_diag(i);
70 for j in j_min..=j_max {
71 self.set(i, j, self.get(i, j) * s);
72 }
73 self.set(i, i, 1.0); }
75 for k in 0..self.dim {
77 let i_max = ((k + 1).min(self.dim - 1)) as isize;
78 let mut i = k as isize + 1;
79 while i <= i_max {
80 let iu = i as usize;
81 let akk = self.get(k, k);
82 let x = -self.get(iu, k) / akk;
83 self.set(iu, k, -x); let j_max = ((k + 1).min(self.dim - 1)) as isize;
85 let mut j = k as isize + 1;
86 while j <= j_max {
87 let ju = j as usize;
88 self.set(iu, ju, self.get(iu, ju) + x * self.get(k, ju));
89 j += 1;
90 }
91 i += 1;
92 }
93 }
94 let _ = dim;
95 }
96
97 fn l_solve(&self, b: &[f64]) -> Vec<f64> {
99 let mut x = vec![0.0; self.dim];
100 for i in 0..self.dim {
101 let mut sum = 0.0;
102 let j_start = (i as isize - 1).max(0) as usize;
103 for j in j_start..i {
104 sum += self.get(i, j) * x[j];
105 }
106 x[i] = b[i] * self.saved_diag(i) - sum;
107 }
108 x
109 }
110
111 fn r_solve(&self, b: &[f64]) -> Vec<f64> {
113 let mut x = vec![0.0; self.dim];
114 for i in (0..self.dim).rev() {
115 let mut sum = 0.0;
116 let j_stop = (i + 1).min(self.dim - 1);
117 for j in (i + 1)..=j_stop {
118 if j > i {
119 sum += self.get(i, j) * x[j];
120 }
121 }
122 x[i] = (b[i] - sum) / self.get(i, i);
123 }
124 x
125 }
126
127 fn lu_solve(&mut self, b: &[f64]) -> Vec<f64> {
128 self.lu_decompose();
129 let y = self.l_solve(b);
130 self.r_solve(&y)
131 }
132}
133
134#[derive(Debug, Clone, Default)]
136pub struct Spline {
137 x: Vec<f64>,
138 y: Vec<f64>,
139 a: Vec<f64>,
140 b: Vec<f64>,
141 c: Vec<f64>,
142 b0: f64,
143 c0: f64,
144}
145
146impl Spline {
147 pub fn new(xs: Vec<f64>, ys: Vec<f64>) -> Self {
150 let n = xs.len();
151 assert!(n >= 3, "cubic spline needs >= 3 points");
152 let x = xs;
153 let y = ys;
154
155 let mut mat = BandMatrix::new(n);
156 let mut rhs = vec![0.0; n];
157 for i in 1..n - 1 {
158 mat.set(i, i - 1, (x[i] - x[i - 1]) / 3.0);
159 mat.set(i, i, 2.0 / 3.0 * (x[i + 1] - x[i - 1]));
160 mat.set(i, i + 1, (x[i + 1] - x[i]) / 3.0);
161 rhs[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1]);
162 }
163 mat.set(0, 0, 2.0);
165 mat.set(0, 1, 0.0);
166 rhs[0] = 0.0;
167 mat.set(n - 1, n - 1, 2.0);
168 mat.set(n - 1, n - 2, 0.0);
169 rhs[n - 1] = 0.0;
170
171 let b = mat.lu_solve(&rhs);
172
173 let mut a = vec![0.0; n];
174 let mut c = vec![0.0; n];
175 for i in 0..n - 1 {
176 a[i] = (b[i + 1] - b[i]) / (x[i + 1] - x[i]) / 3.0;
177 c[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i])
178 - (2.0 * b[i] + b[i + 1]) * (x[i + 1] - x[i]) / 3.0;
179 }
180
181 let b0 = 0.0;
183 let c0 = c[0];
184 let h = x[n - 1] - x[n - 2];
185 a[n - 1] = 0.0;
186 c[n - 1] = 3.0 * a[n - 2] * h * h + 2.0 * b[n - 2] * h + c[n - 2];
187
188 Self {
189 x,
190 y,
191 a,
192 b,
193 c,
194 b0,
195 c0,
196 }
197 }
198
199 #[inline]
201 fn closest_idx_to(&self, x: f64) -> usize {
202 let it = self.x.partition_point(|&v| v < x);
204 if it == 0 {
205 0
206 } else {
207 it - 1
208 }
209 }
210
211 pub fn eval(&self, x: f64) -> f64 {
214 let n = self.x.len();
215 let idx = self.closest_idx_to(x);
216 let h = x - self.x[idx];
217 if x < self.x[0] {
218 (self.b0 * h + self.c0) * h + self.y[0]
219 } else if x > self.x[n - 1] {
220 (self.b[n - 1] * h + self.c[n - 1]) * h + self.y[n - 1]
221 } else {
222 ((self.a[idx] * h + self.b[idx]) * h + self.c[idx]) * h + self.y[idx]
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn interpolates_knots_exactly() {
233 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0];
234 let ys = vec![0.0, 1.0, 4.0, 9.0, 16.0]; let s = Spline::new(xs.clone(), ys.clone());
236 for (x, y) in xs.iter().zip(&ys) {
237 assert!(
238 (s.eval(*x) - y).abs() < 1e-9,
239 "knot {x}: {} != {y}",
240 s.eval(*x)
241 );
242 }
243 }
244
245 #[test]
246 fn monotone_line_is_reproduced() {
247 let xs = vec![0.0, 0.25, 0.5, 0.75, 1.0];
249 let ys: Vec<f64> = xs.iter().map(|x| 2.0 * x + 1.0).collect();
250 let s = Spline::new(xs, ys);
251 for i in 0..=10 {
252 let x = i as f64 / 10.0;
253 assert!((s.eval(x) - (2.0 * x + 1.0)).abs() < 1e-9, "x={x}");
254 }
255 }
256
257 #[test]
258 fn extrapolates_without_panic() {
259 let xs = vec![0.0, 0.5, 1.0];
260 let ys = vec![1.0, 2.0, 1.0];
261 let s = Spline::new(xs, ys);
262 let _ = s.eval(-0.3);
263 let _ = s.eval(1.7);
264 }
265}