1#[macro_export]
51macro_rules! approx_eq {
52 ($type:ty, $left:expr, $right:expr, epsilon = $epsilon:expr) => {{
53 let left_val: $type = $left;
54 let right_val: $type = $right;
55 (left_val - right_val).abs() < $epsilon
56 }};
57}
58
59#[inline]
71#[must_use]
72pub fn linear_weight(x1: f64, x2: f64, x: f64) -> f64 {
73 assert!(
74 x1.is_finite() && x2.is_finite() && x.is_finite(),
75 "All inputs must be finite: x1={x1}, x2={x2}, x={x}"
76 );
77
78 assert!(
79 !too_close_for_interpolation(x1, x2),
80 "`x1` ({x1}) and `x2` ({x2}) are too close for stable interpolation"
81 );
82 (x - x1) / (x2 - x1)
83}
84
85#[inline]
90#[must_use]
91pub fn linear_weighting(y1: f64, y2: f64, x1_diff: f64) -> f64 {
92 x1_diff.mul_add(y2 - y1, y1)
93}
94
95#[inline]
107#[must_use]
108pub fn pos_search(x: f64, xs: &[f64]) -> usize {
109 if xs.is_empty() {
110 return 0;
111 }
112
113 let n_elem = xs.len();
114 let pos = xs.partition_point(|&val| val < x);
115 pos.saturating_sub(1).min(n_elem - 1)
116}
117
118#[inline]
131#[must_use]
132pub fn quad_polynomial(x: f64, x0: f64, x1: f64, x2: f64, y0: f64, y1: f64, y2: f64) -> f64 {
133 assert!(
134 x.is_finite()
135 && x0.is_finite()
136 && x1.is_finite()
137 && x2.is_finite()
138 && y0.is_finite()
139 && y1.is_finite()
140 && y2.is_finite(),
141 "All inputs must be finite: x={x}, x0={x0}, x1={x1}, x2={x2}, y0={y0}, y1={y1}, y2={y2}"
142 );
143
144 assert!(
146 !too_close_for_interpolation(x0, x1)
147 && !too_close_for_interpolation(x0, x2)
148 && !too_close_for_interpolation(x1, x2),
149 "Abscissas are too close for stable interpolation: x0={x0}, x1={x1}, x2={x2}"
150 );
151
152 y0 * (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2))
153 + y1 * (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2))
154 + y2 * (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1))
155}
156
157#[must_use]
163pub fn quadratic_interpolation(x: f64, xs: &[f64], ys: &[f64]) -> f64 {
164 let n_elem = xs.len();
165 let epsilon = 1e-8;
166
167 assert!(
168 n_elem >= 3,
169 "Need at least 3 points for quadratic interpolation"
170 );
171 assert_eq!(xs.len(), ys.len(), "xs and ys must have the same length");
172
173 if x <= xs[0] {
174 return ys[0];
175 }
176
177 if x >= xs[n_elem - 1] {
178 return ys[n_elem - 1];
179 }
180
181 let pos = pos_search(x, xs);
182
183 if (xs[pos] - x).abs() < epsilon {
184 return ys[pos];
185 }
186
187 if pos == 0 {
188 return quad_polynomial(x, xs[0], xs[1], xs[2], ys[0], ys[1], ys[2]);
189 }
190
191 if pos == n_elem - 2 {
192 return quad_polynomial(
193 x,
194 xs[n_elem - 3],
195 xs[n_elem - 2],
196 xs[n_elem - 1],
197 ys[n_elem - 3],
198 ys[n_elem - 2],
199 ys[n_elem - 1],
200 );
201 }
202
203 let w = linear_weight(xs[pos], xs[pos + 1], x);
204
205 linear_weighting(
206 quad_polynomial(
207 x,
208 xs[pos - 1],
209 xs[pos],
210 xs[pos + 1],
211 ys[pos - 1],
212 ys[pos],
213 ys[pos + 1],
214 ),
215 quad_polynomial(
216 x,
217 xs[pos],
218 xs[pos + 1],
219 xs[pos + 2],
220 ys[pos],
221 ys[pos + 1],
222 ys[pos + 2],
223 ),
224 w,
225 )
226}
227
228#[inline]
236#[must_use]
237pub fn too_close_for_interpolation(x1: f64, x2: f64) -> bool {
238 const EPSILON: f64 = f64::EPSILON * 2.0; let scale = 1.0_f64.max(x1.abs()).max(x2.abs());
240 (x2 - x1).abs() < EPSILON * scale
241}
242
243#[cfg(test)]
244mod tests {
245 use rstest::*;
246
247 use super::*;
248
249 #[rstest]
250 #[case(0.0, 10.0, 5.0, 0.5)]
251 #[case(1.0, 3.0, 2.0, 0.5)]
252 #[case(0.0, 1.0, 0.25, 0.25)]
253 #[case(0.0, 1.0, 0.75, 0.75)]
254 fn test_linear_weight_valid_cases(
255 #[case] x1: f64,
256 #[case] x2: f64,
257 #[case] x: f64,
258 #[case] expected: f64,
259 ) {
260 let result = linear_weight(x1, x2, x);
261 assert!(
262 approx_eq!(f64, result, expected, epsilon = 1e-10),
263 "Expected {expected}, was {result}"
264 );
265 }
266
267 #[rstest]
268 #[should_panic(expected = "too close for stable interpolation")]
269 fn test_linear_weight_zero_divisor() {
270 let _ = linear_weight(1.0, 1.0, 0.5);
271 }
272
273 #[rstest]
274 #[should_panic(expected = "too close for stable interpolation")]
275 fn test_linear_weight_near_equal_values() {
276 let _ = linear_weight(1.0, 1.0 + f64::EPSILON, 0.5);
278 }
279
280 #[rstest]
281 fn test_linear_weight_with_small_differences() {
282 let result = linear_weight(0.0, 1e-12, 5e-13);
284 assert!(result.is_finite());
285 assert!((result - 0.5).abs() < 1e-10); }
287
288 #[rstest]
289 fn test_linear_weight_just_above_epsilon() {
290 let result = linear_weight(1.0, 1.0 + 1e-9, 1.0 + 5e-10);
292 assert!(result.is_finite());
294 }
295
296 #[rstest]
297 #[should_panic(expected = "too close for stable interpolation")]
298 fn test_linear_weight_large_magnitude_near_equal_panics() {
299 let _ = linear_weight(1e16, 1e16 + 2.0, 1e16 + 1.0);
302 }
303
304 #[rstest]
305 fn test_linear_weight_large_magnitude_adequate_spacing() {
306 let result = linear_weight(1e16, 1e16 + 1e10, 1e16 + 5e9);
307 assert!(result.is_finite());
308 assert!((result - 0.5).abs() < 1e-6);
309 }
310
311 #[rstest]
312 #[case(1.0, 3.0, 0.5, 2.0)]
313 #[case(10.0, 20.0, 0.25, 12.5)]
314 #[case(0.0, 10.0, 0.0, 0.0)]
315 #[case(0.0, 10.0, 1.0, 10.0)]
316 fn test_linear_weighting(
317 #[case] y1: f64,
318 #[case] y2: f64,
319 #[case] weight: f64,
320 #[case] expected: f64,
321 ) {
322 let result = linear_weighting(y1, y2, weight);
323 assert!(
324 approx_eq!(f64, result, expected, epsilon = 1e-10),
325 "Expected {expected}, was {result}"
326 );
327 }
328
329 #[rstest]
330 #[case(5.0, &[1.0, 2.0, 3.0, 4.0, 6.0, 7.0], 3)]
331 #[case(1.5, &[1.0, 2.0, 3.0, 4.0], 0)]
332 #[case(0.5, &[1.0, 2.0, 3.0, 4.0], 0)]
333 #[case(4.5, &[1.0, 2.0, 3.0, 4.0], 3)]
334 #[case(2.0, &[1.0, 2.0, 3.0, 4.0], 0)]
335 fn test_pos_search(#[case] x: f64, #[case] xs: &[f64], #[case] expected: usize) {
336 let result = pos_search(x, xs);
337 assert_eq!(result, expected);
338 }
339
340 #[rstest]
341 fn test_pos_search_edge_cases() {
342 let result = pos_search(5.0, &[10.0]);
344 assert_eq!(result, 0);
345
346 let result = pos_search(3.0, &[1.0, 2.0, 3.0, 4.0]);
348 assert_eq!(result, 1); let result = pos_search(1.5, &[1.0, 2.0]);
352 assert_eq!(result, 0);
353 }
354
355 #[rstest]
356 fn test_pos_search_empty_slice() {
357 let empty: &[f64] = &[];
358 assert_eq!(pos_search(42.0, empty), 0);
359 }
360
361 #[rstest]
362 fn test_quad_polynomial_linear_case() {
363 let result = quad_polynomial(1.5, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0);
365 assert!(approx_eq!(f64, result, 1.5, epsilon = 1e-10));
366 }
367
368 #[rstest]
369 fn test_quad_polynomial_parabola() {
370 let result = quad_polynomial(1.5, 0.0, 1.0, 2.0, 0.0, 1.0, 4.0);
373 let expected = 1.5 * 1.5; assert!(approx_eq!(f64, result, expected, epsilon = 1e-10));
375 }
376
377 #[rstest]
378 #[should_panic(expected = "too close for stable interpolation")]
379 fn test_quad_polynomial_duplicate_x() {
380 let _ = quad_polynomial(0.5, 1.0, 1.0, 2.0, 0.0, 1.0, 4.0);
381 }
382
383 #[rstest]
384 #[should_panic(expected = "too close for stable interpolation")]
385 fn test_quad_polynomial_near_equal_x_values() {
386 let _ = quad_polynomial(0.5, 1.0, 1.0 + f64::EPSILON, 2.0, 0.0, 1.0, 4.0);
388 }
389
390 #[rstest]
391 fn test_quad_polynomial_with_small_differences() {
392 let result = quad_polynomial(5e-13, 0.0, 1e-12, 2e-12, 0.0, 1.0, 4.0);
394 assert!(result.is_finite());
395 }
396
397 #[rstest]
398 fn test_quad_polynomial_just_above_epsilon() {
399 let result = quad_polynomial(0.5, 0.0, 1.0 + 1e-9, 2.0, 0.0, 1.0, 4.0);
401 assert!(result.is_finite());
403 }
404
405 #[rstest]
406 #[should_panic(expected = "too close for stable interpolation")]
407 fn test_quad_polynomial_large_magnitude_near_equal_panics() {
408 let _ = quad_polynomial(1e16, 1e16, 1e16 + 2.0, 2e16, 0.0, 1.0, 4.0);
410 }
411
412 #[rstest]
413 #[allow(
415 clippy::float_cmp,
416 reason = "boundary inputs must return the exact boundary ys value"
417 )]
418 fn test_quadratic_interpolation_boundary_conditions() {
419 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
420 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(0.5, &xs, &ys);
424 assert_eq!(result, ys[0]);
425
426 let result = quadratic_interpolation(6.0, &xs, &ys);
428 assert_eq!(result, ys[4]);
429 }
430
431 #[rstest]
432 fn test_quadratic_interpolation_exact_points() {
433 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
434 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0];
435
436 for (i, &x) in xs.iter().enumerate() {
438 let result = quadratic_interpolation(x, &xs, &ys);
439 assert!(approx_eq!(f64, result, ys[i], epsilon = 1e-6));
440 }
441 }
442
443 #[rstest]
444 fn test_quadratic_interpolation_intermediate_values() {
445 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
446 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(2.5, &xs, &ys);
450 let expected = 2.5 * 2.5; assert!((result - expected).abs() < 0.1); }
453
454 #[rstest]
455 #[should_panic(expected = "Need at least 3 points")]
456 fn test_quadratic_interpolation_insufficient_points() {
457 let xs = vec![1.0, 2.0];
458 let ys = vec![1.0, 4.0];
459 let _ = quadratic_interpolation(1.5, &xs, &ys);
460 }
461
462 #[rstest]
463 #[should_panic(expected = "xs and ys must have the same length")]
464 fn test_quadratic_interpolation_mismatched_lengths() {
465 let xs = vec![1.0, 2.0, 3.0];
466 let ys = vec![1.0, 4.0];
467 let _ = quadratic_interpolation(1.5, &xs, &ys);
468 }
469
470 #[rstest]
471 #[case(f64::NAN, 0.0, 1.0)]
472 #[case(0.0, f64::NAN, 1.0)]
473 #[case(0.0, 1.0, f64::NAN)]
474 #[case(f64::INFINITY, 0.0, 1.0)]
475 #[case(0.0, f64::NEG_INFINITY, 1.0)]
476 #[should_panic(expected = "All inputs must be finite")]
477 fn test_linear_weight_non_finite_panics(#[case] x1: f64, #[case] x2: f64, #[case] x: f64) {
478 let _ = linear_weight(x1, x2, x);
479 }
480
481 #[rstest]
482 #[should_panic(expected = "All inputs must be finite")]
483 fn test_quad_polynomial_nan_panics() {
484 let _ = quad_polynomial(f64::NAN, 0.0, 1.0, 2.0, 0.0, 1.0, 4.0);
485 }
486
487 #[rstest]
488 #[should_panic(expected = "All inputs must be finite")]
489 fn test_quad_polynomial_infinity_panics() {
490 let _ = quad_polynomial(0.5, f64::INFINITY, 1.0, 2.0, 0.0, 1.0, 4.0);
491 }
492}