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