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