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` ({x1}) and `x2` ({x2}) are too close for stable interpolation (diff: {diff}, min: {EPSILON})"
70 );
71 (x - x1) / (x2 - x1)
72}
73
74#[inline]
79#[must_use]
80pub fn linear_weighting(y1: f64, y2: f64, x1_diff: f64) -> f64 {
81 x1_diff.mul_add(y2 - y1, y1)
82}
83
84#[inline]
109#[must_use]
110pub fn pos_search(x: f64, xs: &[f64]) -> usize {
111 if xs.is_empty() {
112 return 0;
113 }
114
115 let n_elem = xs.len();
116 let pos = xs.partition_point(|&val| val < x);
117 std::cmp::min(std::cmp::max(pos.saturating_sub(1), 0), n_elem - 1)
118}
119
120#[inline]
131#[must_use]
132pub fn quad_polynomial(x: f64, x0: f64, x1: f64, x2: f64, y0: f64, y1: f64, y2: f64) -> f64 {
133 const EPSILON: f64 = f64::EPSILON * 2.0; let diff_01 = (x0 - x1).abs();
137 let diff_02 = (x0 - x2).abs();
138 let diff_12 = (x1 - x2).abs();
139
140 assert!(
141 diff_01 >= EPSILON && diff_02 >= EPSILON && diff_12 >= EPSILON,
142 "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})"
143 );
144
145 y0 * (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2))
146 + y1 * (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2))
147 + y2 * (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1))
148}
149
150#[must_use]
156pub fn quadratic_interpolation(x: f64, xs: &[f64], ys: &[f64]) -> f64 {
157 let n_elem = xs.len();
158 let epsilon = 1e-8;
159
160 assert!(
161 n_elem >= 3,
162 "Need at least 3 points for quadratic interpolation"
163 );
164 assert_eq!(xs.len(), ys.len(), "xs and ys must have the same length");
165
166 if x <= xs[0] {
167 return ys[0];
168 }
169
170 if x >= xs[n_elem - 1] {
171 return ys[n_elem - 1];
172 }
173
174 let pos = pos_search(x, xs);
175
176 if (xs[pos] - x).abs() < epsilon {
177 return ys[pos];
178 }
179
180 if pos == 0 {
181 return quad_polynomial(x, xs[0], xs[1], xs[2], ys[0], ys[1], ys[2]);
182 }
183
184 if pos == n_elem - 2 {
185 return quad_polynomial(
186 x,
187 xs[n_elem - 3],
188 xs[n_elem - 2],
189 xs[n_elem - 1],
190 ys[n_elem - 3],
191 ys[n_elem - 2],
192 ys[n_elem - 1],
193 );
194 }
195
196 let w = linear_weight(xs[pos], xs[pos + 1], x);
197
198 linear_weighting(
199 quad_polynomial(
200 x,
201 xs[pos - 1],
202 xs[pos],
203 xs[pos + 1],
204 ys[pos - 1],
205 ys[pos],
206 ys[pos + 1],
207 ),
208 quad_polynomial(
209 x,
210 xs[pos],
211 xs[pos + 1],
212 xs[pos + 2],
213 ys[pos],
214 ys[pos + 1],
215 ys[pos + 2],
216 ),
217 w,
218 )
219}
220
221#[cfg(test)]
225mod tests {
226 use rstest::*;
227
228 use super::*;
229
230 #[rstest]
231 #[case(0.0, 10.0, 5.0, 0.5)]
232 #[case(1.0, 3.0, 2.0, 0.5)]
233 #[case(0.0, 1.0, 0.25, 0.25)]
234 #[case(0.0, 1.0, 0.75, 0.75)]
235 fn test_linear_weight_valid_cases(
236 #[case] x1: f64,
237 #[case] x2: f64,
238 #[case] x: f64,
239 #[case] expected: f64,
240 ) {
241 let result = linear_weight(x1, x2, x);
242 assert!(
243 approx_eq!(f64, result, expected, epsilon = 1e-10),
244 "Expected {expected}, was {result}"
245 );
246 }
247
248 #[rstest]
249 #[should_panic(expected = "too close for stable interpolation")]
250 fn test_linear_weight_zero_divisor() {
251 let _ = linear_weight(1.0, 1.0, 0.5);
252 }
253
254 #[rstest]
255 #[should_panic(expected = "too close for stable interpolation")]
256 fn test_linear_weight_near_equal_values() {
257 let _ = linear_weight(1.0, 1.0 + f64::EPSILON, 0.5);
259 }
260
261 #[rstest]
262 fn test_linear_weight_with_small_differences() {
263 let result = linear_weight(0.0, 1e-12, 5e-13);
265 assert!(result.is_finite());
266 assert!((result - 0.5).abs() < 1e-10); }
268
269 #[rstest]
270 fn test_linear_weight_just_above_epsilon() {
271 let result = linear_weight(1.0, 1.0 + 1e-9, 1.0 + 5e-10);
273 assert!(result.is_finite());
275 }
276
277 #[rstest]
278 #[case(1.0, 3.0, 0.5, 2.0)]
279 #[case(10.0, 20.0, 0.25, 12.5)]
280 #[case(0.0, 10.0, 0.0, 0.0)]
281 #[case(0.0, 10.0, 1.0, 10.0)]
282 fn test_linear_weighting(
283 #[case] y1: f64,
284 #[case] y2: f64,
285 #[case] weight: f64,
286 #[case] expected: f64,
287 ) {
288 let result = linear_weighting(y1, y2, weight);
289 assert!(
290 approx_eq!(f64, result, expected, epsilon = 1e-10),
291 "Expected {expected}, was {result}"
292 );
293 }
294
295 #[rstest]
296 #[case(5.0, &[1.0, 2.0, 3.0, 4.0, 6.0, 7.0], 3)]
297 #[case(1.5, &[1.0, 2.0, 3.0, 4.0], 0)]
298 #[case(0.5, &[1.0, 2.0, 3.0, 4.0], 0)]
299 #[case(4.5, &[1.0, 2.0, 3.0, 4.0], 3)]
300 #[case(2.0, &[1.0, 2.0, 3.0, 4.0], 0)]
301 fn test_pos_search(#[case] x: f64, #[case] xs: &[f64], #[case] expected: usize) {
302 let result = pos_search(x, xs);
303 assert_eq!(result, expected);
304 }
305
306 #[rstest]
307 fn test_pos_search_edge_cases() {
308 let result = pos_search(5.0, &[10.0]);
310 assert_eq!(result, 0);
311
312 let result = pos_search(3.0, &[1.0, 2.0, 3.0, 4.0]);
314 assert_eq!(result, 1); let result = pos_search(1.5, &[1.0, 2.0]);
318 assert_eq!(result, 0);
319 }
320
321 #[rstest]
322 fn test_pos_search_empty_slice() {
323 let empty: &[f64] = &[];
324 assert_eq!(pos_search(42.0, empty), 0);
325 }
326
327 #[rstest]
328 fn test_quad_polynomial_linear_case() {
329 let result = quad_polynomial(1.5, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0);
331 assert!(approx_eq!(f64, result, 1.5, epsilon = 1e-10));
332 }
333
334 #[rstest]
335 fn test_quad_polynomial_parabola() {
336 let result = quad_polynomial(1.5, 0.0, 1.0, 2.0, 0.0, 1.0, 4.0);
339 let expected = 1.5 * 1.5; assert!(approx_eq!(f64, result, expected, epsilon = 1e-10));
341 }
342
343 #[rstest]
344 #[should_panic(expected = "too close for stable interpolation")]
345 fn test_quad_polynomial_duplicate_x() {
346 let _ = quad_polynomial(0.5, 1.0, 1.0, 2.0, 0.0, 1.0, 4.0);
347 }
348
349 #[rstest]
350 #[should_panic(expected = "too close for stable interpolation")]
351 fn test_quad_polynomial_near_equal_x_values() {
352 let _ = quad_polynomial(0.5, 1.0, 1.0 + f64::EPSILON, 2.0, 0.0, 1.0, 4.0);
354 }
355
356 #[rstest]
357 fn test_quad_polynomial_with_small_differences() {
358 let result = quad_polynomial(5e-13, 0.0, 1e-12, 2e-12, 0.0, 1.0, 4.0);
360 assert!(result.is_finite());
361 }
362
363 #[rstest]
364 fn test_quad_polynomial_just_above_epsilon() {
365 let result = quad_polynomial(0.5, 0.0, 1.0 + 1e-9, 2.0, 0.0, 1.0, 4.0);
367 assert!(result.is_finite());
369 }
370
371 #[rstest]
372 fn test_quadratic_interpolation_boundary_conditions() {
373 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
374 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(0.5, &xs, &ys);
378 assert_eq!(result, ys[0]);
379
380 let result = quadratic_interpolation(6.0, &xs, &ys);
382 assert_eq!(result, ys[4]);
383 }
384
385 #[rstest]
386 fn test_quadratic_interpolation_exact_points() {
387 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
388 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0];
389
390 for (i, &x) in xs.iter().enumerate() {
392 let result = quadratic_interpolation(x, &xs, &ys);
393 assert!(approx_eq!(f64, result, ys[i], epsilon = 1e-6));
394 }
395 }
396
397 #[rstest]
398 fn test_quadratic_interpolation_intermediate_values() {
399 let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
400 let ys = vec![1.0, 4.0, 9.0, 16.0, 25.0]; let result = quadratic_interpolation(2.5, &xs, &ys);
404 let expected = 2.5 * 2.5; assert!((result - expected).abs() < 0.1); }
407
408 #[rstest]
409 #[should_panic(expected = "Need at least 3 points")]
410 fn test_quadratic_interpolation_insufficient_points() {
411 let xs = vec![1.0, 2.0];
412 let ys = vec![1.0, 4.0];
413 let _ = quadratic_interpolation(1.5, &xs, &ys);
414 }
415
416 #[rstest]
417 #[should_panic(expected = "xs and ys must have the same length")]
418 fn test_quadratic_interpolation_mismatched_lengths() {
419 let xs = vec![1.0, 2.0, 3.0];
420 let ys = vec![1.0, 4.0];
421 let _ = quadratic_interpolation(1.5, &xs, &ys);
422 }
423}