1#![allow(clippy::needless_range_loop)]
6use super::types::{BSplineBasis, RbfKernel};
7
8pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
12 a + (b - a) * t
13}
14pub fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
16 [
17 lerp(a[0], b[0], t),
18 lerp(a[1], b[1], t),
19 lerp(a[2], b[2], t),
20 ]
21}
22pub fn bilinear(f00: f64, f10: f64, f01: f64, f11: f64, u: f64, v: f64) -> f64 {
27 lerp(lerp(f00, f10, u), lerp(f01, f11, u), v)
28}
29pub fn trilinear(values: &[f64; 8], u: f64, v: f64, w: f64) -> f64 {
35 let c00 = lerp(values[0], values[1], u);
36 let c10 = lerp(values[2], values[3], u);
37 let c01 = lerp(values[4], values[5], u);
38 let c11 = lerp(values[6], values[7], u);
39 let c0 = lerp(c00, c10, v);
40 let c1 = lerp(c01, c11, v);
41 lerp(c0, c1, w)
42}
43pub fn quat_dot(a: [f64; 4], b: [f64; 4]) -> f64 {
45 a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
46}
47pub fn quat_norm(q: [f64; 4]) -> f64 {
49 quat_dot(q, q).sqrt()
50}
51pub fn quat_normalize(q: [f64; 4]) -> [f64; 4] {
53 let n = quat_norm(q);
54 if n < f64::EPSILON {
55 return [1.0, 0.0, 0.0, 0.0];
56 }
57 [q[0] / n, q[1] / n, q[2] / n, q[3] / n]
58}
59pub fn quat_nlerp(a: [f64; 4], b: [f64; 4], t: f64) -> [f64; 4] {
63 let b = if quat_dot(a, b) < 0.0 {
64 [-b[0], -b[1], -b[2], -b[3]]
65 } else {
66 b
67 };
68 let raw = [
69 lerp(a[0], b[0], t),
70 lerp(a[1], b[1], t),
71 lerp(a[2], b[2], t),
72 lerp(a[3], b[3], t),
73 ];
74 quat_normalize(raw)
75}
76pub fn quat_slerp(a: [f64; 4], b: [f64; 4], t: f64) -> [f64; 4] {
81 let mut dot = quat_dot(a, b);
82 let b = if dot < 0.0 {
83 dot = -dot;
84 [-b[0], -b[1], -b[2], -b[3]]
85 } else {
86 b
87 };
88 if dot > 0.9995 {
89 return quat_nlerp(a, b, t);
90 }
91 let theta_0 = dot.acos();
92 let theta = theta_0 * t;
93 let sin_theta = theta.sin();
94 let sin_theta_0 = theta_0.sin();
95 let s0 = (theta_0 - theta).sin() / sin_theta_0;
96 let s1 = sin_theta / sin_theta_0;
97 [
98 s0 * a[0] + s1 * b[0],
99 s0 * a[1] + s1 * b[1],
100 s0 * a[2] + s1 * b[2],
101 s0 * a[3] + s1 * b[3],
102 ]
103}
104pub fn quat_squad(q0: [f64; 4], q1: [f64; 4], q2: [f64; 4], q3: [f64; 4], t: f64) -> [f64; 4] {
109 let s1 = squad_inner(q0, q1, q2);
110 let s2 = squad_inner(q1, q2, q3);
111 let u = 2.0 * t * (1.0 - t);
112 quat_slerp(quat_slerp(q1, q2, t), quat_slerp(s1, s2, t), u)
113}
114pub(super) fn squad_inner(q_prev: [f64; 4], q: [f64; 4], q_next: [f64; 4]) -> [f64; 4] {
116 let inv_q_prev = quat_slerp(q, q_prev, 1.0);
117 let inv_q_next = quat_slerp(q, q_next, 1.0);
118 let mid = quat_nlerp(inv_q_prev, inv_q_next, 0.5);
119 quat_slerp(q, mid, -0.25)
120}
121pub fn hermite(p0: f64, m0: f64, p1: f64, m1: f64, t: f64) -> f64 {
125 let t2 = t * t;
126 let t3 = t2 * t;
127 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
128 let h10 = t3 - 2.0 * t2 + t;
129 let h01 = -2.0 * t3 + 3.0 * t2;
130 let h11 = t3 - t2;
131 h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
132}
133pub fn hermite_deriv(p0: f64, m0: f64, p1: f64, m1: f64, t: f64) -> f64 {
135 let t2 = t * t;
136 let dh00 = 6.0 * t2 - 6.0 * t;
137 let dh10 = 3.0 * t2 - 4.0 * t + 1.0;
138 let dh01 = -6.0 * t2 + 6.0 * t;
139 let dh11 = 3.0 * t2 - 2.0 * t;
140 dh00 * p0 + dh10 * m0 + dh01 * p1 + dh11 * m1
141}
142pub fn hermite3(p0: [f64; 3], m0: [f64; 3], p1: [f64; 3], m1: [f64; 3], t: f64) -> [f64; 3] {
144 [
145 hermite(p0[0], m0[0], p1[0], m1[0], t),
146 hermite(p0[1], m0[1], p1[1], m1[1], t),
147 hermite(p0[2], m0[2], p1[2], m1[2], t),
148 ]
149}
150pub fn catmull_rom(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3], p3: [f64; 3], t: f64) -> [f64; 3] {
155 let m1 = [
156 (p2[0] - p0[0]) * 0.5,
157 (p2[1] - p0[1]) * 0.5,
158 (p2[2] - p0[2]) * 0.5,
159 ];
160 let m2 = [
161 (p3[0] - p1[0]) * 0.5,
162 (p3[1] - p1[1]) * 0.5,
163 (p3[2] - p1[2]) * 0.5,
164 ];
165 hermite3(p1, m1, p2, m2, t)
166}
167pub fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
169 let dx = b[0] - a[0];
170 let dy = b[1] - a[1];
171 let dz = b[2] - a[2];
172 (dx * dx + dy * dy + dz * dz).sqrt()
173}
174pub fn bilinear_grid(
180 grid: &[Vec<f64>],
181 x_min: f64,
182 y_min: f64,
183 dx: f64,
184 dy: f64,
185 x: f64,
186 y: f64,
187) -> f64 {
188 let rows = grid.len();
189 if rows == 0 {
190 return 0.0;
191 }
192 let cols = grid[0].len();
193 if cols == 0 {
194 return 0.0;
195 }
196 let fx = ((x - x_min) / dx).clamp(0.0, (cols - 1) as f64);
197 let fy = ((y - y_min) / dy).clamp(0.0, (rows - 1) as f64);
198 let c0 = fx.floor() as usize;
199 let c1 = (c0 + 1).min(cols - 1);
200 let r0 = fy.floor() as usize;
201 let r1 = (r0 + 1).min(rows - 1);
202 let u = fx - c0 as f64;
203 let v = fy - r0 as f64;
204 bilinear(grid[r0][c0], grid[r0][c1], grid[r1][c0], grid[r1][c1], u, v)
205}
206#[allow(clippy::too_many_arguments)]
212pub fn trilinear_grid(
213 grid: &[Vec<Vec<f64>>],
214 x_min: f64,
215 y_min: f64,
216 z_min: f64,
217 dx: f64,
218 dy: f64,
219 dz: f64,
220 x: f64,
221 y: f64,
222 z: f64,
223) -> f64 {
224 let nz = grid.len();
225 if nz == 0 {
226 return 0.0;
227 }
228 let ny = grid[0].len();
229 if ny == 0 {
230 return 0.0;
231 }
232 let nx = grid[0][0].len();
233 if nx == 0 {
234 return 0.0;
235 }
236 let fi = ((x - x_min) / dx).clamp(0.0, (nx - 1) as f64);
237 let fj = ((y - y_min) / dy).clamp(0.0, (ny - 1) as f64);
238 let fk = ((z - z_min) / dz).clamp(0.0, (nz - 1) as f64);
239 let x0 = fi.floor() as usize;
240 let x1 = (x0 + 1).min(nx - 1);
241 let y0 = fj.floor() as usize;
242 let y1 = (y0 + 1).min(ny - 1);
243 let z0 = fk.floor() as usize;
244 let z1 = (z0 + 1).min(nz - 1);
245 let u = fi - x0 as f64;
246 let v = fj - y0 as f64;
247 let w = fk - z0 as f64;
248 let vals = [
249 grid[z0][y0][x0],
250 grid[z0][y0][x1],
251 grid[z0][y1][x0],
252 grid[z0][y1][x1],
253 grid[z1][y0][x0],
254 grid[z1][y0][x1],
255 grid[z1][y1][x0],
256 grid[z1][y1][x1],
257 ];
258 trilinear(&vals, u, v, w)
259}
260pub(super) fn bicubic_weight(t: f64) -> f64 {
262 let at = t.abs();
263 if at < 1.0 {
264 1.5 * at * at * at - 2.5 * at * at + 1.0
265 } else if at < 2.0 {
266 -0.5 * at * at * at + 2.5 * at * at - 4.0 * at + 2.0
267 } else {
268 0.0
269 }
270}
271pub fn bicubic(grid: &[Vec<f64>], x: f64, y: f64) -> f64 {
277 let rows = grid.len() as isize;
278 if rows == 0 {
279 return 0.0;
280 }
281 let cols = grid[0].len() as isize;
282 if cols == 0 {
283 return 0.0;
284 }
285 let get = |r: isize, c: isize| -> f64 {
286 let r = r.clamp(0, rows - 1) as usize;
287 let c = c.clamp(0, cols - 1) as usize;
288 grid[r][c]
289 };
290 let xi = x.floor() as isize;
291 let yi = y.floor() as isize;
292 let dx = x - xi as f64;
293 let dy = y - yi as f64;
294 let mut result = 0.0;
295 for m in -1_isize..=2 {
296 let wy = bicubic_weight(dy - m as f64);
297 for n in -1_isize..=2 {
298 let wx = bicubic_weight(dx - n as f64);
299 result += wx * wy * get(yi + m, xi + n);
300 }
301 }
302 result
303}
304pub fn barycentric_2d(p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> (f64, f64, f64) {
310 let denom = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]);
311 if denom.abs() < f64::EPSILON {
312 return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
313 }
314 let u = ((b[1] - c[1]) * (p[0] - c[0]) + (c[0] - b[0]) * (p[1] - c[1])) / denom;
315 let v = ((c[1] - a[1]) * (p[0] - c[0]) + (a[0] - c[0]) * (p[1] - c[1])) / denom;
316 let w = 1.0 - u - v;
317 (u, v, w)
318}
319pub fn barycentric_interp_2d(
322 p: [f64; 2],
323 a: [f64; 2],
324 b: [f64; 2],
325 c: [f64; 2],
326 fa: f64,
327 fb: f64,
328 fc: f64,
329) -> f64 {
330 let (u, v, w) = barycentric_2d(p, a, b, c);
331 u * fa + v * fb + w * fc
332}
333pub fn natural_neighbor_interp(points: &[[f64; 2]], values: &[f64], p: [f64; 2], k: usize) -> f64 {
344 let n = points.len();
345 if n == 0 || values.len() != n {
346 return 0.0;
347 }
348 let k = k.min(n).max(1);
349 let mut dists: Vec<(usize, f64)> = points
350 .iter()
351 .enumerate()
352 .map(|(i, q)| {
353 let dx = p[0] - q[0];
354 let dy = p[1] - q[1];
355 (i, dx * dx + dy * dy)
356 })
357 .collect();
358 dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
359 if dists[0].1 < f64::EPSILON * f64::EPSILON {
360 return values[dists[0].0];
361 }
362 let mut weight_sum = 0.0;
363 let mut weighted_val = 0.0;
364 for &(idx, d2) in dists.iter().take(k) {
365 let w = 1.0 / d2;
366 weight_sum += w;
367 weighted_val += w * values[idx];
368 }
369 if weight_sum < f64::EPSILON {
370 return 0.0;
371 }
372 weighted_val / weight_sum
373}
374pub fn nurbs_evaluate_with_derivative(
380 control_points: &[[f64; 3]],
381 weights: &[f64],
382 degree: usize,
383 knots: &[f64],
384 t: f64,
385) -> ([f64; 3], [f64; 3]) {
386 let n = control_points.len();
387 if n == 0 || weights.len() != n || knots.len() < n + degree + 1 {
388 return ([0.0; 3], [0.0; 3]);
389 }
390 let mut point = [0.0_f64; 3];
391 let mut deriv = [0.0_f64; 3];
392 let mut denom = 0.0_f64;
393 let mut denom_deriv = 0.0_f64;
394 for i in 0..n {
395 let b = BSplineBasis::basis(i, degree, t, knots);
396 let db = BSplineBasis::basis_derivative(i, degree, t, knots);
397 let w = weights[i];
398 let wb = w * b;
399 let wdb = w * db;
400 for k in 0..3 {
401 point[k] += wb * control_points[i][k];
402 deriv[k] += wdb * control_points[i][k];
403 }
404 denom += wb;
405 denom_deriv += wdb;
406 }
407 if denom.abs() < f64::EPSILON {
408 return ([0.0; 3], [0.0; 3]);
409 }
410 let inv_d = 1.0 / denom;
411 let pt = [point[0] * inv_d, point[1] * inv_d, point[2] * inv_d];
412 let tang = [
413 (deriv[0] - denom_deriv * pt[0]) * inv_d,
414 (deriv[1] - denom_deriv * pt[1]) * inv_d,
415 (deriv[2] - denom_deriv * pt[2]) * inv_d,
416 ];
417 (pt, tang)
418}
419pub fn rbf_thin_plate_spline(r: f64) -> f64 {
421 if r < f64::EPSILON {
422 return 0.0;
423 }
424 r * r * r.ln()
425}
426pub fn rbf_tps_evaluate(centers: &[[f64; 2]], weights: &[f64], p: [f64; 2]) -> f64 {
432 centers
433 .iter()
434 .zip(weights.iter())
435 .map(|(c, &w)| {
436 let dx = p[0] - c[0];
437 let dy = p[1] - c[1];
438 let r = (dx * dx + dy * dy).sqrt();
439 w * rbf_thin_plate_spline(r)
440 })
441 .sum()
442}
443pub fn rbf_tps_fit(centers: &[[f64; 2]], values: &[f64]) -> Result<Vec<f64>, String> {
447 let n = centers.len();
448 if n == 0 || values.len() != n {
449 return Err("centers and values must be non-empty and same length".into());
450 }
451 let mut mat: Vec<Vec<f64>> = (0..n)
452 .map(|i| {
453 (0..n)
454 .map(|j| {
455 let dx = centers[i][0] - centers[j][0];
456 let dy = centers[i][1] - centers[j][1];
457 let r = (dx * dx + dy * dy).sqrt();
458 rbf_thin_plate_spline(r)
459 })
460 .collect()
461 })
462 .collect();
463 let mut rhs = values.to_vec();
464 for col in 0..n {
465 let pivot = (col..n)
466 .max_by(|&a, &b| {
467 mat[a][col]
468 .abs()
469 .partial_cmp(&mat[b][col].abs())
470 .unwrap_or(std::cmp::Ordering::Equal)
471 })
472 .ok_or("RBF TPS matrix is singular")?;
473 if mat[pivot][col].abs() < 1e-14 {
474 return Err("RBF TPS matrix is singular or near-singular".into());
475 }
476 mat.swap(col, pivot);
477 rhs.swap(col, pivot);
478 let diag = mat[col][col];
479 for j in col..n {
480 mat[col][j] /= diag;
481 }
482 rhs[col] /= diag;
483 for row in 0..n {
484 if row == col {
485 continue;
486 }
487 let f = mat[row][col];
488 for j in col..n {
489 let v = mat[col][j] * f;
490 mat[row][j] -= v;
491 }
492 let rv = rhs[col] * f;
493 rhs[row] -= rv;
494 }
495 }
496 Ok(rhs)
497}
498pub fn monotone_cubic(xs: &[f64], ys: &[f64], x: f64) -> f64 {
512 let n = xs.len();
513 assert!(
514 n > 0 && n == ys.len(),
515 "xs and ys must be non-empty and equal length"
516 );
517 if n == 1 {
518 return ys[0];
519 }
520 if x <= xs[0] {
521 return ys[0];
522 }
523 if x >= xs[n - 1] {
524 return ys[n - 1];
525 }
526 let delta: Vec<f64> = (0..n - 1)
527 .map(|i| {
528 let h = xs[i + 1] - xs[i];
529 if h.abs() < f64::EPSILON {
530 0.0
531 } else {
532 (ys[i + 1] - ys[i]) / h
533 }
534 })
535 .collect();
536 let mut d: Vec<f64> = vec![0.0; n];
537 d[0] = delta[0];
538 d[n - 1] = delta[n - 2];
539 for i in 1..n - 1 {
540 d[i] = (delta[i - 1] + delta[i]) / 2.0;
541 }
542 for i in 0..n - 1 {
543 if delta[i].abs() < f64::EPSILON {
544 d[i] = 0.0;
545 d[i + 1] = 0.0;
546 continue;
547 }
548 let alpha = d[i] / delta[i];
549 let beta = d[i + 1] / delta[i];
550 let tau = alpha * alpha + beta * beta;
551 if tau > 9.0 {
552 let scale = 3.0 / tau.sqrt();
553 d[i] = scale * alpha * delta[i];
554 d[i + 1] = scale * beta * delta[i];
555 }
556 }
557 let seg = (0..n - 1).rev().find(|&i| x >= xs[i]).unwrap_or(0);
558 let h = xs[seg + 1] - xs[seg];
559 if h.abs() < f64::EPSILON {
560 return ys[seg];
561 }
562 let t = (x - xs[seg]) / h;
563 let t2 = t * t;
564 let t3 = t2 * t;
565 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
566 let h10 = t3 - 2.0 * t2 + t;
567 let h01 = -2.0 * t3 + 3.0 * t2;
568 let h11 = t3 - t2;
569 h00 * ys[seg] + h10 * h * d[seg] + h01 * ys[seg + 1] + h11 * h * d[seg + 1]
570}
571pub fn rbf_interpolate(centers: &[Vec<f64>], weights: &[f64], x: &[f64], kernel: RbfKernel) -> f64 {
582 centers
583 .iter()
584 .zip(weights.iter())
585 .map(|(c, &w)| {
586 let r = c
587 .iter()
588 .zip(x.iter())
589 .map(|(ci, xi)| (ci - xi).powi(2))
590 .sum::<f64>()
591 .sqrt();
592 w * kernel.eval(r)
593 })
594 .sum()
595}
596pub fn rbf_fit(
601 centers: &[Vec<f64>],
602 values: &[f64],
603 kernel: RbfKernel,
604) -> Result<Vec<f64>, String> {
605 let n = centers.len();
606 if n == 0 || values.len() != n {
607 return Err("centers and values must be non-empty and the same length".into());
608 }
609 let mut mat: Vec<Vec<f64>> = (0..n)
610 .map(|i| {
611 (0..n)
612 .map(|j| {
613 let r = centers[i]
614 .iter()
615 .zip(centers[j].iter())
616 .map(|(a, b)| (a - b).powi(2))
617 .sum::<f64>()
618 .sqrt();
619 kernel.eval(r)
620 })
621 .collect()
622 })
623 .collect();
624 let mut rhs = values.to_vec();
625 for col in 0..n {
626 let pivot = (col..n)
627 .max_by(|&a, &b| {
628 mat[a][col]
629 .abs()
630 .partial_cmp(&mat[b][col].abs())
631 .unwrap_or(std::cmp::Ordering::Equal)
632 })
633 .ok_or("RBF matrix is empty")?;
634 if mat[pivot][col].abs() < 1e-14 {
635 return Err("RBF kernel matrix is singular or near-singular".into());
636 }
637 mat.swap(col, pivot);
638 rhs.swap(col, pivot);
639 let diag = mat[col][col];
640 for j in col..n {
641 mat[col][j] /= diag;
642 }
643 rhs[col] /= diag;
644 for row in 0..n {
645 if row == col {
646 continue;
647 }
648 let f = mat[row][col];
649 for j in col..n {
650 let v = mat[col][j] * f;
651 mat[row][j] -= v;
652 }
653 let rv = rhs[col] * f;
654 rhs[row] -= rv;
655 }
656 }
657 Ok(rhs)
658}
659pub fn barycentric_rational(xs: &[f64], ys: &[f64], d: usize, x: f64) -> f64 {
679 let n = xs.len();
680 assert!(
681 n > 0 && n == ys.len(),
682 "xs and ys must be non-empty and equal length"
683 );
684 assert!(d < n, "blending parameter d must be < n");
685 for (i, &xi) in xs.iter().enumerate() {
686 if (x - xi).abs() < f64::EPSILON * 100.0 {
687 return ys[i];
688 }
689 }
690 let mut weights = vec![0.0_f64; n];
691 for i in 0..n {
692 let j_lo = i.saturating_sub(d);
693 let j_hi = (i + 1).min(n - d);
694 let sign = if i % 2 == 0 { 1.0_f64 } else { -1.0_f64 };
695 let mut s = 0.0_f64;
696 for j in j_lo..j_hi {
697 let k = i - j;
698 let mut c = 1.0_f64;
699 for l in 0..k {
700 c *= (d - l) as f64 / (l + 1) as f64;
701 }
702 s += c;
703 }
704 weights[i] = sign * s;
705 }
706 let mut num = 0.0_f64;
707 let mut den = 0.0_f64;
708 for i in 0..n {
709 let t = weights[i] / (x - xs[i]);
710 num += t * ys[i];
711 den += t;
712 }
713 num / den
714}
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use crate::AkimaSpline;
719 use crate::BSplineCurve;
720 use crate::CatmullRomSpline;
721 use crate::HermiteSpline;
722 use crate::MonotoneCubicSpline;
723 use crate::NaturalCubicSpline;
724 use crate::NurbsCurve;
725 use crate::RBFInterpolation;
726 pub(super) const EPS: f64 = 1e-9;
727 fn approx(a: f64, b: f64) -> bool {
728 (a - b).abs() < EPS
729 }
730 fn approx3(a: [f64; 3], b: [f64; 3]) -> bool {
731 approx(a[0], b[0]) && approx(a[1], b[1]) && approx(a[2], b[2])
732 }
733 fn approx_eq(a: f64, b: f64) -> bool {
734 approx(a, b)
735 }
736 fn approx_eq4(a: [f64; 4], b: [f64; 4]) -> bool {
737 approx(a[0], b[0]) && approx(a[1], b[1]) && approx(a[2], b[2]) && approx(a[3], b[3])
738 }
739 #[test]
740 fn test_lerp_at_zero_is_a() {
741 assert!(approx(lerp(3.0, 7.0, 0.0), 3.0));
742 }
743 #[test]
744 fn test_lerp_at_one_is_b() {
745 assert!(approx(lerp(3.0, 7.0, 1.0), 7.0));
746 }
747 #[test]
748 fn test_lerp_midpoint() {
749 assert!(approx(lerp(0.0, 1.0, 0.5), 0.5));
750 }
751 #[test]
752 fn test_bilinear_corners() {
753 assert!(approx(bilinear(1.0, 2.0, 3.0, 4.0, 0.0, 0.0), 1.0));
754 assert!(approx(bilinear(1.0, 2.0, 3.0, 4.0, 1.0, 0.0), 2.0));
755 assert!(approx(bilinear(1.0, 2.0, 3.0, 4.0, 0.0, 1.0), 3.0));
756 assert!(approx(bilinear(1.0, 2.0, 3.0, 4.0, 1.0, 1.0), 4.0));
757 }
758 #[test]
759 fn test_trilinear_corners() {
760 let vals = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
761 assert!(approx(trilinear(&vals, 0.0, 0.0, 0.0), 1.0));
762 assert!(approx(trilinear(&vals, 1.0, 1.0, 1.0), 8.0));
763 }
764 #[test]
765 fn test_slerp_t0() {
766 let a = quat_normalize([1.0, 0.0, 0.0, 0.0]);
767 let b = quat_normalize([0.0, 1.0, 0.0, 0.0]);
768 assert!(approx_eq4(quat_slerp(a, b, 0.0), a));
769 }
770 #[test]
771 fn test_slerp_t1() {
772 let a = quat_normalize([1.0, 0.0, 0.0, 0.0]);
773 let b = quat_normalize([0.0, 1.0, 0.0, 0.0]);
774 assert!(approx_eq4(quat_slerp(a, b, 1.0), b));
775 }
776 #[test]
777 fn test_hermite_spline_endpoints() {
778 let pts = vec![1.0, 3.0, 2.0];
779 let tans = vec![0.5, -0.5, 1.0];
780 let sp = HermiteSpline::new(pts, tans).unwrap();
781 assert!(approx(sp.evaluate(0.0), 1.0), "start = first point");
782 assert!(approx(sp.evaluate(2.0), 2.0), "end = last point");
783 }
784 #[test]
785 fn test_hermite_spline_passes_through_interior() {
786 let pts = vec![0.0, 1.0, 0.0];
787 let tans = vec![1.0, 0.0, -1.0];
788 let sp = HermiteSpline::new(pts, tans).unwrap();
789 assert!(approx(sp.evaluate(1.0), 1.0));
790 }
791 #[test]
792 fn test_hermite_spline_error_mismatch() {
793 let result = HermiteSpline::new(vec![1.0, 2.0], vec![0.0]);
794 assert!(result.is_err());
795 }
796 #[test]
797 fn test_catmull_rom_spline_passes_through_points() {
798 let pts = vec![
799 [0.0, 0.0, 0.0],
800 [1.0, 2.0, 0.0],
801 [3.0, 1.0, 0.0],
802 [4.0, 3.0, 0.0],
803 ];
804 let sp = CatmullRomSpline::new(pts.clone(), 0.5);
805 for (i, &p) in pts.iter().enumerate() {
806 let ev = sp.evaluate(i as f64);
807 assert!(approx3(ev, p), "t={i}: expected {p:?}, got {ev:?}");
808 }
809 }
810 #[test]
811 fn test_catmull_rom_arc_length_positive() {
812 let pts = vec![
813 [0.0, 0.0, 0.0],
814 [1.0, 0.0, 0.0],
815 [2.0, 0.0, 0.0],
816 [3.0, 0.0, 0.0],
817 ];
818 let sp = CatmullRomSpline::new(pts, 0.5);
819 let l = sp.arc_length(1000);
820 assert!(l > 0.0, "arc length should be positive");
821 }
822 #[test]
823 fn test_natural_cubic_spline_interpolates_at_knots() {
824 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0];
825 let ys = vec![0.0, 1.0, 0.0, 1.0, 0.0];
826 let sp = NaturalCubicSpline::fit(xs.clone(), ys.clone()).unwrap();
827 for (xi, yi) in xs.iter().zip(ys.iter()) {
828 let v = sp.evaluate(*xi);
829 assert!((v - yi).abs() < 1e-10, "at x={xi}: expected {yi}, got {v}");
830 }
831 }
832 #[test]
833 fn test_natural_cubic_spline_derivative_continuous() {
834 let xs = vec![0.0, 1.0, 2.0, 3.0];
835 let ys = vec![0.0, 1.0, 4.0, 9.0];
836 let sp = NaturalCubicSpline::fit(xs, ys).unwrap();
837 for &x in &[1.0_f64, 2.0_f64] {
838 let dl = sp.derivative(x - 1e-7);
839 let dr = sp.derivative(x + 1e-7);
840 assert!(
841 (dl - dr).abs() < 1e-5,
842 "derivative jump at x={x}: left={dl}, right={dr}"
843 );
844 }
845 }
846 #[test]
847 fn test_natural_cubic_spline_integral() {
848 let xs = vec![0.0, 1.0, 2.0];
849 let ys = vec![0.0, 1.0, 2.0];
850 let sp = NaturalCubicSpline::fit(xs, ys).unwrap();
851 let ig = sp.integral(0.0, 2.0);
852 assert!((ig - 2.0).abs() < 1e-10, "integral = {ig}");
853 }
854 #[test]
855 fn test_bspline_degree1_piecewise_linear() {
856 let pts = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
857 let curve = BSplineCurve::new(pts.clone(), 1);
858 let p0 = curve.evaluate(0.0);
859 assert!(approx3(p0, pts[0]), "start: {p0:?}");
860 let pn = curve.evaluate(1.0);
861 assert!(approx3(pn, *pts.last().unwrap()), "end: {pn:?}");
862 }
863 #[test]
864 fn test_bspline_endpoints_clamped() {
865 let pts = vec![
866 [0.0, 0.0, 0.0],
867 [1.0, 2.0, 0.0],
868 [3.0, 1.0, 0.0],
869 [4.0, 0.0, 0.0],
870 ];
871 let curve = BSplineCurve::new(pts.clone(), 3);
872 let p0 = curve.evaluate(0.0);
873 let pn = curve.evaluate(1.0);
874 assert!(approx3(p0, pts[0]), "clamped start: {p0:?}");
875 assert!(approx3(pn, *pts.last().unwrap()), "clamped end: {pn:?}");
876 }
877 #[test]
878 fn test_nurbs_unit_weights_equals_bspline() {
879 let pts = vec![
880 [0.0, 0.0, 0.0],
881 [1.0, 2.0, 0.0],
882 [3.0, 1.0, 0.0],
883 [4.0, 0.0, 0.0],
884 ];
885 let degree = 3;
886 let weights = vec![1.0; pts.len()];
887 let nurbs = NurbsCurve::new(pts.clone(), weights, degree);
888 let bspline = BSplineCurve::new(pts, degree);
889 for i in 0..=10 {
890 let t = i as f64 / 10.0;
891 let pn = nurbs.evaluate(t);
892 let pb = bspline.evaluate(t);
893 assert!(approx3(pn, pb), "t={t}: nurbs={pn:?}, bspline={pb:?}");
894 }
895 }
896 #[test]
897 fn test_rbf_exact_at_data_points() {
898 let points: Vec<[f64; 3]> = vec![
899 [0.0, 0.0, 0.0],
900 [1.0, 0.0, 0.0],
901 [0.0, 1.0, 0.0],
902 [0.0, 0.0, 1.0],
903 ];
904 let values = vec![1.0, 2.0, 3.0, 4.0];
905 let rbf = RBFInterpolation::fit(&points, &values, 1.0).unwrap();
906 for (p, &v) in points.iter().zip(values.iter()) {
907 let ev = rbf.evaluate(*p);
908 assert!((ev - v).abs() < 1e-6, "at {p:?}: expected {v}, got {ev}");
909 }
910 }
911 #[test]
912 fn test_bilinear_grid_at_corners() {
913 let grid = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
914 assert!(approx(
915 bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0),
916 1.0
917 ));
918 assert!(approx(
919 bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0),
920 2.0
921 ));
922 assert!(approx(
923 bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0),
924 3.0
925 ));
926 assert!(approx(
927 bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0),
928 4.0
929 ));
930 }
931 #[test]
932 fn test_bilinear_grid_midpoint() {
933 let grid = vec![vec![0.0, 0.0], vec![0.0, 4.0]];
934 let v = bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, 0.5, 0.5);
935 assert!((v - 1.0).abs() < 1e-9, "midpoint should be 1.0, got {v}");
936 }
937 #[test]
938 fn test_bilinear_grid_clamping() {
939 let grid = vec![vec![5.0, 6.0], vec![7.0, 8.0]];
940 let v = bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, -1.0, -1.0);
941 assert!(approx(v, 5.0), "clamped to (0,0) should be 5.0, got {v}");
942 }
943 #[test]
944 fn test_trilinear_grid_corners() {
945 let grid = vec![
946 vec![vec![1.0, 2.0], vec![3.0, 4.0]],
947 vec![vec![5.0, 6.0], vec![7.0, 8.0]],
948 ];
949 assert!(approx(
950 trilinear_grid(&grid, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0),
951 1.0
952 ));
953 assert!(approx(
954 trilinear_grid(&grid, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
955 8.0
956 ));
957 }
958 #[test]
959 fn test_bicubic_at_integer_coords() {
960 let grid = vec![
961 vec![1.0, 2.0, 3.0, 4.0],
962 vec![5.0, 6.0, 7.0, 8.0],
963 vec![9.0, 10.0, 11.0, 12.0],
964 vec![13.0, 14.0, 15.0, 16.0],
965 ];
966 let v = bicubic(&grid, 1.0, 1.0);
967 assert!(
968 (v - 6.0).abs() < 1e-6,
969 "bicubic at (1,1) should be ~6, got {v}"
970 );
971 }
972 #[test]
973 fn test_bicubic_interior_bounded() {
974 let grid = vec![
975 vec![0.0, 1.0, 0.0],
976 vec![1.0, 4.0, 1.0],
977 vec![0.0, 1.0, 0.0],
978 ];
979 let v = bicubic(&grid, 1.0, 1.0);
980 assert!(v > 0.0, "bicubic center should be positive, got {v}");
981 }
982 #[test]
983 fn test_barycentric_2d_centroid() {
984 let a = [0.0, 0.0];
985 let b = [1.0, 0.0];
986 let c = [0.0, 1.0];
987 let p = [1.0 / 3.0, 1.0 / 3.0];
988 let (u, v, w) = barycentric_2d(p, a, b, c);
989 assert!((u - 1.0 / 3.0).abs() < 1e-9);
990 assert!((v - 1.0 / 3.0).abs() < 1e-9);
991 assert!((w - 1.0 / 3.0).abs() < 1e-9);
992 assert!((u + v + w - 1.0).abs() < 1e-9);
993 }
994 #[test]
995 fn test_barycentric_2d_vertex_a() {
996 let a = [0.0, 0.0];
997 let b = [1.0, 0.0];
998 let c = [0.0, 1.0];
999 let (u, v, w) = barycentric_2d(a, a, b, c);
1000 assert!((u - 1.0).abs() < 1e-9);
1001 assert!(v.abs() < 1e-9);
1002 assert!(w.abs() < 1e-9);
1003 }
1004 #[test]
1005 fn test_barycentric_interp_2d_linear() {
1006 let a = [0.0, 0.0];
1007 let b = [1.0, 0.0];
1008 let c = [0.0, 1.0];
1009 let p = [0.5, 0.0];
1010 let v = barycentric_interp_2d(p, a, b, c, 0.0, 1.0, 2.0);
1011 assert!((v - 0.5).abs() < 1e-9, "expected 0.5, got {v}");
1012 }
1013 #[test]
1014 fn test_natural_neighbor_at_data_point() {
1015 let points = vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
1016 let values = vec![3.0, 5.0, 7.0];
1017 let v = natural_neighbor_interp(&points, &values, [0.0, 0.0], 3);
1018 assert!(
1019 (v - 3.0).abs() < 1e-9,
1020 "exact data point should return its value, got {v}"
1021 );
1022 }
1023 #[test]
1024 fn test_natural_neighbor_midpoint() {
1025 let points = vec![[0.0, 0.0], [2.0, 0.0]];
1026 let values = vec![0.0, 4.0];
1027 let v = natural_neighbor_interp(&points, &values, [1.0, 0.0], 2);
1028 assert!((v - 2.0).abs() < 1e-9, "midpoint should be 2.0, got {v}");
1029 }
1030 #[test]
1031 fn test_monotone_cubic_spline_interpolates_knots() {
1032 let xs = vec![0.0, 1.0, 2.0, 3.0];
1033 let ys = vec![0.0, 1.0, 4.0, 9.0];
1034 let sp = MonotoneCubicSpline::fit(xs.clone(), ys.clone()).unwrap();
1035 for (&x, &y) in xs.iter().zip(ys.iter()) {
1036 let v = sp.evaluate(x);
1037 assert!((v - y).abs() < 1e-9, "at x={x}: expected {y}, got {v}");
1038 }
1039 }
1040 #[test]
1041 fn test_monotone_cubic_spline_monotone() {
1042 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1043 let ys = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1044 let sp = MonotoneCubicSpline::fit(xs, ys).unwrap();
1045 let mut prev = sp.evaluate(0.0);
1046 for i in 1..=40 {
1047 let x = i as f64 * 0.1;
1048 let cur = sp.evaluate(x);
1049 assert!(
1050 cur >= prev - 1e-9,
1051 "monotone cubic should be non-decreasing at x={x}"
1052 );
1053 prev = cur;
1054 }
1055 }
1056 #[test]
1057 fn test_monotone_cubic_spline_clamped() {
1058 let xs = vec![0.0, 1.0];
1059 let ys = vec![2.0, 5.0];
1060 let sp = MonotoneCubicSpline::fit(xs, ys).unwrap();
1061 assert!(
1062 approx(sp.evaluate(-1.0), 2.0),
1063 "should clamp to first value"
1064 );
1065 assert!(approx(sp.evaluate(2.0), 5.0), "should clamp to last value");
1066 }
1067 #[test]
1068 fn test_akima_spline_interpolates_knots() {
1069 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
1070 let ys = vec![0.0, 1.0, 8.0, 27.0, 64.0, 125.0];
1071 let sp = AkimaSpline::fit(xs.clone(), ys.clone()).unwrap();
1072 for (&x, &y) in xs.iter().zip(ys.iter()) {
1073 let v = sp.evaluate(x);
1074 assert!((v - y).abs() < 1e-6, "at x={x}: expected {y}, got {v}");
1075 }
1076 }
1077 #[test]
1078 fn test_akima_spline_clamped() {
1079 let xs = vec![0.0, 1.0, 2.0];
1080 let ys = vec![1.0, 3.0, 2.0];
1081 let sp = AkimaSpline::fit(xs, ys).unwrap();
1082 assert!(approx(sp.evaluate(-5.0), 1.0));
1083 assert!(approx(sp.evaluate(10.0), 2.0));
1084 }
1085 #[test]
1086 fn test_akima_spline_error_on_unsorted() {
1087 assert!(AkimaSpline::fit(vec![1.0, 0.0], vec![0.0, 1.0]).is_err());
1088 }
1089 #[test]
1090 fn test_nurbs_with_derivative_endpoint() {
1091 let pts = vec![
1092 [0.0, 0.0, 0.0],
1093 [1.0, 2.0, 0.0],
1094 [3.0, 1.0, 0.0],
1095 [4.0, 0.0, 0.0],
1096 ];
1097 let weights = vec![1.0; 4];
1098 let degree = 3;
1099 let knots = BSplineCurve::uniform_knots(4, degree);
1100 let (pt, _tang) = nurbs_evaluate_with_derivative(&pts, &weights, degree, &knots, 0.0);
1101 assert!(
1102 approx3(pt, pts[0]),
1103 "t=0 should be first control point, got {pt:?}"
1104 );
1105 }
1106 #[test]
1107 fn test_nurbs_with_derivative_at_one() {
1108 let pts = vec![
1109 [0.0, 0.0, 0.0],
1110 [1.0, 2.0, 0.0],
1111 [3.0, 1.0, 0.0],
1112 [4.0, 0.0, 0.0],
1113 ];
1114 let weights = vec![1.0; 4];
1115 let degree = 3;
1116 let knots = BSplineCurve::uniform_knots(4, degree);
1117 let (pt, _tang) = nurbs_evaluate_with_derivative(&pts, &weights, degree, &knots, 1.0);
1118 assert!(
1119 approx3(pt, *pts.last().unwrap()),
1120 "t=1 should be last control point, got {pt:?}"
1121 );
1122 }
1123 #[test]
1124 fn test_rbf_tps_fit_and_evaluate_exact() {
1125 let centers = vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1126 let values = vec![0.0, 1.0, 2.0, 3.0];
1127 let weights = rbf_tps_fit(¢ers, &values).unwrap();
1128 for (c, &v) in centers.iter().zip(values.iter()) {
1129 let ev = rbf_tps_evaluate(¢ers, &weights, *c);
1130 assert!((ev - v).abs() < 1e-5, "at {c:?}: expected {v}, got {ev}");
1131 }
1132 }
1133 #[test]
1134 fn test_rbf_tps_kernel_zero_at_origin() {
1135 assert!(rbf_thin_plate_spline(0.0).abs() < 1e-15);
1136 }
1137 #[test]
1138 fn test_lerp_endpoints_legacy() {
1139 assert!(approx_eq(lerp(3.0, 7.0, 0.0), 3.0));
1140 assert!(approx_eq(lerp(3.0, 7.0, 1.0), 7.0));
1141 }
1142 #[test]
1143 fn test_hermite_endpoints_legacy() {
1144 assert!(approx_eq(hermite(1.0, 0.5, 3.0, 0.5, 0.0), 1.0));
1145 assert!(approx_eq(hermite(1.0, 0.5, 3.0, 0.5, 1.0), 3.0));
1146 }
1147 #[test]
1148 fn test_catmull_rom_endpoints_legacy() {
1149 let p0 = [0.0, 0.0, 0.0];
1150 let p1 = [1.0, 0.0, 0.0];
1151 let p2 = [2.0, 1.0, 0.0];
1152 let p3 = [3.0, 0.0, 0.0];
1153 assert!(approx3(catmull_rom(p0, p1, p2, p3, 0.0), p1));
1154 assert!(approx3(catmull_rom(p0, p1, p2, p3, 1.0), p2));
1155 }
1156 #[test]
1157 fn test_lerp_extrapolation() {
1158 assert!(approx(lerp(0.0, 10.0, 2.0), 20.0));
1159 assert!(approx(lerp(0.0, 10.0, -1.0), -10.0));
1160 }
1161 #[test]
1162 fn test_lerp3_component_wise() {
1163 let a = [1.0, 2.0, 3.0];
1164 let b = [3.0, 4.0, 5.0];
1165 let mid = lerp3(a, b, 0.5);
1166 assert!(approx3(mid, [2.0, 3.0, 4.0]));
1167 }
1168 #[test]
1169 fn test_bilinear_centre() {
1170 let v = bilinear(1.0, 3.0, 5.0, 7.0, 0.5, 0.5);
1171 assert!(approx(v, (1.0 + 3.0 + 5.0 + 7.0) / 4.0));
1172 }
1173 #[test]
1174 fn test_trilinear_unit_cube_centre() {
1175 let vals = [0.0_f64; 8];
1176 assert!(approx(trilinear(&vals, 0.5, 0.5, 0.5), 0.0));
1177 }
1178 #[test]
1179 fn test_natural_cubic_spline_extrapolation_left() {
1180 let xs = vec![0.0, 1.0, 2.0];
1181 let ys = vec![0.0, 1.0, 4.0];
1182 let sp = NaturalCubicSpline::fit(xs, ys).unwrap();
1183 let v = sp.evaluate(-1.0);
1184 assert!(v.is_finite(), "extrapolated value should be finite");
1185 }
1186 #[test]
1187 fn test_natural_cubic_spline_extrapolation_right() {
1188 let xs = vec![0.0, 1.0, 2.0];
1189 let ys = vec![0.0, 1.0, 4.0];
1190 let sp = NaturalCubicSpline::fit(xs, ys).unwrap();
1191 let v = sp.evaluate(10.0);
1192 assert!(v.is_finite(), "extrapolated value should be finite");
1193 }
1194 #[test]
1195 fn test_natural_cubic_spline_monotone_input() {
1196 let xs: Vec<f64> = (0..5).map(|i| i as f64).collect();
1197 let ys: Vec<f64> = xs.iter().map(|x| x * x).collect();
1198 let sp = NaturalCubicSpline::fit(xs.clone(), ys).unwrap();
1199 for &x in &[0.5_f64, 1.5, 2.5, 3.5] {
1200 let v = sp.evaluate(x);
1201 let expected = x * x;
1202 assert!(
1203 (v - expected).abs() < 0.2,
1204 "x={x}: spline={v}, parabola={expected}"
1205 );
1206 }
1207 }
1208 #[test]
1209 fn test_natural_cubic_spline_too_few_points_error() {
1210 assert!(NaturalCubicSpline::fit(vec![1.0], vec![1.0]).is_err());
1211 }
1212 #[test]
1213 fn test_nurbs_midpoint_inside_hull() {
1214 let pts = vec![[0.0, 0.0, 0.0], [2.0, 2.0, 0.0], [4.0, 0.0, 0.0]];
1215 let weights = vec![1.0; 3];
1216 let nurbs = NurbsCurve::new(pts, weights, 2);
1217 let mid = nurbs.evaluate(0.5);
1218 assert!(mid[0] > 0.0 && mid[0] < 4.0, "x out of range: {}", mid[0]);
1219 assert!(mid[1] >= 0.0, "y should be non-negative: {}", mid[1]);
1220 }
1221 #[test]
1222 fn test_nurbs_evaluate_multiple_interior_points() {
1223 let pts = vec![[0.0, 0.0, 0.0], [1.0, 2.0, 0.0], [2.0, 0.0, 0.0]];
1224 let weights = vec![1.0; 3];
1225 let nurbs = NurbsCurve::new(pts, weights, 2);
1226 for i in 0..=10 {
1227 let t = i as f64 / 10.0;
1228 let p = nurbs.evaluate(t);
1229 assert!(
1230 p[0].is_finite() && p[1].is_finite(),
1231 "t={t}: non-finite point {p:?}"
1232 );
1233 }
1234 }
1235 #[test]
1236 fn test_bspline_evaluates_midpoint() {
1237 let pts = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
1238 let curve = BSplineCurve::new(pts, 2);
1239 let mid = curve.evaluate(0.5);
1240 assert!(mid[0] > 0.0 && mid[0] < 2.0, "mid[0]={}", mid[0]);
1241 }
1242 #[test]
1243 fn test_bspline_evaluates_many_t() {
1244 let pts = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
1245 let curve = BSplineCurve::new(pts, 2);
1246 for i in 0..=10 {
1247 let t = i as f64 / 10.0;
1248 let p = curve.evaluate(t);
1249 assert!(
1250 p[0].is_finite() && p[1].is_finite(),
1251 "t={t}: non-finite {p:?}"
1252 );
1253 }
1254 }
1255 #[test]
1256 fn test_rbf_interpolation_constant() {
1257 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1258 let vals = vec![5.0, 5.0, 5.0];
1259 let rbf = RBFInterpolation::fit(&pts, &vals, 1.0).unwrap();
1260 for (p, &v) in pts.iter().zip(vals.iter()) {
1261 let ev = rbf.evaluate(*p);
1262 assert!(
1263 (ev - v).abs() < 1e-4,
1264 "RBF at data point: expected {v}, got {ev}"
1265 );
1266 }
1267 }
1268 #[test]
1269 fn test_pchip_constant_data() {
1270 let xs = vec![0.0, 1.0, 2.0, 3.0];
1271 let ys = vec![7.0, 7.0, 7.0, 7.0];
1272 let sp = MonotoneCubicSpline::fit(xs, ys).unwrap();
1273 for &x in &[0.3_f64, 1.0, 1.7, 2.5] {
1274 assert!(approx(sp.evaluate(x), 7.0));
1275 }
1276 }
1277 #[test]
1278 fn test_pchip_decreasing_data_is_monotone() {
1279 let xs = vec![0.0, 1.0, 2.0, 3.0];
1280 let ys = vec![4.0, 3.0, 2.0, 1.0];
1281 let sp = MonotoneCubicSpline::fit(xs, ys).unwrap();
1282 let mut prev = sp.evaluate(0.0);
1283 for i in 1..=30 {
1284 let x = i as f64 * 0.1;
1285 let cur = sp.evaluate(x);
1286 assert!(
1287 cur <= prev + 1e-9,
1288 "PCHIP should be non-increasing at x={x}"
1289 );
1290 prev = cur;
1291 }
1292 }
1293 #[test]
1294 fn test_akima_spline_linear_data() {
1295 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
1296 let ys: Vec<f64> = xs.iter().map(|&x| 2.0 * x + 1.0).collect();
1297 let sp = AkimaSpline::fit(xs, ys.clone()).unwrap();
1298 for &x in &[0.5_f64, 1.5, 2.5, 3.5, 4.5] {
1299 let v = sp.evaluate(x);
1300 let expected = 2.0 * x + 1.0;
1301 assert!(
1302 (v - expected).abs() < 1e-6,
1303 "Akima linear at x={x}: {v} != {expected}"
1304 );
1305 }
1306 }
1307 #[test]
1308 fn test_akima_interpolates_all_knots() {
1309 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
1310 let ys = vec![1.0, -1.0, 2.0, 0.5, 3.0, 1.5];
1311 let sp = AkimaSpline::fit(xs.clone(), ys.clone()).unwrap();
1312 for (&x, &y) in xs.iter().zip(ys.iter()) {
1313 let v = sp.evaluate(x);
1314 assert!(
1315 (v - y).abs() < 1e-8,
1316 "Akima at x={x}: expected {y}, got {v}"
1317 );
1318 }
1319 }
1320 #[test]
1321 fn test_hermite_midpoint() {
1322 let v = hermite(0.0, 0.0, 2.0, 0.0, 0.5);
1323 assert!((v - 1.0).abs() < 0.1, "Hermite midpoint approx: {v}");
1324 }
1325 #[test]
1326 fn test_hermite3_endpoints() {
1327 let p0 = [0.0, 0.0, 0.0];
1328 let m0 = [1.0, 0.0, 0.0];
1329 let p1 = [1.0, 1.0, 0.0];
1330 let m1 = [0.0, 1.0, 0.0];
1331 let r0 = hermite3(p0, m0, p1, m1, 0.0);
1332 let r1 = hermite3(p0, m0, p1, m1, 1.0);
1333 assert!(approx3(r0, p0), "hermite3 at t=0: {r0:?}");
1334 assert!(approx3(r1, p1), "hermite3 at t=1: {r1:?}");
1335 }
1336 #[test]
1337 fn test_quat_normalize_unit() {
1338 let q = [3.0, 1.0, 2.0, 1.0];
1339 let n = quat_normalize(q);
1340 let norm = quat_norm(n);
1341 assert!((norm - 1.0).abs() < 1e-12, "normalized quat norm={norm}");
1342 }
1343 #[test]
1344 fn test_quat_nlerp_t_half() {
1345 let a = [1.0, 0.0, 0.0, 0.0];
1346 let b = [0.0, 1.0, 0.0, 0.0];
1347 let mid = quat_nlerp(a, b, 0.5);
1348 let norm = quat_norm(mid);
1349 assert!(
1350 (norm - 1.0).abs() < 1e-10,
1351 "nlerp result should be unit: {norm}"
1352 );
1353 }
1354 #[test]
1355 fn test_slerp_unit_result() {
1356 let a = quat_normalize([1.0, 0.5, 0.0, 0.0]);
1357 let b = quat_normalize([0.0, 0.0, 1.0, 0.5]);
1358 for t_int in 0..=10 {
1359 let t = t_int as f64 / 10.0;
1360 let r = quat_slerp(a, b, t);
1361 let norm = quat_norm(r);
1362 assert!((norm - 1.0).abs() < 1e-10, "slerp at t={t}: norm={norm}");
1363 }
1364 }
1365 #[test]
1366 fn test_bilinear_grid_linear_x() {
1367 let grid = vec![vec![0.0, 1.0], vec![0.0, 1.0]];
1368 for i in 0..=10 {
1369 let x = i as f64 / 10.0;
1370 let v = bilinear_grid(&grid, 0.0, 0.0, 1.0, 1.0, x, 0.5);
1371 assert!(
1372 (v - x).abs() < 1e-12,
1373 "bilinear_grid linear x at x={x}: {v}"
1374 );
1375 }
1376 }
1377 #[test]
1378 fn test_trilinear_grid_z_linear() {
1379 let grid = vec![
1380 vec![vec![0.0, 0.0], vec![0.0, 0.0]],
1381 vec![vec![1.0, 1.0], vec![1.0, 1.0]],
1382 ];
1383 for i in 0..=10 {
1384 let z = i as f64 / 10.0;
1385 let v = trilinear_grid(&grid, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.5, 0.5, z);
1386 assert!((v - z).abs() < 1e-12, "trilinear z-linear at z={z}: {v}");
1387 }
1388 }
1389 #[test]
1390 fn test_monotone_cubic_interpolates_knots() {
1391 let xs = vec![0.0, 1.0, 2.0, 3.0];
1392 let ys = vec![0.0, 1.0, 4.0, 9.0];
1393 for (xi, yi) in xs.iter().zip(ys.iter()) {
1394 let v = monotone_cubic(&xs, &ys, *xi);
1395 assert!(
1396 (v - yi).abs() < 1e-10,
1397 "pchip at x={xi}: got {v}, expected {yi}"
1398 );
1399 }
1400 }
1401 #[test]
1402 fn test_monotone_cubic_linear_data_exact() {
1403 let xs: Vec<f64> = (0..6).map(|i| i as f64).collect();
1404 let ys: Vec<f64> = xs.iter().map(|&x| 2.0 * x + 1.0).collect();
1405 for i in 0..=20 {
1406 let x = i as f64 * 0.25;
1407 let expected = 2.0 * x + 1.0;
1408 let v = monotone_cubic(&xs, &ys, x);
1409 assert!(
1410 (v - expected).abs() < 1e-9,
1411 "monotone_cubic linear at x={x}: got {v}, expected {expected}"
1412 );
1413 }
1414 }
1415 #[test]
1416 fn test_monotone_cubic_monotone_on_monotone_data() {
1417 let xs = vec![0.0, 1.0, 2.0, 4.0, 8.0];
1418 let ys = vec![0.0, 0.5, 1.0, 1.5, 2.0];
1419 let mut prev = monotone_cubic(&xs, &ys, 0.0);
1420 for i in 1..=40 {
1421 let x = i as f64 * 0.2;
1422 if x > 8.0 {
1423 break;
1424 }
1425 let v = monotone_cubic(&xs, &ys, x);
1426 assert!(
1427 v >= prev - 1e-10,
1428 "monotone_cubic should be non-decreasing at x={x}: {prev} -> {v}"
1429 );
1430 prev = v;
1431 }
1432 }
1433 #[test]
1434 fn test_monotone_cubic_extrapolates_to_boundary() {
1435 let xs = vec![1.0, 2.0, 3.0];
1436 let ys = vec![10.0, 20.0, 30.0];
1437 assert!((monotone_cubic(&xs, &ys, 0.0) - 10.0).abs() < 1e-12);
1438 assert!((monotone_cubic(&xs, &ys, 5.0) - 30.0).abs() < 1e-12);
1439 }
1440 #[test]
1441 fn test_rbf_gaussian_interpolates_centres() {
1442 let centers = vec![vec![0.0_f64], vec![1.0], vec![2.0]];
1443 let values = vec![1.0, 2.0, 3.0];
1444 let kernel = RbfKernel::Gaussian(1.0);
1445 let weights = rbf_fit(¢ers, &values, kernel).expect("RBF fit should succeed");
1446 for (c, &v) in centers.iter().zip(values.iter()) {
1447 let pred = rbf_interpolate(¢ers, &weights, c, kernel);
1448 assert!(
1449 (pred - v).abs() < 1e-8,
1450 "RBF at centre {}: pred={pred}, expected={v}",
1451 c[0]
1452 );
1453 }
1454 }
1455 #[test]
1456 fn test_rbf_multiquadric_fit_1d() {
1457 let centers: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64]).collect();
1458 let values: Vec<f64> = centers.iter().map(|c| c[0] * c[0]).collect();
1459 let kernel = RbfKernel::Multiquadric(0.5);
1460 let weights = rbf_fit(¢ers, &values, kernel).expect("fit should succeed");
1461 for (c, &v) in centers.iter().zip(values.iter()) {
1462 let pred = rbf_interpolate(¢ers, &weights, c, kernel);
1463 assert!(
1464 (pred - v).abs() < 1e-6,
1465 "MQ RBF at {}: pred={pred}, expected={v}",
1466 c[0]
1467 );
1468 }
1469 }
1470 #[test]
1471 fn test_rbf_fit_empty_returns_error() {
1472 let centers: Vec<Vec<f64>> = vec![];
1473 let values: Vec<f64> = vec![];
1474 let result = rbf_fit(¢ers, &values, RbfKernel::Gaussian(1.0));
1475 assert!(result.is_err(), "empty input should return error");
1476 }
1477 #[test]
1478 fn test_barycentric_rational_interpolates_nodes() {
1479 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1480 let ys: Vec<f64> = xs.iter().map(|&x| x * x - x + 1.0).collect();
1481 for (xi, yi) in xs.iter().zip(ys.iter()) {
1482 let v = barycentric_rational(&xs, &ys, 2, *xi);
1483 assert!(
1484 (v - yi).abs() < 1e-8,
1485 "barycentric_rational at x={xi}: got {v}, expected {yi}"
1486 );
1487 }
1488 }
1489 #[test]
1490 fn test_barycentric_rational_d0_is_berrut() {
1491 let xs = vec![0.0, 1.0, 2.0, 3.0];
1492 let ys = vec![1.0, 4.0, 2.0, 5.0];
1493 for (xi, yi) in xs.iter().zip(ys.iter()) {
1494 let v = barycentric_rational(&xs, &ys, 0, *xi);
1495 assert!(
1496 (v - yi).abs() < 1e-8,
1497 "Berrut (d=0) at x={xi}: got {v}, expected {yi}"
1498 );
1499 }
1500 }
1501 #[test]
1502 fn test_barycentric_rational_smooth_function() {
1503 let n = 8;
1504 let xs: Vec<f64> = (0..n)
1505 .map(|i| i as f64 * std::f64::consts::PI / (n - 1) as f64)
1506 .collect();
1507 let ys: Vec<f64> = xs.iter().map(|&x| x.sin()).collect();
1508 for i in 0..n - 1 {
1509 let x = (xs[i] + xs[i + 1]) / 2.0;
1510 let exact = x.sin();
1511 let v = barycentric_rational(&xs, &ys, 3, x);
1512 assert!(
1513 (v - exact).abs() < 0.01,
1514 "barycentric_rational sin at x={x:.4}: got {v:.6}, exact={exact:.6}"
1515 );
1516 }
1517 }
1518}