1#[macro_export]
49macro_rules! approx_eq {
50 ($type:ty, $left:expr, $right:expr, epsilon = $epsilon:expr) => {{
51 let left_val: $type = $left;
52 let right_val: $type = $right;
53 (left_val - right_val).abs() < $epsilon
54 }};
55}
56
57#[inline]
68#[must_use]
69pub fn linear_weight(x1: f64, x2: f64, x: f64) -> f64 {
70 assert!(
71 x1.is_finite() && x2.is_finite() && x.is_finite(),
72 "All inputs must be finite: x1={x1}, x2={x2}, x={x}"
73 );
74
75 const EPSILON: f64 = f64::EPSILON * 2.0; let diff = (x2 - x1).abs();
77 assert!(
78 diff >= EPSILON,
79 "`x1` ({x1}) and `x2` ({x2}) are too close for stable interpolation (diff: {diff}, min: {EPSILON})"
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 std::cmp::min(std::cmp::max(pos.saturating_sub(1), 0), 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 const EPSILON: f64 = f64::EPSILON * 2.0; let diff_01 = (x0 - x1).abs();
146 let diff_02 = (x0 - x2).abs();
147 let diff_12 = (x1 - x2).abs();
148
149 assert!(
150 diff_01 >= EPSILON && diff_02 >= EPSILON && diff_12 >= EPSILON,
151 "Abscissas are too close for stable interpolation: x0={x0}, x1={x1}, x2={x2} (diffs: {diff_01:.2e}, {diff_02:.2e}, {diff_12:.2e}, min: {EPSILON})"
152 );
153
154 y0 * (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2))
155 + y1 * (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2))
156 + y2 * (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1))
157}
158
159#[must_use]
165pub fn quadratic_interpolation(x: f64, xs: &[f64], ys: &[f64]) -> f64 {
166 let n_elem = xs.len();
167 let epsilon = 1e-8;
168
169 assert!(
170 n_elem >= 3,
171 "Need at least 3 points for quadratic interpolation"
172 );
173 assert_eq!(xs.len(), ys.len(), "xs and ys must have the same length");
174
175 if x <= xs[0] {
176 return ys[0];
177 }
178
179 if x >= xs[n_elem - 1] {
180 return ys[n_elem - 1];
181 }
182
183 let pos = pos_search(x, xs);
184
185 if (xs[pos] - x).abs() < epsilon {
186 return ys[pos];
187 }
188
189 if pos == 0 {
190 return quad_polynomial(x, xs[0], xs[1], xs[2], ys[0], ys[1], ys[2]);
191 }
192
193 if pos == n_elem - 2 {
194 return quad_polynomial(
195 x,
196 xs[n_elem - 3],
197 xs[n_elem - 2],
198 xs[n_elem - 1],
199 ys[n_elem - 3],
200 ys[n_elem - 2],
201 ys[n_elem - 1],
202 );
203 }
204
205 let w = linear_weight(xs[pos], xs[pos + 1], x);
206
207 linear_weighting(
208 quad_polynomial(
209 x,
210 xs[pos - 1],
211 xs[pos],
212 xs[pos + 1],
213 ys[pos - 1],
214 ys[pos],
215 ys[pos + 1],
216 ),
217 quad_polynomial(
218 x,
219 xs[pos],
220 xs[pos + 1],
221 xs[pos + 2],
222 ys[pos],
223 ys[pos + 1],
224 ys[pos + 2],
225 ),
226 w,
227 )
228}
229
230#[cfg(test)]
231mod tests {
232 use rstest::*;
233
234 use super::*;
235
236 #[rstest]
237 #[case(0.0, 10.0, 5.0, 0.5)]
238 #[case(1.0, 3.0, 2.0, 0.5)]
239 #[case(0.0, 1.0, 0.25, 0.25)]
240 #[case(0.0, 1.0, 0.75, 0.75)]
241 fn test_linear_weight_valid_cases(
242 #[case] x1: f64,
243 #[case] x2: f64,
244 #[case] x: f64,
245 #[case] expected: f64,
246 ) {
247 let result = linear_weight(x1, x2, x);
248 assert!(
249 approx_eq!(f64, result, expected, epsilon = 1e-10),
250 "Expected {expected}, was {result}"
251 );
252 }
253
254 #[rstest]
255 #[should_panic(expected = "too close for stable interpolation")]
256 fn test_linear_weight_zero_divisor() {
257 let _ = linear_weight(1.0, 1.0, 0.5);
258 }
259
260 #[rstest]
261 #[should_panic(expected = "too close for stable interpolation")]
262 fn test_linear_weight_near_equal_values() {
263 let _ = linear_weight(1.0, 1.0 + f64::EPSILON, 0.5);
265 }
266
267 #[rstest]
268 fn test_linear_weight_with_small_differences() {
269 let result = linear_weight(0.0, 1e-12, 5e-13);
271 assert!(result.is_finite());
272 assert!((result - 0.5).abs() < 1e-10); }
274
275 #[rstest]
276 fn test_linear_weight_just_above_epsilon() {
277 let result = linear_weight(1.0, 1.0 + 1e-9, 1.0 + 5e-10);
279 assert!(result.is_finite());
281 }
282
283 #[rstest]
284 #[case(1.0, 3.0, 0.5, 2.0)]
285 #[case(10.0, 20.0, 0.25, 12.5)]
286 #[case(0.0, 10.0, 0.0, 0.0)]
287 #[case(0.0, 10.0, 1.0, 10.0)]
288 fn test_linear_weighting(
289 #[case] y1: f64,
290 #[case] y2: f64,
291 #[case] weight: f64,
292 #[case] expected: f64,
293 ) {
294 let result = linear_weighting(y1, y2, weight);
295 assert!(
296 approx_eq!(f64, result, expected, epsilon = 1e-10),
297 "Expected {expected}, was {result}"
298 );
299 }
300
301 #[rstest]
302 #[case(5.0, &[1.0, 2.0, 3.0, 4.0, 6.0, 7.0], 3)]
303 #[case(1.5, &[1.0, 2.0, 3.0, 4.0], 0)]
304 #[case(0.5, &[1.0, 2.0, 3.0, 4.0], 0)]
305 #[case(4.5, &[1.0, 2.0, 3.0, 4.0], 3)]
306 #[case(2.0, &[1.0, 2.0, 3.0, 4.0], 0)]
307 fn test_pos_search(#[case] x: f64, #[case] xs: &[f64], #[case] expected: usize) {
308 let result = pos_search(x, xs);
309 assert_eq!(result, expected);
310 }
311
312 #[rstest]
313 fn test_pos_search_edge_cases() {
314 let result = pos_search(5.0, &[10.0]);
316 assert_eq!(result, 0);
317
318 let result = pos_search(3.0, &[1.0, 2.0, 3.0, 4.0]);
320 assert_eq!(result, 1); let result = pos_search(1.5, &[1.0, 2.0]);
324 assert_eq!(result, 0);
325 }
326
327 #[rstest]
328 fn test_pos_search_empty_slice() {
329 let empty: &[f64] = &[];
330 assert_eq!(pos_search(42.0, empty), 0);
331 }
332
333 #[rstest]
334 fn test_quad_polynomial_linear_case() {
335 let result = quad_polynomial(1.5, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0);
337 assert!(approx_eq!(f64, result, 1.5, epsilon = 1e-10));
338 }
339
340 #[rstest]
341 fn test_quad_polynomial_parabola() {
342 let result = quad_polynomial(1.5, 0.0, 1.0, 2.0, 0.0, 1.0, 4.0);
345 let expected = 1.5 * 1.5; assert!(approx_eq!(f64, result, expected, epsilon = 1e-10));
347 }
348
349 #[rstest]
350 #[should_panic(expected = "too close for stable interpolation")]
351 fn test_quad_polynomial_duplicate_x() {
352 let _ = quad_polynomial(0.5, 1.0, 1.0, 2.0, 0.0, 1.0, 4.0);
353 }
354
355 #[rstest]
356 #[should_panic(expected = "too close for stable interpolation")]
357 fn test_quad_polynomial_near_equal_x_values() {
358 let _ = quad_polynomial(0.5, 1.0, 1.0 + f64::EPSILON, 2.0, 0.0, 1.0, 4.0);
360 }
361
362 #[rstest]
363 fn test_quad_polynomial_with_small_differences() {
364 let result = quad_polynomial(5e-13, 0.0, 1e-12, 2e-12, 0.0, 1.0, 4.0);
366 assert!(result.is_finite());
367 }
368
369 #[rstest]
370 fn test_quad_polynomial_just_above_epsilon() {
371 let result = quad_polynomial(0.5, 0.0, 1.0 + 1e-9, 2.0, 0.0, 1.0, 4.0);
373 assert!(result.is_finite());
375 }
376
377 #[rstest]
378 fn test_quadratic_interpolation_boundary_conditions() {
379 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
380 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(0.5, &xs, &ys);
384 assert_eq!(result, ys[0]);
385
386 let result = quadratic_interpolation(6.0, &xs, &ys);
388 assert_eq!(result, ys[4]);
389 }
390
391 #[rstest]
392 fn test_quadratic_interpolation_exact_points() {
393 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
394 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0];
395
396 for (i, &x) in xs.iter().enumerate() {
398 let result = quadratic_interpolation(x, &xs, &ys);
399 assert!(approx_eq!(f64, result, ys[i], epsilon = 1e-6));
400 }
401 }
402
403 #[rstest]
404 fn test_quadratic_interpolation_intermediate_values() {
405 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
406 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(2.5, &xs, &ys);
410 let expected = 2.5 * 2.5; assert!((result - expected).abs() < 0.1); }
413
414 #[rstest]
415 #[should_panic(expected = "Need at least 3 points")]
416 fn test_quadratic_interpolation_insufficient_points() {
417 let xs = vec![1.0, 2.0];
418 let ys = vec![1.0, 4.0];
419 let _ = quadratic_interpolation(1.5, &xs, &ys);
420 }
421
422 #[rstest]
423 #[should_panic(expected = "xs and ys must have the same length")]
424 fn test_quadratic_interpolation_mismatched_lengths() {
425 let xs = vec![1.0, 2.0, 3.0];
426 let ys = vec![1.0, 4.0];
427 let _ = quadratic_interpolation(1.5, &xs, &ys);
428 }
429
430 #[rstest]
431 #[case(f64::NAN, 0.0, 1.0)]
432 #[case(0.0, f64::NAN, 1.0)]
433 #[case(0.0, 1.0, f64::NAN)]
434 #[case(f64::INFINITY, 0.0, 1.0)]
435 #[case(0.0, f64::NEG_INFINITY, 1.0)]
436 #[should_panic(expected = "All inputs must be finite")]
437 fn test_linear_weight_non_finite_panics(#[case] x1: f64, #[case] x2: f64, #[case] x: f64) {
438 let _ = linear_weight(x1, x2, x);
439 }
440
441 #[rstest]
442 #[should_panic(expected = "All inputs must be finite")]
443 fn test_quad_polynomial_nan_panics() {
444 let _ = quad_polynomial(f64::NAN, 0.0, 1.0, 2.0, 0.0, 1.0, 4.0);
445 }
446
447 #[rstest]
448 #[should_panic(expected = "All inputs must be finite")]
449 fn test_quad_polynomial_infinity_panics() {
450 let _ = quad_polynomial(0.5, f64::INFINITY, 1.0, 2.0, 0.0, 1.0, 4.0);
451 }
452}