Skip to main content

oxiphysics_core/interpolation/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::types::{BSplineBasis, RbfKernel};
7
8/// Linear interpolation between two scalars.
9///
10/// Returns `a` at `t = 0` and `b` at `t = 1`.
11pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
12    a + (b - a) * t
13}
14/// Component-wise linear interpolation between two 3-vectors.
15pub 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}
22/// Bilinear interpolation on a unit square.
23///
24/// `f00` = value at (0,0), `f10` at (1,0), `f01` at (0,1), `f11` at (1,1).
25/// `u` and `v` are parameters in \[0, 1\].
26pub 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}
29/// Trilinear interpolation on a unit cube.
30///
31/// `values` is ordered as `[f000, f100, f010, f110, f001, f101, f011, f111]`
32/// where the index bits are (w-bit, v-bit, u-bit).
33/// `u`, `v`, `w` are parameters in \[0, 1\].
34pub 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}
43/// Dot product of two quaternions.
44pub 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}
47/// Euclidean norm of a quaternion.
48pub fn quat_norm(q: [f64; 4]) -> f64 {
49    quat_dot(q, q).sqrt()
50}
51/// Normalize a quaternion to unit length.
52pub 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}
59/// Normalized linear interpolation (nlerp) of two quaternions.
60///
61/// Fast approximation of slerp; constant angular velocity is not guaranteed.
62pub 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}
76/// Spherical linear interpolation (slerp) between two unit quaternions.
77///
78/// Falls back to `quat_nlerp` when the quaternions are nearly parallel
79/// (`|dot| > 0.9995`).
80pub 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}
104/// SQUAD — smooth quaternion spline interpolation.
105///
106/// Interpolates between `q1` and `q2` using auxiliary control points derived
107/// from `q0` and `q3`. `t` ∈ \[0, 1\].
108pub 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}
114/// Compute the inner quadrangle point for SQUAD.
115pub(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}
121/// Cubic Hermite interpolation (scalar).
122///
123/// Blends position and tangent values: H00·p0 + H10·m0 + H01·p1 + H11·m1.
124pub 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}
133/// Derivative of cubic Hermite at local parameter `t`.
134pub 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}
142/// Cubic Hermite interpolation (3-vector).
143pub 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}
150/// Catmull-Rom segment between `p1` and `p2` using finite-difference tangents
151/// derived from `p0` and `p3`.
152///
153/// At `t = 0` the result is `p1`; at `t = 1` it is `p2`.
154pub 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}
167/// Helper: distance between two 3-D points.
168pub 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}
174/// Bilinear interpolation on a regular 2-D grid of values.
175///
176/// `grid[row][col]` holds the value at `(x_min + col*dx, y_min + row*dy)`.
177/// Returns the linearly interpolated value at `(x, y)`.
178/// Clamps `(x, y)` to the grid boundary.
179pub 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/// Trilinear interpolation on a regular 3-D grid.
207///
208/// `grid[z][y][x]` holds the value at grid index `(x, y, z)`.
209/// Grid coordinates start at `(x_min, y_min, z_min)` with steps `(dx, dy, dz)`.
210/// Clamps to the grid boundary.
211#[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}
260/// Cubic polynomial weight for the Catmull-Rom / Keys kernel.
261pub(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}
271/// Bicubic interpolation using a 4×4 stencil on a regular 2-D grid.
272///
273/// `grid[row][col]` is indexed as `(y, x)`.  Grid spacing is assumed uniform
274/// with spacing 1 (normalized coordinates); `x` and `y` are in `[0, cols-1]`
275/// and `[0, rows-1]` respectively.  Out-of-bounds accesses are clamped.
276pub 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}
304/// Compute the barycentric coordinates `(u, v, w)` of point `p` with respect
305/// to triangle `(a, b, c)` in 2-D.
306///
307/// Returns `(u, v, w)` such that `p = u·a + v·b + w·c` and `u + v + w = 1`.
308/// If the triangle is degenerate (zero area) all coordinates are `1/3`.
309pub 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}
319/// Interpolate a scalar at point `p` inside triangle `(a, b, c)` with values
320/// `(fa, fb, fc)` using barycentric coordinates.
321pub 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}
333/// Natural neighbor interpolation using a simplified area-based Sibson weight.
334///
335/// For each query point `p`, computes inverse-distance-squared weights to the
336/// `k` nearest supplied data points (a practical stand-in for true Sibson
337/// weights when no Voronoi library is available).
338///
339/// `points` — 2-D scatter positions.
340/// `values` — scalar value at each point.
341/// `p`      — query location.
342/// `k`      — number of nearest neighbours to use (clamped to `n`).
343pub 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}
374/// Evaluate a NURBS curve using the Cox-de Boor algorithm and return the
375/// point together with its first derivative via automatic differentiation.
376///
377/// `t` is clamped to the valid knot span `[knots[degree\], knots[n - degree - 1 + 1]]`.
378/// Returns `(point, tangent)` as two 3-D arrays.
379pub 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}
419/// Thin-plate spline RBF: `r² · ln(r)` (convention: 0 at r=0).
420pub fn rbf_thin_plate_spline(r: f64) -> f64 {
421    if r < f64::EPSILON {
422        return 0.0;
423    }
424    r * r * r.ln()
425}
426/// Evaluate a 2-D RBF interpolant using the thin-plate spline kernel.
427///
428/// `centers` — 2-D scatter positions.
429/// `weights` — fitted weights (same length as `centers`).
430/// `p`        — query point.
431pub 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}
443/// Fit a 2-D RBF interpolant with the thin-plate spline kernel.
444///
445/// Returns fitted weights or an error if the system is singular.
446pub 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}
498/// Monotone cubic (PCHIP / Fritsch-Carlson) interpolation.
499///
500/// Given data points `(xs[i], ys[i])` sorted by `xs`, computes the
501/// Fritsch-Carlson derivative estimates at each knot, then evaluates the
502/// piecewise Hermite cubic at query point `x`.
503///
504/// The Fritsch-Carlson method modifies the local finite-difference slopes so
505/// that the interpolant is monotone within each interval where the data is
506/// monotone.  This avoids the spurious oscillations that can occur with
507/// unconstrained cubic splines.
508///
509/// # Panics
510/// Panics if `xs` is empty or `xs.len() != ys.len()`.
511pub 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}
571/// Evaluate an RBF interpolant at query point `x`.
572///
573/// # Arguments
574/// * `centers` - Slice of centre vectors (each of the same dimensionality).
575/// * `weights` - Fitted RBF weights (one per centre).
576/// * `x`       - Query point vector (same dimensionality as centres).
577/// * `kernel`  - The RBF kernel to use.
578///
579/// # Returns
580/// The interpolated value `sum_i weights[i] * phi(||x - centers[i]||)`.
581pub 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}
596/// Fit RBF weights by solving the interpolation system Φ w = f.
597///
598/// Uses Gaussian elimination with partial pivoting.
599/// Returns an error if the kernel matrix is singular.
600pub 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}
659/// Floater-Hormann family of barycentric rational interpolants.
660///
661/// Computes the interpolant of blending parameter `d` at query point `x`.
662///
663/// The Floater-Hormann weights are:
664/// ```text
665/// w_i = (-1)^i * sum_{j=max(0,i-d)}^{min(i,n-1-d)} C(d, i-j)
666/// ```
667/// giving an interpolant of rational type with no poles on the real line
668/// and convergence rate O(h^(d+1)).
669///
670/// Setting `d = 0` gives the Berrut interpolant; larger `d` gives higher
671/// accuracy on smooth functions.
672///
673/// # Arguments
674/// * `xs` - Distinct, sorted abscissae.
675/// * `ys` - Function values at `xs`.
676/// * `d`  - Blending parameter in `0..xs.len()`.
677/// * `x`  - Query point.
678pub 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(&centers, &values).unwrap();
1128        for (c, &v) in centers.iter().zip(values.iter()) {
1129            let ev = rbf_tps_evaluate(&centers, &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(&centers, &values, kernel).expect("RBF fit should succeed");
1446        for (c, &v) in centers.iter().zip(values.iter()) {
1447            let pred = rbf_interpolate(&centers, &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(&centers, &values, kernel).expect("fit should succeed");
1461        for (c, &v) in centers.iter().zip(values.iter()) {
1462            let pred = rbf_interpolate(&centers, &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(&centers, &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}