1#[macro_export]
38macro_rules! approx_eq {
39 ($type:ty, $left:expr, $right:expr, epsilon = $epsilon:expr) => {{
40 let left_val: $type = $left;
41 let right_val: $type = $right;
42 (left_val - right_val).abs() < $epsilon
43 }};
44 ($type:ty, $left:expr, $right:expr, epsilon = $epsilon:expr, ulps = $ulps:expr) => {{
45 let left_val: $type = $left;
46 let right_val: $type = $right;
47 (left_val - right_val).abs() < $epsilon
49 }};
50}
51
52#[inline]
63#[must_use]
64pub fn linear_weight(x1: f64, x2: f64, x: f64) -> f64 {
65 const EPSILON: f64 = f64::EPSILON * 2.0; let diff = (x2 - x1).abs();
67 assert!(
68 diff >= EPSILON,
69 "`x1` ({}) and `x2` ({}) are too close for stable interpolation (diff: {}, min: {})",
70 x1,
71 x2,
72 diff,
73 EPSILON
74 );
75 (x - x1) / (x2 - x1)
76}
77
78#[inline]
83#[must_use]
84pub fn linear_weighting(y1: f64, y2: f64, x1_diff: f64) -> f64 {
85 x1_diff.mul_add(y2 - y1, y1)
86}
87
88#[inline]
113#[must_use]
114pub fn pos_search(x: f64, xs: &[f64]) -> usize {
115 if xs.is_empty() {
116 return 0;
117 }
118
119 let n_elem = xs.len();
120 let pos = xs.partition_point(|&val| val < x);
121 std::cmp::min(std::cmp::max(pos.saturating_sub(1), 0), n_elem - 1)
122}
123
124#[inline]
135#[must_use]
136pub fn quad_polynomial(x: f64, x0: f64, x1: f64, x2: f64, y0: f64, y1: f64, y2: f64) -> f64 {
137 const EPSILON: f64 = f64::EPSILON * 2.0; let diff_01 = (x0 - x1).abs();
141 let diff_02 = (x0 - x2).abs();
142 let diff_12 = (x1 - x2).abs();
143
144 assert!(
145 diff_01 >= EPSILON && diff_02 >= EPSILON && diff_12 >= EPSILON,
146 "Abscissas are too close for stable interpolation: x0={}, x1={}, x2={} (diffs: {:.2e}, {:.2e}, {:.2e}, min: {})",
147 x0,
148 x1,
149 x2,
150 diff_01,
151 diff_02,
152 diff_12,
153 EPSILON
154 );
155
156 y0 * (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2))
157 + y1 * (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2))
158 + y2 * (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1))
159}
160
161#[must_use]
167pub fn quadratic_interpolation(x: f64, xs: &[f64], ys: &[f64]) -> f64 {
168 let n_elem = xs.len();
169 let epsilon = 1e-8;
170
171 assert!(
172 n_elem >= 3,
173 "Need at least 3 points for quadratic interpolation"
174 );
175 assert_eq!(xs.len(), ys.len(), "xs and ys must have the same length");
176
177 if x <= xs[0] {
178 return ys[0];
179 }
180
181 if x >= xs[n_elem - 1] {
182 return ys[n_elem - 1];
183 }
184
185 let pos = pos_search(x, xs);
186
187 if (xs[pos] - x).abs() < epsilon {
188 return ys[pos];
189 }
190
191 if pos == 0 {
192 return quad_polynomial(x, xs[0], xs[1], xs[2], ys[0], ys[1], ys[2]);
193 }
194
195 if pos == n_elem - 2 {
196 return quad_polynomial(
197 x,
198 xs[n_elem - 3],
199 xs[n_elem - 2],
200 xs[n_elem - 1],
201 ys[n_elem - 3],
202 ys[n_elem - 2],
203 ys[n_elem - 1],
204 );
205 }
206
207 let w = linear_weight(xs[pos], xs[pos + 1], x);
208
209 linear_weighting(
210 quad_polynomial(
211 x,
212 xs[pos - 1],
213 xs[pos],
214 xs[pos + 1],
215 ys[pos - 1],
216 ys[pos],
217 ys[pos + 1],
218 ),
219 quad_polynomial(
220 x,
221 xs[pos],
222 xs[pos + 1],
223 xs[pos + 2],
224 ys[pos],
225 ys[pos + 1],
226 ys[pos + 2],
227 ),
228 w,
229 )
230}
231
232#[cfg(test)]
236mod tests {
237 use rstest::*;
238
239 use super::*;
240
241 #[rstest]
242 #[case(0.0, 10.0, 5.0, 0.5)]
243 #[case(1.0, 3.0, 2.0, 0.5)]
244 #[case(0.0, 1.0, 0.25, 0.25)]
245 #[case(0.0, 1.0, 0.75, 0.75)]
246 fn test_linear_weight_valid_cases(
247 #[case] x1: f64,
248 #[case] x2: f64,
249 #[case] x: f64,
250 #[case] expected: f64,
251 ) {
252 let result = linear_weight(x1, x2, x);
253 assert!(
254 approx_eq!(f64, result, expected, epsilon = 1e-10),
255 "Expected {expected}, was {result}"
256 );
257 }
258
259 #[rstest]
260 #[should_panic(expected = "too close for stable interpolation")]
261 fn test_linear_weight_zero_divisor() {
262 let _ = linear_weight(1.0, 1.0, 0.5);
263 }
264
265 #[rstest]
266 #[should_panic(expected = "too close for stable interpolation")]
267 fn test_linear_weight_near_equal_values() {
268 let _ = linear_weight(1.0, 1.0 + f64::EPSILON, 0.5);
270 }
271
272 #[rstest]
273 fn test_linear_weight_with_small_differences() {
274 let result = linear_weight(0.0, 1e-12, 5e-13);
276 assert!(result.is_finite());
277 assert!((result - 0.5).abs() < 1e-10); }
279
280 #[rstest]
281 fn test_linear_weight_just_above_epsilon() {
282 let result = linear_weight(1.0, 1.0 + 1e-9, 1.0 + 5e-10);
284 assert!(result.is_finite());
286 }
287
288 #[rstest]
289 #[case(1.0, 3.0, 0.5, 2.0)]
290 #[case(10.0, 20.0, 0.25, 12.5)]
291 #[case(0.0, 10.0, 0.0, 0.0)]
292 #[case(0.0, 10.0, 1.0, 10.0)]
293 fn test_linear_weighting(
294 #[case] y1: f64,
295 #[case] y2: f64,
296 #[case] weight: f64,
297 #[case] expected: f64,
298 ) {
299 let result = linear_weighting(y1, y2, weight);
300 assert!(
301 approx_eq!(f64, result, expected, epsilon = 1e-10),
302 "Expected {expected}, was {result}"
303 );
304 }
305
306 #[rstest]
307 #[case(5.0, &[1.0, 2.0, 3.0, 4.0, 6.0, 7.0], 3)]
308 #[case(1.5, &[1.0, 2.0, 3.0, 4.0], 0)]
309 #[case(0.5, &[1.0, 2.0, 3.0, 4.0], 0)]
310 #[case(4.5, &[1.0, 2.0, 3.0, 4.0], 3)]
311 #[case(2.0, &[1.0, 2.0, 3.0, 4.0], 0)]
312 fn test_pos_search(#[case] x: f64, #[case] xs: &[f64], #[case] expected: usize) {
313 let result = pos_search(x, xs);
314 assert_eq!(result, expected);
315 }
316
317 #[rstest]
318 fn test_pos_search_edge_cases() {
319 let result = pos_search(5.0, &[10.0]);
321 assert_eq!(result, 0);
322
323 let result = pos_search(3.0, &[1.0, 2.0, 3.0, 4.0]);
325 assert_eq!(result, 1); let result = pos_search(1.5, &[1.0, 2.0]);
329 assert_eq!(result, 0);
330 }
331
332 #[rstest]
333 fn test_pos_search_empty_slice() {
334 let empty: &[f64] = &[];
335 assert_eq!(pos_search(42.0, empty), 0);
336 }
337
338 #[rstest]
339 fn test_quad_polynomial_linear_case() {
340 let result = quad_polynomial(1.5, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0);
342 assert!(approx_eq!(f64, result, 1.5, epsilon = 1e-10));
343 }
344
345 #[rstest]
346 fn test_quad_polynomial_parabola() {
347 let result = quad_polynomial(1.5, 0.0, 1.0, 2.0, 0.0, 1.0, 4.0);
350 let expected = 1.5 * 1.5; assert!(approx_eq!(f64, result, expected, epsilon = 1e-10));
352 }
353
354 #[rstest]
355 #[should_panic(expected = "too close for stable interpolation")]
356 fn test_quad_polynomial_duplicate_x() {
357 let _ = quad_polynomial(0.5, 1.0, 1.0, 2.0, 0.0, 1.0, 4.0);
358 }
359
360 #[rstest]
361 #[should_panic(expected = "too close for stable interpolation")]
362 fn test_quad_polynomial_near_equal_x_values() {
363 let _ = quad_polynomial(0.5, 1.0, 1.0 + f64::EPSILON, 2.0, 0.0, 1.0, 4.0);
365 }
366
367 #[rstest]
368 fn test_quad_polynomial_with_small_differences() {
369 let result = quad_polynomial(5e-13, 0.0, 1e-12, 2e-12, 0.0, 1.0, 4.0);
371 assert!(result.is_finite());
372 }
373
374 #[rstest]
375 fn test_quad_polynomial_just_above_epsilon() {
376 let result = quad_polynomial(0.5, 0.0, 1.0 + 1e-9, 2.0, 0.0, 1.0, 4.0);
378 assert!(result.is_finite());
380 }
381
382 #[rstest]
383 fn test_quadratic_interpolation_boundary_conditions() {
384 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
385 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(0.5, &xs, &ys);
389 assert_eq!(result, ys[0]);
390
391 let result = quadratic_interpolation(6.0, &xs, &ys);
393 assert_eq!(result, ys[4]);
394 }
395
396 #[rstest]
397 fn test_quadratic_interpolation_exact_points() {
398 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
399 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0];
400
401 for (i, &x) in xs.iter().enumerate() {
403 let result = quadratic_interpolation(x, &xs, &ys);
404 assert!(approx_eq!(f64, result, ys[i], epsilon = 1e-6));
405 }
406 }
407
408 #[rstest]
409 fn test_quadratic_interpolation_intermediate_values() {
410 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
411 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(2.5, &xs, &ys);
415 let expected = 2.5 * 2.5; assert!((result - expected).abs() < 0.1); }
418
419 #[rstest]
420 #[should_panic(expected = "Need at least 3 points")]
421 fn test_quadratic_interpolation_insufficient_points() {
422 let xs = vec![1.0, 2.0];
423 let ys = vec![1.0, 4.0];
424 let _ = quadratic_interpolation(1.5, &xs, &ys);
425 }
426
427 #[rstest]
428 #[should_panic(expected = "xs and ys must have the same length")]
429 fn test_quadratic_interpolation_mismatched_lengths() {
430 let xs = vec![1.0, 2.0, 3.0];
431 let ys = vec![1.0, 4.0];
432 let _ = quadratic_interpolation(1.5, &xs, &ys);
433 }
434}