Skip to main content

oxiphysics_core/
numerical_methods_ext.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3//! Extended numerical methods: integral equations, BEM, radial basis functions.
4//!
5//! Provides high-order quadrature, spline interpolation, RBF interpolation,
6//! barycentric interpolation, and Richardson-extrapolated differentiation.
7
8#![allow(dead_code)]
9
10use std::f64::consts::FRAC_1_SQRT_2;
11
12/// Gauss-Legendre quadrature rule with `n` nodes on \[-1, 1\].
13///
14/// Exact for polynomials of degree up to 2n-1.
15pub struct GaussLegendreQuad {
16    /// Number of quadrature nodes.
17    pub n: usize,
18    /// Quadrature nodes on \[-1, 1\].
19    pub nodes: Vec<f64>,
20    /// Quadrature weights summing to 2.
21    pub weights: Vec<f64>,
22}
23
24impl GaussLegendreQuad {
25    /// Construct a Gauss-Legendre rule with `n` points (2 ≤ n ≤ 20).
26    pub fn new(n: usize) -> Self {
27        let (nodes, weights) = gauss_legendre_nodes_weights(n);
28        Self { n, nodes, weights }
29    }
30
31    /// Integrate `f` over \[a, b\] using the n-point rule.
32    pub fn integrate<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F) -> f64 {
33        let mid = 0.5 * (a + b);
34        let half = 0.5 * (b - a);
35        self.nodes
36            .iter()
37            .zip(self.weights.iter())
38            .map(|(&x, &w)| w * f(mid + half * x))
39            .sum::<f64>()
40            * half
41    }
42
43    /// Composite Gauss-Legendre integration with `m` subintervals.
44    pub fn integrate_composite<F: Fn(f64) -> f64>(&self, a: f64, b: f64, m: usize, f: &F) -> f64 {
45        let h = (b - a) / m as f64;
46        (0..m)
47            .map(|i| {
48                let ai = a + i as f64 * h;
49                let bi = ai + h;
50                self.integrate(ai, bi, f)
51            })
52            .sum()
53    }
54}
55
56/// Compute Gauss-Legendre nodes and weights using hardcoded tables for n ≤ 9,
57/// and Newton iteration on Legendre polynomials for larger n.
58fn gauss_legendre_nodes_weights(n: usize) -> (Vec<f64>, Vec<f64>) {
59    match n {
60        1 => (vec![0.0], vec![2.0]),
61        2 => (
62            vec![-0.5773502691896257, 0.5773502691896257],
63            vec![1.0, 1.0],
64        ),
65        3 => (
66            vec![-0.7745966692414834, 0.0, 0.7745966692414834],
67            vec![0.5555555555555556, 0.8888888888888888, 0.5555555555555556],
68        ),
69        4 => (
70            vec![
71                -0.8611363115940526,
72                -0.3399810435848563,
73                0.3399810435848563,
74                0.8611363115940526,
75            ],
76            vec![
77                0.3478548451374538,
78                0.6521451548625461,
79                0.6521451548625461,
80                0.3478548451374538,
81            ],
82        ),
83        5 => (
84            vec![
85                -0.906_179_845_938_664,
86                -0.5384693101056831,
87                0.0,
88                0.5384693101056831,
89                0.906_179_845_938_664,
90            ],
91            vec![
92                0.2369268850561891,
93                0.4786286704993665,
94                0.5688888888888889,
95                0.4786286704993665,
96                0.2369268850561891,
97            ],
98        ),
99        6 => (
100            vec![
101                -0.932_469_514_203_152,
102                -0.6612093864662645,
103                -0.2386191860831969,
104                0.2386191860831969,
105                0.6612093864662645,
106                0.932_469_514_203_152,
107            ],
108            vec![
109                0.1713244923791704,
110                0.3607615730481386,
111                0.467_913_934_572_691,
112                0.467_913_934_572_691,
113                0.3607615730481386,
114                0.1713244923791704,
115            ],
116        ),
117        7 => (
118            vec![
119                -0.9491079123427585,
120                -0.7415311855993945,
121                -0.4058451513773972,
122                0.0,
123                0.4058451513773972,
124                0.7415311855993945,
125                0.9491079123427585,
126            ],
127            vec![
128                0.1294849661688697,
129                0.2797053914892767,
130                0.3818300505051189,
131                0.4179591836734694,
132                0.3818300505051189,
133                0.2797053914892767,
134                0.1294849661688697,
135            ],
136        ),
137        8 => (
138            vec![
139                -0.9602898564975363,
140                -0.7966664774136267,
141                -0.525_532_409_916_329,
142                -0.1834346424956498,
143                0.1834346424956498,
144                0.525_532_409_916_329,
145                0.7966664774136267,
146                0.9602898564975363,
147            ],
148            vec![
149                0.1012285362903763,
150                0.2223810344533745,
151                0.3137066458778873,
152                0.362_683_783_378_362,
153                0.362_683_783_378_362,
154                0.3137066458778873,
155                0.2223810344533745,
156                0.1012285362903763,
157            ],
158        ),
159        9 => (
160            vec![
161                -0.9681602395076261,
162                -0.8360311073266358,
163                -0.6133714327005904,
164                -0.3242534234038089,
165                0.0,
166                0.3242534234038089,
167                0.6133714327005904,
168                0.8360311073266358,
169                0.9681602395076261,
170            ],
171            vec![
172                0.0812743883615744,
173                0.1806481606948574,
174                0.2606106964029354,
175                0.3123470770400029,
176                0.3302393550012598,
177                0.3123470770400029,
178                0.2606106964029354,
179                0.1806481606948574,
180                0.0812743883615744,
181            ],
182        ),
183        _ => gauss_legendre_gw(n),
184    }
185}
186
187/// Compute Gauss-Legendre nodes/weights for n > 9 using Newton iteration
188/// on Legendre polynomials (Golub-Welsch style).
189fn gauss_legendre_gw(n: usize) -> (Vec<f64>, Vec<f64>) {
190    let mut nodes = Vec::with_capacity(n);
191    let mut weights = Vec::with_capacity(n);
192    let m = n.div_ceil(2);
193    for i in 0..m {
194        // Initial guess via asymptotic formula
195        let theta = std::f64::consts::PI * (i as f64 + 0.75) / (n as f64 + 0.5);
196        let mut x = theta.cos();
197        // Newton iteration
198        for _ in 0..100 {
199            let (p, dp) = legendre_pn_dpn(n, x);
200            let dx = -p / dp;
201            x += dx;
202            if dx.abs() < 1e-15 {
203                break;
204            }
205        }
206        let (_, dp) = legendre_pn_dpn(n, x);
207        let w = 2.0 / ((1.0 - x * x) * dp * dp);
208        nodes.push(-x);
209        weights.push(w);
210    }
211    // Mirror for symmetric pairs
212    let mut nodes_full: Vec<f64> = nodes.clone();
213    let mut weights_full: Vec<f64> = weights.clone();
214    let start = if n % 2 == 1 { m - 1 } else { m };
215    for i in (0..start).rev() {
216        nodes_full.push(-nodes[i]);
217        weights_full.push(weights[i]);
218    }
219    nodes_full.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
220    // Recompute weights in sorted order to align
221    let mut result_nodes = Vec::with_capacity(n);
222    let mut result_weights = Vec::with_capacity(n);
223    for &x in &nodes_full {
224        let (_, dp) = legendre_pn_dpn(n, x);
225        let w = 2.0 / ((1.0 - x * x) * dp * dp);
226        result_nodes.push(x);
227        result_weights.push(w);
228    }
229    (result_nodes, result_weights)
230}
231
232/// Evaluate Legendre polynomial P_n(x) and its derivative P_n'(x).
233fn legendre_pn_dpn(n: usize, x: f64) -> (f64, f64) {
234    let mut p0 = 1.0_f64;
235    let mut p1 = x;
236    for k in 1..n {
237        let kf = k as f64;
238        let p2 = ((2.0 * kf + 1.0) * x * p1 - kf * p0) / (kf + 1.0);
239        p0 = p1;
240        p1 = p2;
241    }
242    if n == 0 {
243        (1.0, 0.0)
244    } else {
245        let pn = p1;
246        let nf = n as f64;
247        let dpn = nf * (p0 - x * p1) / (1.0 - x * x);
248        (pn, dpn)
249    }
250}
251
252/// Gauss-Hermite quadrature for ∫f(x)e^{-x²}dx on (-∞, +∞).
253pub struct GaussHermiteQuad {
254    /// Number of quadrature nodes.
255    pub n: usize,
256    /// Quadrature nodes.
257    pub nodes: Vec<f64>,
258    /// Quadrature weights.
259    pub weights: Vec<f64>,
260}
261
262impl GaussHermiteQuad {
263    /// Construct Gauss-Hermite rule with `n` points (up to 20).
264    pub fn new(n: usize) -> Self {
265        let (nodes, weights) = gauss_hermite_nw(n);
266        Self { n, nodes, weights }
267    }
268
269    /// Evaluate ∫f(x)e^{-x²}dx ≈ Σ w_i f(x_i).
270    pub fn integrate<F: Fn(f64) -> f64>(&self, f: &F) -> f64 {
271        self.nodes
272            .iter()
273            .zip(self.weights.iter())
274            .map(|(&x, &w)| w * f(x))
275            .sum()
276    }
277}
278
279/// Compute Gauss-Hermite nodes and weights using hardcoded tables for small n,
280/// and Newton iteration on Hermite polynomials for larger n.
281fn gauss_hermite_nw(n: usize) -> (Vec<f64>, Vec<f64>) {
282    match n {
283        2 => (
284            vec![-FRAC_1_SQRT_2, FRAC_1_SQRT_2],
285            vec![0.886_226_925_452_758, 0.886_226_925_452_758],
286        ),
287        3 => (
288            vec![-1.224_744_871_391_589, 0.0, 1.224_744_871_391_589],
289            vec![0.2954089751509193, 1.1816359006036772, 0.2954089751509193],
290        ),
291        4 => (
292            vec![
293                -1.6506801238857845,
294                -0.5246476232752903,
295                0.5246476232752903,
296                1.6506801238857845,
297            ],
298            vec![
299                0.08131283544724518,
300                0.8049140900055128,
301                0.8049140900055128,
302                0.08131283544724518,
303            ],
304        ),
305        5 => (
306            vec![
307                -2.0201828704560856,
308                -0.9585724646138185,
309                0.0,
310                0.9585724646138185,
311                2.0201828704560856,
312            ],
313            vec![
314                0.01995324205904592,
315                0.3936193231522412,
316                0.9453087204829419,
317                0.3936193231522412,
318                0.01995324205904592,
319            ],
320        ),
321        _ => gauss_hermite_iter(n),
322    }
323}
324
325/// Compute Gauss-Hermite nodes/weights for n > 5 via Newton iteration on
326/// physicist's Hermite polynomials H_n(x).
327fn gauss_hermite_iter(n: usize) -> (Vec<f64>, Vec<f64>) {
328    let nf = n as f64;
329    let m = n.div_ceil(2);
330    let mut nodes = Vec::with_capacity(n);
331    let mut weights = Vec::with_capacity(n);
332    for i in 0..m {
333        // Initial guess
334        let mut x = (2.0 * nf + 1.0).sqrt()
335            - 1.85575
336                * (2.0 * nf + 1.0_f64).powf(-1.0 / 6.0)
337                * ((i as f64 + 1.0) * std::f64::consts::PI / (nf + 0.5)).cos();
338        for _ in 0..100 {
339            let (hn, hn1) = hermite_hn_hn1(n, x);
340            let dx = -hn / (2.0 * nf * hn1);
341            x += dx;
342            if dx.abs() < 1e-14 {
343                break;
344            }
345        }
346        let (_, hn1) = hermite_hn_hn1(n, x);
347        let w = (2.0_f64.powi(n as i32 - 1) * factorial_approx(n) * std::f64::consts::PI.sqrt())
348            / (nf * nf * hn1 * hn1);
349        nodes.push(-x);
350        weights.push(w);
351    }
352    let start = if n % 2 == 1 { m - 1 } else { m };
353    let mut full_nodes = nodes.clone();
354    let mut full_weights = weights.clone();
355    for i in (0..start).rev() {
356        full_nodes.push(-nodes[i]);
357        full_weights.push(weights[i]);
358    }
359    let mut pairs: Vec<(f64, f64)> = full_nodes.into_iter().zip(full_weights).collect();
360    pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
361    let (sorted_nodes, sorted_weights) = pairs.into_iter().unzip();
362    (sorted_nodes, sorted_weights)
363}
364
365/// Evaluate physicist's Hermite polynomial H_n(x) and H_{n-1}(x).
366fn hermite_hn_hn1(n: usize, x: f64) -> (f64, f64) {
367    let mut h0 = 1.0_f64;
368    let mut h1 = 2.0 * x;
369    if n == 0 {
370        return (h0, 0.0);
371    }
372    if n == 1 {
373        return (h1, h0);
374    }
375    for k in 1..n {
376        let kf = k as f64;
377        let h2 = 2.0 * x * h1 - 2.0 * kf * h0;
378        h0 = h1;
379        h1 = h2;
380    }
381    (h1, h0)
382}
383
384/// Approximate n! via Stirling for weight computation.
385fn factorial_approx(n: usize) -> f64 {
386    (1..=n).map(|k| k as f64).product()
387}
388
389/// Adaptive trapezoidal rule with Richardson extrapolation.
390pub struct TrapezoidalRule {
391    /// Absolute tolerance for adaptive refinement.
392    pub tol: f64,
393    /// Maximum number of refinement levels.
394    pub max_levels: usize,
395}
396
397impl TrapezoidalRule {
398    /// Create with given tolerance and max refinement levels.
399    pub fn new(tol: f64, max_levels: usize) -> Self {
400        Self { tol, max_levels }
401    }
402
403    /// Integrate `f` over \[a, b\] adaptively.
404    pub fn integrate<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F) -> f64 {
405        let mut h = b - a;
406        let mut t = 0.5 * h * (f(a) + f(b));
407        let mut prev = t;
408        for k in 1..=self.max_levels {
409            let n = 1usize << k;
410            let new_h = h / 2.0;
411            // Add midpoints
412            let sum_mid: f64 = (0..n / 2).map(|i| f(a + (2 * i + 1) as f64 * new_h)).sum();
413            t = 0.5 * t + new_h * sum_mid;
414            h = new_h;
415            // Richardson extrapolation: T_rich = (4T - T_prev) / 3
416            let t_rich = (4.0 * t - prev) / 3.0;
417            if (t_rich - prev).abs() < self.tol * (1.0 + t_rich.abs()) {
418                return t_rich;
419            }
420            prev = t;
421        }
422        t
423    }
424}
425
426/// Adaptive Simpson's rule (1/3 rule).
427pub struct SimpsonRule {
428    /// Absolute tolerance.
429    pub tol: f64,
430    /// Maximum recursion depth.
431    pub max_depth: usize,
432}
433
434impl SimpsonRule {
435    /// Create with given tolerance.
436    pub fn new(tol: f64, max_depth: usize) -> Self {
437        Self { tol, max_depth }
438    }
439
440    /// Integrate `f` over \[a, b\] using adaptive Simpson.
441    pub fn integrate<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F) -> f64 {
442        let fa = f(a);
443        let fb = f(b);
444        let fm = f(0.5 * (a + b));
445        let s = simpson13(a, b, fa, fm, fb);
446        self.recursive(a, b, fa, fm, fb, s, self.tol, self.max_depth, f)
447    }
448
449    #[allow(clippy::too_many_arguments)]
450    fn recursive<F: Fn(f64) -> f64>(
451        &self,
452        a: f64,
453        b: f64,
454        fa: f64,
455        fm: f64,
456        fb: f64,
457        s: f64,
458        tol: f64,
459        depth: usize,
460        f: &F,
461    ) -> f64 {
462        let mid = 0.5 * (a + b);
463        let fml = f(0.5 * (a + mid));
464        let fmr = f(0.5 * (mid + b));
465        let sl = simpson13(a, mid, fa, fml, fm);
466        let sr = simpson13(mid, b, fm, fmr, fb);
467        let err = ((sl + sr) - s).abs() / 15.0;
468        if depth == 0 || err < tol {
469            sl + sr + err
470        } else {
471            self.recursive(a, mid, fa, fml, fm, sl, tol / 2.0, depth - 1, f)
472                + self.recursive(mid, b, fm, fmr, fb, sr, tol / 2.0, depth - 1, f)
473        }
474    }
475
476    /// Simpson 3/8 rule on \[a, b\] with four points.
477    pub fn integrate38<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F) -> f64 {
478        let h = (b - a) / 3.0;
479        (3.0 * h / 8.0) * (f(a) + 3.0 * f(a + h) + 3.0 * f(a + 2.0 * h) + f(b))
480    }
481}
482
483#[inline]
484fn simpson13(a: f64, b: f64, fa: f64, fm: f64, fb: f64) -> f64 {
485    (b - a) / 6.0 * (fa + 4.0 * fm + fb)
486}
487
488/// Romberg integration using Richardson extrapolation table.
489pub struct RombergIntegration {
490    /// Tolerance for convergence.
491    pub tol: f64,
492    /// Maximum table size (rows).
493    pub max_rows: usize,
494}
495
496impl RombergIntegration {
497    /// Create with given tolerance and max rows.
498    pub fn new(tol: f64, max_rows: usize) -> Self {
499        Self { tol, max_rows }
500    }
501
502    /// Integrate `f` over \[a, b\] using Romberg method. Returns (result, table).
503    pub fn integrate<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F) -> (f64, Vec<Vec<f64>>) {
504        let mut table: Vec<Vec<f64>> = Vec::new();
505        let mut h = b - a;
506        // T(0,0)
507        table.push(vec![0.5 * h * (f(a) + f(b))]);
508
509        for k in 1..self.max_rows {
510            let n = 1usize << k;
511            let new_h = h / 2.0;
512            let sum: f64 = (0..n / 2).map(|i| f(a + (2 * i + 1) as f64 * new_h)).sum();
513            let t = 0.5 * table[k - 1][0] + new_h * sum;
514            h = new_h;
515
516            let mut row = vec![t];
517            for j in 1..=k {
518                let prev = row[j - 1];
519                let older = table[k - 1][j - 1];
520                let fac = (4u64.pow(j as u32)) as f64;
521                row.push((fac * prev - older) / (fac - 1.0));
522            }
523
524            let last = *row.last().expect("row is non-empty");
525            let prev_last = *table[k - 1].last().expect("table row is non-empty");
526            table.push(row);
527
528            if (last - prev_last).abs() < self.tol * (1.0 + last.abs()) {
529                return (last, table);
530            }
531        }
532        let last = *table
533            .last()
534            .expect("table is non-empty")
535            .last()
536            .expect("row is non-empty");
537        (last, table)
538    }
539}
540
541/// Gauss-Kronrod G7K15 adaptive quadrature.
542pub struct GaussKronrod {
543    /// Absolute tolerance.
544    pub tol: f64,
545    /// Maximum recursion depth.
546    pub max_depth: usize,
547}
548
549impl GaussKronrod {
550    /// G7K15 nodes on \[-1,1\] (Kronrod extension of 7-point Gauss).
551    const K15_NODES: [f64; 15] = [
552        -0.991_455_371_120_813,
553        -0.949_107_912_342_758,
554        -0.864_864_423_359_769,
555        -0.741_531_185_599_394,
556        -0.586_087_235_467_691,
557        -0.405_845_151_377_397,
558        -0.207_784_955_007_898,
559        0.0,
560        0.207_784_955_007_898,
561        0.405_845_151_377_397,
562        0.586_087_235_467_691,
563        0.741_531_185_599_394,
564        0.864_864_423_359_769,
565        0.949_107_912_342_758,
566        0.991_455_371_120_813,
567    ];
568
569    /// K15 weights.
570    const K15_WEIGHTS: [f64; 15] = [
571        0.022_935_322_010_529,
572        0.063_092_092_629_979,
573        0.104_790_010_322_250,
574        0.140_653_259_715_525,
575        0.169_004_726_639_267,
576        0.190_350_578_064_785,
577        0.204_432_940_075_298,
578        0.209_482_141_084_728,
579        0.204_432_940_075_298,
580        0.190_350_578_064_785,
581        0.169_004_726_639_267,
582        0.140_653_259_715_525,
583        0.104_790_010_322_250,
584        0.063_092_092_629_979,
585        0.022_935_322_010_529,
586    ];
587
588    /// G7 weights (embedded in K15, using even indices 1,3,5,7,9,11,13).
589    const G7_WEIGHTS: [f64; 7] = [
590        0.129_484_966_168_870,
591        0.279_705_391_489_277,
592        0.381_830_050_505_119,
593        0.417_959_183_673_469,
594        0.381_830_050_505_119,
595        0.279_705_391_489_277,
596        0.129_484_966_168_870,
597    ];
598
599    /// G7 node indices in K15.
600    const G7_IDX: [usize; 7] = [1, 3, 5, 7, 9, 11, 13];
601
602    /// Create with given tolerance.
603    pub fn new(tol: f64, max_depth: usize) -> Self {
604        Self { tol, max_depth }
605    }
606
607    /// Integrate `f` over \[a, b\] adaptively.
608    pub fn integrate<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F) -> f64 {
609        self.adaptive(a, b, f, self.max_depth)
610    }
611
612    fn adaptive<F: Fn(f64) -> f64>(&self, a: f64, b: f64, f: &F, depth: usize) -> f64 {
613        let mid = 0.5 * (a + b);
614        let half = 0.5 * (b - a);
615        let fvals: Vec<f64> = Self::K15_NODES.iter().map(|&x| f(mid + half * x)).collect();
616
617        let k15: f64 = Self::K15_WEIGHTS
618            .iter()
619            .zip(fvals.iter())
620            .map(|(&w, &fv)| w * fv)
621            .sum::<f64>()
622            * half;
623
624        let g7: f64 = Self::G7_IDX
625            .iter()
626            .zip(Self::G7_WEIGHTS.iter())
627            .map(|(&idx, &w)| w * fvals[idx])
628            .sum::<f64>()
629            * half;
630
631        let err = (k15 - g7).abs();
632        if depth == 0 || err < self.tol * (1.0 + k15.abs()) {
633            k15
634        } else {
635            self.adaptive(a, mid, f, depth - 1) + self.adaptive(mid, b, f, depth - 1)
636        }
637    }
638}
639
640/// Radial basis function types.
641#[derive(Debug, Clone, Copy)]
642pub enum RbfKind {
643    /// Multiquadric: √(r² + c²).
644    Multiquadric,
645    /// Inverse multiquadric: 1/√(r² + c²).
646    InverseMultiquadric,
647    /// Gaussian: exp(-r²/(2σ²)).
648    Gaussian,
649    /// Thin-plate spline: r²·ln(r).
650    ThinPlateSpline,
651}
652
653/// Radial basis function interpolation in arbitrary dimension.
654pub struct RadialBasisFunction {
655    /// RBF kernel type.
656    pub kind: RbfKind,
657    /// Shape parameter c or σ.
658    pub shape: f64,
659    /// Data points (each row is one point).
660    pub centers: Vec<Vec<f64>>,
661    /// Fitted coefficients.
662    pub coeffs: Vec<f64>,
663}
664
665impl RadialBasisFunction {
666    /// Create an RBF interpolant fitted to data.
667    ///
668    /// `points`: N×D matrix of data locations.
669    /// `values`: N values at those locations.
670    pub fn fit(kind: RbfKind, shape: f64, points: &[Vec<f64>], values: &[f64]) -> Self {
671        let n = points.len();
672        // Build kernel matrix K[i,j] = phi(||xi - xj||)
673        let mut k_mat = vec![0.0f64; n * n];
674        for i in 0..n {
675            for j in 0..n {
676                let r = euclidean_dist(&points[i], &points[j]);
677                k_mat[i * n + j] = rbf_kernel(kind, r, shape);
678            }
679        }
680        // Solve K * alpha = values via Gaussian elimination
681        let coeffs = solve_linear_system(&k_mat, values, n);
682        Self {
683            kind,
684            shape,
685            centers: points.to_vec(),
686            coeffs,
687        }
688    }
689
690    /// Evaluate interpolant at a new point `x`.
691    pub fn eval(&self, x: &[f64]) -> f64 {
692        self.centers
693            .iter()
694            .zip(self.coeffs.iter())
695            .map(|(c, &alpha)| {
696                let r = euclidean_dist(x, c);
697                alpha * rbf_kernel(self.kind, r, self.shape)
698            })
699            .sum()
700    }
701}
702
703fn euclidean_dist(a: &[f64], b: &[f64]) -> f64 {
704    a.iter()
705        .zip(b.iter())
706        .map(|(&ai, &bi)| (ai - bi).powi(2))
707        .sum::<f64>()
708        .sqrt()
709}
710
711fn rbf_kernel(kind: RbfKind, r: f64, c: f64) -> f64 {
712    match kind {
713        RbfKind::Multiquadric => (r * r + c * c).sqrt(),
714        RbfKind::InverseMultiquadric => 1.0 / (r * r + c * c).sqrt(),
715        RbfKind::Gaussian => (-(r * r) / (2.0 * c * c)).exp(),
716        RbfKind::ThinPlateSpline => {
717            if r < 1e-14 {
718                0.0
719            } else {
720                r * r * r.ln()
721            }
722        }
723    }
724}
725
726/// Solve A*x = b via Gaussian elimination with partial pivoting.
727pub fn solve_linear_system(a_flat: &[f64], b: &[f64], n: usize) -> Vec<f64> {
728    let mut mat = a_flat.to_vec();
729    let mut rhs = b.to_vec();
730
731    for col in 0..n {
732        // Pivot
733        let mut max_row = col;
734        let mut max_val = mat[col * n + col].abs();
735        for row in (col + 1)..n {
736            let v = mat[row * n + col].abs();
737            if v > max_val {
738                max_val = v;
739                max_row = row;
740            }
741        }
742        if max_row != col {
743            for k in 0..n {
744                mat.swap(col * n + k, max_row * n + k);
745            }
746            rhs.swap(col, max_row);
747        }
748        let pivot = mat[col * n + col];
749        if pivot.abs() < 1e-14 {
750            continue;
751        }
752        for row in (col + 1)..n {
753            let factor = mat[row * n + col] / pivot;
754            for k in col..n {
755                let v = mat[col * n + k];
756                mat[row * n + k] -= factor * v;
757            }
758            rhs[row] -= factor * rhs[col];
759        }
760    }
761    // Back substitution
762    let mut x = vec![0.0f64; n];
763    for i in (0..n).rev() {
764        let mut s = rhs[i];
765        for j in (i + 1)..n {
766            s -= mat[i * n + j] * x[j];
767        }
768        x[i] = s / mat[i * n + i];
769    }
770    x
771}
772
773/// Natural cubic spline interpolation.
774pub struct SplineInterpolation {
775    /// Breakpoints (sorted ascending).
776    pub xs: Vec<f64>,
777    /// Values at breakpoints.
778    pub ys: Vec<f64>,
779    /// Second derivatives at breakpoints (from tridiagonal solve).
780    pub m: Vec<f64>,
781    /// Spline boundary condition type.
782    pub kind: SplineKind,
783}
784
785/// Spline boundary conditions.
786#[derive(Debug, Clone, Copy)]
787pub enum SplineKind {
788    /// Natural: second derivative = 0 at endpoints.
789    Natural,
790    /// Clamped: prescribed first derivative at endpoints.
791    Clamped(f64, f64),
792    /// Not-a-knot: third derivative continuous at second and second-to-last knots.
793    NotAKnot,
794}
795
796impl SplineInterpolation {
797    /// Fit cubic spline to data `(xs, ys)` with given boundary condition.
798    pub fn fit(xs: &[f64], ys: &[f64], kind: SplineKind) -> Self {
799        let n = xs.len();
800        assert!(n >= 3, "Need at least 3 points for cubic spline");
801        let m = compute_spline_second_derivs(xs, ys, kind);
802        Self {
803            xs: xs.to_vec(),
804            ys: ys.to_vec(),
805            m,
806            kind,
807        }
808    }
809
810    /// Evaluate spline at x.
811    pub fn eval(&self, x: f64) -> f64 {
812        let n = self.xs.len();
813        // Find interval
814        let mut idx = 0;
815        for i in 1..n - 1 {
816            if x >= self.xs[i] {
817                idx = i;
818            }
819        }
820        let h = self.xs[idx + 1] - self.xs[idx];
821        let t = (x - self.xs[idx]) / h;
822        let a = self.ys[idx];
823        let b_val = self.ys[idx + 1];
824        let ma = self.m[idx];
825        let mb = self.m[idx + 1];
826        // Cubic Hermite form using second derivatives
827        (1.0 - t) * a
828            + t * b_val
829            + t * (1.0 - t)
830                * ((1.0 - t) * h * h * ma / 6.0 - t * h * h * mb / 6.0 - h * h * (ma - mb) / 6.0
831                    + h * h * ma / 6.0 * (2.0 * t - 1.0))
832    }
833
834    /// Evaluate spline using proper cubic formula.
835    pub fn eval_cubic(&self, x: f64) -> f64 {
836        let n = self.xs.len();
837        let mut idx = 0;
838        for i in 1..n - 1 {
839            if x >= self.xs[i] {
840                idx = i;
841            }
842        }
843        let h = self.xs[idx + 1] - self.xs[idx];
844        let dx = x - self.xs[idx];
845        let dx2 = self.xs[idx + 1] - x;
846        let a = self.ys[idx];
847        let b_val = self.ys[idx + 1];
848        let ma = self.m[idx];
849        let mb = self.m[idx + 1];
850        (dx2 / h) * a
851            + (dx / h) * b_val
852            + (dx2 / h) * ((dx2 * dx2 / (h * h) - 1.0) * h * h * ma / 6.0)
853            + (dx / h) * ((dx * dx / (h * h) - 1.0) * h * h * mb / 6.0)
854    }
855}
856
857/// Solve the tridiagonal system for cubic spline second derivatives.
858fn compute_spline_second_derivs(xs: &[f64], ys: &[f64], kind: SplineKind) -> Vec<f64> {
859    let n = xs.len();
860    let mut h = vec![0.0f64; n - 1];
861    for i in 0..n - 1 {
862        h[i] = xs[i + 1] - xs[i];
863    }
864
865    // Build tridiagonal system
866    let mut diag = vec![0.0f64; n];
867    let mut upper = vec![0.0f64; n];
868    let mut lower = vec![0.0f64; n];
869    let mut rhs = vec![0.0f64; n];
870
871    for i in 1..n - 1 {
872        lower[i] = h[i - 1];
873        diag[i] = 2.0 * (h[i - 1] + h[i]);
874        upper[i] = h[i];
875        rhs[i] = 6.0 * ((ys[i + 1] - ys[i]) / h[i] - (ys[i] - ys[i - 1]) / h[i - 1]);
876    }
877
878    match kind {
879        SplineKind::Natural => {
880            diag[0] = 1.0;
881            upper[0] = 0.0;
882            rhs[0] = 0.0;
883            diag[n - 1] = 1.0;
884            lower[n - 1] = 0.0;
885            rhs[n - 1] = 0.0;
886        }
887        SplineKind::Clamped(d0, dn) => {
888            diag[0] = 2.0 * h[0];
889            upper[0] = h[0];
890            rhs[0] = 6.0 * ((ys[1] - ys[0]) / h[0] - d0);
891            diag[n - 1] = 2.0 * h[n - 2];
892            lower[n - 1] = h[n - 2];
893            rhs[n - 1] = 6.0 * (dn - (ys[n - 1] - ys[n - 2]) / h[n - 2]);
894        }
895        SplineKind::NotAKnot => {
896            // Third derivative continuous at xs[1]: h[1]*m[0] = h[0]*m[2] - (h[0]+h[1])*m[1]
897            diag[0] = h[1];
898            upper[0] = -(h[0] + h[1]);
899            rhs[0] = 0.0;
900            // ... also at xs[n-2]
901            diag[n - 1] = h[n - 3];
902            lower[n - 1] = -(h[n - 3] + h[n - 2]);
903            rhs[n - 1] = 0.0;
904        }
905    }
906
907    // Solve tridiagonal via Thomas algorithm
908    thomas_algorithm(&diag, &lower, &upper, &rhs, n)
909}
910
911/// Thomas algorithm (tridiagonal matrix algorithm).
912fn thomas_algorithm(diag: &[f64], lower: &[f64], upper: &[f64], rhs: &[f64], n: usize) -> Vec<f64> {
913    let mut c = vec![0.0f64; n];
914    let mut d = rhs.to_vec();
915    let mut w = vec![0.0f64; n];
916
917    w[0] = upper[0] / diag[0];
918    d[0] /= diag[0];
919
920    for i in 1..n {
921        let denom = diag[i] - lower[i] * w[i - 1];
922        if denom.abs() < 1e-14 {
923            w[i] = 0.0;
924            d[i] = 0.0;
925            continue;
926        }
927        w[i] = upper[i] / denom;
928        d[i] = (d[i] - lower[i] * d[i - 1]) / denom;
929    }
930
931    c[n - 1] = d[n - 1];
932    for i in (0..n - 1).rev() {
933        c[i] = d[i] - w[i] * c[i + 1];
934    }
935    c
936}
937
938/// Barycentric interpolation (Berrut second form).
939pub struct BarycentricInterpolation {
940    /// Interpolation nodes.
941    pub nodes: Vec<f64>,
942    /// Function values at nodes.
943    pub values: Vec<f64>,
944    /// Barycentric weights.
945    pub weights: Vec<f64>,
946}
947
948impl BarycentricInterpolation {
949    /// Fit barycentric interpolant to `(nodes, values)`.
950    /// Uses Chebyshev-based weights for numerical stability.
951    pub fn fit(nodes: &[f64], values: &[f64]) -> Self {
952        let n = nodes.len();
953        let weights = compute_barycentric_weights(nodes, n);
954        Self {
955            nodes: nodes.to_vec(),
956            values: values.to_vec(),
957            weights,
958        }
959    }
960
961    /// Evaluate interpolant at x using second barycentric formula.
962    pub fn eval(&self, x: f64) -> f64 {
963        let n = self.nodes.len();
964        // Check if x is a node
965        for i in 0..n {
966            if (x - self.nodes[i]).abs() < 1e-14 {
967                return self.values[i];
968            }
969        }
970        let numer: f64 = (0..n)
971            .map(|i| self.weights[i] / (x - self.nodes[i]) * self.values[i])
972            .sum();
973        let denom: f64 = (0..n).map(|i| self.weights[i] / (x - self.nodes[i])).sum();
974        numer / denom
975    }
976}
977
978fn compute_barycentric_weights(nodes: &[f64], n: usize) -> Vec<f64> {
979    let mut w = vec![1.0f64; n];
980    for i in 0..n {
981        for j in 0..n {
982            if i != j {
983                w[i] /= nodes[i] - nodes[j];
984            }
985        }
986    }
987    w
988}
989
990/// Richardson-extrapolated numerical differentiation.
991pub struct NumericalDifferentiation {
992    /// Base step size.
993    pub h: f64,
994    /// Number of Richardson extrapolation levels.
995    pub levels: usize,
996}
997
998impl NumericalDifferentiation {
999    /// Create with base step and extrapolation levels.
1000    pub fn new(h: f64, levels: usize) -> Self {
1001        Self { h, levels }
1002    }
1003
1004    /// First derivative via central differences + Richardson extrapolation.
1005    pub fn first_deriv<F: Fn(f64) -> f64>(&self, x: f64, f: &F) -> f64 {
1006        richardson_deriv(x, self.h, self.levels, f, 1)
1007    }
1008
1009    /// Second derivative via central differences + Richardson extrapolation.
1010    pub fn second_deriv<F: Fn(f64) -> f64>(&self, x: f64, f: &F) -> f64 {
1011        richardson_deriv2(x, self.h, self.levels, f)
1012    }
1013
1014    /// Mixed partial ∂²f/∂x∂y at (x, y).
1015    pub fn mixed_partial<F: Fn(f64, f64) -> f64>(&self, x: f64, y: f64, f: &F) -> f64 {
1016        let h = self.h;
1017        let fpp = f(x + h, y + h);
1018        let fpm = f(x + h, y - h);
1019        let fmp = f(x - h, y + h);
1020        let fmm = f(x - h, y - h);
1021        (fpp - fpm - fmp + fmm) / (4.0 * h * h)
1022    }
1023
1024    /// Arbitrary-order derivative (order 1..4) via finite differences.
1025    #[allow(clippy::too_many_arguments)]
1026    pub fn nth_deriv<F: Fn(f64) -> f64>(&self, x: f64, order: usize, f: &F) -> f64 {
1027        match order {
1028            1 => self.first_deriv(x, f),
1029            2 => self.second_deriv(x, f),
1030            3 => {
1031                let h = self.h;
1032                (f(x + 2.0 * h) - 2.0 * f(x + h) + 2.0 * f(x - h) - f(x - 2.0 * h))
1033                    / (2.0 * h * h * h)
1034            }
1035            4 => {
1036                let h = self.h;
1037                (f(x + 2.0 * h) - 4.0 * f(x + h) + 6.0 * f(x) - 4.0 * f(x - h) + f(x - 2.0 * h))
1038                    / (h * h * h * h)
1039            }
1040            _ => panic!("Only orders 1-4 supported"),
1041        }
1042    }
1043}
1044
1045fn richardson_deriv<F: Fn(f64) -> f64>(
1046    x: f64,
1047    h0: f64,
1048    levels: usize,
1049    f: &F,
1050    _order: usize,
1051) -> f64 {
1052    let mut table: Vec<Vec<f64>> = Vec::new();
1053    let mut h = h0;
1054    for k in 0..levels {
1055        let d = (f(x + h) - f(x - h)) / (2.0 * h);
1056        if k == 0 {
1057            table.push(vec![d]);
1058        } else {
1059            let mut row = vec![d];
1060            for j in 1..=k {
1061                let fac = (4u64.pow(j as u32)) as f64;
1062                let val = (fac * row[j - 1] - table[k - 1][j - 1]) / (fac - 1.0);
1063                row.push(val);
1064            }
1065            table.push(row);
1066        }
1067        h /= 2.0;
1068    }
1069    *table
1070        .last()
1071        .expect("table is non-empty")
1072        .last()
1073        .expect("row is non-empty")
1074}
1075
1076fn richardson_deriv2<F: Fn(f64) -> f64>(x: f64, h0: f64, levels: usize, f: &F) -> f64 {
1077    let mut table: Vec<Vec<f64>> = Vec::new();
1078    let mut h = h0;
1079    for k in 0..levels {
1080        let d = (f(x + h) - 2.0 * f(x) + f(x - h)) / (h * h);
1081        if k == 0 {
1082            table.push(vec![d]);
1083        } else {
1084            let mut row = vec![d];
1085            for j in 1..=k {
1086                let fac = (4u64.pow(j as u32)) as f64;
1087                let val = (fac * row[j - 1] - table[k - 1][j - 1]) / (fac - 1.0);
1088                row.push(val);
1089            }
1090            table.push(row);
1091        }
1092        h /= 2.0;
1093    }
1094    *table
1095        .last()
1096        .expect("table is non-empty")
1097        .last()
1098        .expect("row is non-empty")
1099}
1100
1101// Helper trait for tests
1102trait DiffExact {
1103    fn diff_exact(self) -> f64;
1104}
1105
1106impl DiffExact for f64 {
1107    fn diff_exact(self) -> f64 {
1108        3.0 * self * self
1109    }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115
1116    // ── Gauss-Legendre tests ──────────────────────────────────────────────
1117
1118    #[test]
1119    fn test_gl_exact_degree3() {
1120        // 2-point GL is exact for degree 2*2-1=3 polynomials
1121        let gl = GaussLegendreQuad::new(2);
1122        let result = gl.integrate(0.0, 1.0, &|x| x * x * x);
1123        assert!((result - 0.25).abs() < 1e-12, "GL2 x^3: {result:.6}");
1124    }
1125
1126    #[test]
1127    fn test_gl_exact_degree5() {
1128        // 3-point GL exact for degree 5
1129        let gl = GaussLegendreQuad::new(3);
1130        let result = gl.integrate(-1.0, 1.0, &|x| x.powi(5));
1131        assert!(result.abs() < 1e-12, "GL3 x^5 on [-1,1]: {result:.6}");
1132    }
1133
1134    #[test]
1135    fn test_gl_sine() {
1136        let gl = GaussLegendreQuad::new(5);
1137        let result = gl.integrate(0.0, std::f64::consts::PI, &|x| x.sin());
1138        assert!((result - 2.0).abs() < 1e-4, "GL5 sin: {result:.12}");
1139    }
1140
1141    #[test]
1142    fn test_gl_composite_sine() {
1143        let gl = GaussLegendreQuad::new(3);
1144        let result = gl.integrate_composite(0.0, std::f64::consts::PI, 10, &|x| x.sin());
1145        assert!((result - 2.0).abs() < 1e-8, "GL composite sin: {result:.6}");
1146    }
1147
1148    #[test]
1149    fn test_gl_nodes_count() {
1150        for n in [2, 3, 5, 10] {
1151            let gl = GaussLegendreQuad::new(n);
1152            assert_eq!(gl.nodes.len(), n);
1153            assert_eq!(gl.weights.len(), n);
1154        }
1155    }
1156
1157    #[test]
1158    fn test_gl_weights_sum_to_2() {
1159        for n in [2, 3, 4, 5] {
1160            let gl = GaussLegendreQuad::new(n);
1161            let s: f64 = gl.weights.iter().sum();
1162            assert!((s - 2.0).abs() < 1e-10, "GL{n} weights sum {s:.6}");
1163        }
1164    }
1165
1166    // ── Gauss-Hermite tests ───────────────────────────────────────────────
1167
1168    #[test]
1169    fn test_hermite_constant() {
1170        // ∫ e^{-x²} dx = sqrt(pi)
1171        let gh = GaussHermiteQuad::new(5);
1172        let result = gh.integrate(&|_x| 1.0);
1173        assert!(
1174            (result - std::f64::consts::PI.sqrt()).abs() < 1e-8,
1175            "GH constant: {result:.6}"
1176        );
1177    }
1178
1179    #[test]
1180    fn test_hermite_x_squared() {
1181        // ∫ x² e^{-x²} dx = sqrt(pi)/2
1182        let gh = GaussHermiteQuad::new(5);
1183        let result = gh.integrate(&|x| x * x);
1184        let exact = std::f64::consts::PI.sqrt() / 2.0;
1185        assert!(
1186            (result - exact).abs() < 1e-8,
1187            "GH x^2: {result:.6} vs {exact:.6}"
1188        );
1189    }
1190
1191    // ── Trapezoidal tests ─────────────────────────────────────────────────
1192
1193    #[test]
1194    fn test_trapezoidal_exp() {
1195        let trap = TrapezoidalRule::new(1e-10, 20);
1196        let result = trap.integrate(0.0, 1.0, &|x| x.exp());
1197        let exact = std::f64::consts::E - 1.0;
1198        assert!((result - exact).abs() < 1e-9, "Trap exp: {result:.6}");
1199    }
1200
1201    #[test]
1202    fn test_trapezoidal_polynomial() {
1203        let trap = TrapezoidalRule::new(1e-12, 20);
1204        let result = trap.integrate(0.0, 1.0, &|x| x * x);
1205        assert!((result - 1.0 / 3.0).abs() < 1e-10, "Trap x^2: {result:.6}");
1206    }
1207
1208    // ── Simpson tests ─────────────────────────────────────────────────────
1209
1210    #[test]
1211    fn test_simpson_exp() {
1212        let simp = SimpsonRule::new(1e-10, 20);
1213        let result = simp.integrate(0.0, 1.0, &|x| x.exp());
1214        let exact = std::f64::consts::E - 1.0;
1215        assert!((result - exact).abs() < 1e-8, "Simpson exp: {result:.6}");
1216    }
1217
1218    #[test]
1219    fn test_simpson38() {
1220        let simp = SimpsonRule::new(1e-10, 20);
1221        let result = simp.integrate38(0.0, 1.0, &|x| x * x * x);
1222        assert!((result - 0.25).abs() < 1e-10, "Simpson38 x^3: {result:.6}");
1223    }
1224
1225    // ── Romberg tests ─────────────────────────────────────────────────────
1226
1227    #[test]
1228    fn test_romberg_converges_faster() {
1229        let rom = RombergIntegration::new(1e-12, 15);
1230        let (result, table) = rom.integrate(0.0, 1.0, &|x| x.exp());
1231        let exact = std::f64::consts::E - 1.0;
1232        assert!((result - exact).abs() < 1e-11, "Romberg exp: {result:.12}");
1233        // Romberg table should have multiple rows
1234        assert!(table.len() >= 2);
1235    }
1236
1237    #[test]
1238    fn test_romberg_sine() {
1239        let rom = RombergIntegration::new(1e-12, 15);
1240        let (result, _) = rom.integrate(0.0, std::f64::consts::PI, &|x| x.sin());
1241        assert!((result - 2.0).abs() < 1e-11, "Romberg sin: {result:.12}");
1242    }
1243
1244    #[test]
1245    fn test_romberg_table_diagonal_improves() {
1246        let rom = RombergIntegration::new(1e-14, 8);
1247        let (_, table) = rom.integrate(0.0, 1.0, &|x| x * x);
1248        // Last diagonal entry should be very close to 1/3
1249        let last = *table.last().unwrap().last().unwrap();
1250        assert!(
1251            (last - 1.0 / 3.0).abs() < 1e-12,
1252            "Romberg table: {last:.14}"
1253        );
1254    }
1255
1256    // ── Gauss-Kronrod tests ───────────────────────────────────────────────
1257
1258    #[test]
1259    fn test_gauss_kronrod_exp() {
1260        let gk = GaussKronrod::new(1e-10, 10);
1261        let result = gk.integrate(0.0, 1.0, &|x| x.exp());
1262        let exact = std::f64::consts::E - 1.0;
1263        assert!((result - exact).abs() < 1e-9, "GK exp: {result:.6}");
1264    }
1265
1266    #[test]
1267    fn test_gauss_kronrod_oscillatory() {
1268        let gk = GaussKronrod::new(1e-8, 15);
1269        let result = gk.integrate(0.0, 10.0 * std::f64::consts::PI, &|x| x.sin());
1270        // Integral over full periods = 0
1271        assert!(result.abs() < 1e-6, "GK oscillatory: {result:.6}");
1272    }
1273
1274    // ── RBF tests ─────────────────────────────────────────────────────────
1275
1276    #[test]
1277    fn test_rbf_exact_at_data_points_gaussian() {
1278        let pts: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64]).collect();
1279        let vals: Vec<f64> = pts.iter().map(|p| p[0] * p[0]).collect();
1280        let rbf = RadialBasisFunction::fit(RbfKind::Gaussian, 1.0, &pts, &vals);
1281        for (i, pt) in pts.iter().enumerate() {
1282            let v = rbf.eval(pt);
1283            assert!(
1284                (v - vals[i]).abs() < 1e-8,
1285                "RBF Gaussian at data[{i}]: {v:.6} vs {:.6}",
1286                vals[i]
1287            );
1288        }
1289    }
1290
1291    #[test]
1292    fn test_rbf_exact_at_data_points_mq() {
1293        let pts: Vec<Vec<f64>> = vec![vec![0.0], vec![0.5], vec![1.0]];
1294        let vals = vec![0.0, 0.25, 1.0];
1295        let rbf = RadialBasisFunction::fit(RbfKind::Multiquadric, 0.5, &pts, &vals);
1296        for (i, pt) in pts.iter().enumerate() {
1297            let v = rbf.eval(pt);
1298            assert!((v - vals[i]).abs() < 1e-8, "RBF MQ at data[{i}]: {v:.6}");
1299        }
1300    }
1301
1302    #[test]
1303    fn test_rbf_inv_mq() {
1304        let pts: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0], vec![2.0]];
1305        let vals = vec![1.0, 2.0, 4.0];
1306        let rbf = RadialBasisFunction::fit(RbfKind::InverseMultiquadric, 0.5, &pts, &vals);
1307        for (i, pt) in pts.iter().enumerate() {
1308            let v = rbf.eval(pt);
1309            assert!((v - vals[i]).abs() < 1e-8, "RBF InvMQ at data[{i}]: {v:.6}");
1310        }
1311    }
1312
1313    // ── Spline tests ──────────────────────────────────────────────────────
1314
1315    #[test]
1316    fn test_spline_natural_at_nodes() {
1317        let xs: Vec<f64> = (0..6).map(|i| i as f64).collect();
1318        let ys: Vec<f64> = xs.iter().map(|&x| x.sin()).collect();
1319        let sp = SplineInterpolation::fit(&xs, &ys, SplineKind::Natural);
1320        for i in 0..xs.len() {
1321            let v = sp.eval_cubic(xs[i]);
1322            assert!(
1323                (v - ys[i]).abs() < 1e-10,
1324                "Natural spline at node {i}: {v:.6} vs {:.6}",
1325                ys[i]
1326            );
1327        }
1328    }
1329
1330    #[test]
1331    fn test_spline_natural_zero_second_deriv_endpoints() {
1332        let xs: Vec<f64> = (0..5).map(|i| i as f64).collect();
1333        let ys: Vec<f64> = xs.iter().map(|&x| x * x).collect();
1334        let sp = SplineInterpolation::fit(&xs, &ys, SplineKind::Natural);
1335        assert!(sp.m[0].abs() < 1e-10, "Natural spline M[0]={:.6}", sp.m[0]);
1336        let n = sp.m.len();
1337        assert!(
1338            sp.m[n - 1].abs() < 1e-10,
1339            "Natural spline M[n-1]={:.6}",
1340            sp.m[n - 1]
1341        );
1342    }
1343
1344    #[test]
1345    fn test_spline_clamped_at_nodes() {
1346        let xs: Vec<f64> = (0..5).map(|i| i as f64 * 0.5).collect();
1347        let ys: Vec<f64> = xs.iter().map(|&x| x * x * x).collect();
1348        let sp = SplineInterpolation::fit(&xs, &ys, SplineKind::Clamped(0.0, 3.0 * 2.0 * 2.0));
1349        for i in 0..xs.len() {
1350            let v = sp.eval_cubic(xs[i]);
1351            assert!(
1352                (v - ys[i]).abs() < 1e-8,
1353                "Clamped spline at node {i}: {v:.6}"
1354            );
1355        }
1356    }
1357
1358    #[test]
1359    fn test_spline_not_a_knot_at_nodes() {
1360        let xs: Vec<f64> = (0..5).map(|i| i as f64).collect();
1361        let ys: Vec<f64> = xs.iter().map(|&x| x.exp()).collect();
1362        let sp = SplineInterpolation::fit(&xs, &ys, SplineKind::NotAKnot);
1363        for i in 0..xs.len() {
1364            let v = sp.eval_cubic(xs[i]);
1365            assert!(
1366                (v - ys[i]).abs() < 1e-8,
1367                "NotAKnot spline at node {i}: {v:.6}"
1368            );
1369        }
1370    }
1371
1372    // ── Barycentric tests ─────────────────────────────────────────────────
1373
1374    #[test]
1375    fn test_barycentric_exact_at_nodes() {
1376        let nodes: Vec<f64> = (0..5).map(|i| i as f64).collect();
1377        let values: Vec<f64> = nodes.iter().map(|&x| x * x).collect();
1378        let bary = BarycentricInterpolation::fit(&nodes, &values);
1379        for i in 0..nodes.len() {
1380            let v = bary.eval(nodes[i]);
1381            assert!(
1382                (v - values[i]).abs() < 1e-10,
1383                "Barycentric at node {i}: {v:.6}"
1384            );
1385        }
1386    }
1387
1388    #[test]
1389    fn test_barycentric_polynomial_reconstruction() {
1390        // Polynomial p(x) = x^3 - 2x + 1, interpolated at 4 nodes
1391        let nodes = vec![0.0, 1.0, 2.0, 3.0];
1392        let values: Vec<f64> = nodes.iter().map(|&x| x * x * x - 2.0 * x + 1.0).collect();
1393        let bary = BarycentricInterpolation::fit(&nodes, &values);
1394        let test_pts = [0.5, 1.5, 2.5];
1395        for &x in &test_pts {
1396            let v = bary.eval(x);
1397            let exact = x * x * x - 2.0 * x + 1.0;
1398            assert!(
1399                (v - exact).abs() < 1e-10,
1400                "Barycentric poly at {x}: {v:.6} vs {exact:.6}"
1401            );
1402        }
1403    }
1404
1405    // ── Richardson differentiation tests ─────────────────────────────────
1406
1407    #[test]
1408    fn test_first_deriv_sine() {
1409        let nd = NumericalDifferentiation::new(1e-4, 4);
1410        let x = 1.0;
1411        let d = nd.first_deriv(x, &|t| t.sin());
1412        let exact = x.cos();
1413        assert!(
1414            (d - exact).abs() < 1e-9,
1415            "d/dx sin(1): {d:.9} vs {exact:.9}"
1416        );
1417    }
1418
1419    #[test]
1420    fn test_second_deriv_exp() {
1421        let nd = NumericalDifferentiation::new(1e-4, 4);
1422        let x = 0.5;
1423        let d2 = nd.second_deriv(x, &|t| t.exp());
1424        assert!((d2 - x.exp()).abs() < 1e-5, "d^2/dx^2 exp(0.5): {d2:.6}");
1425    }
1426
1427    #[test]
1428    fn test_mixed_partial_xy() {
1429        let nd = NumericalDifferentiation::new(1e-4, 4);
1430        // f(x,y) = x*y^2, df/dxdy = 2y
1431        let y = 2.0;
1432        let d = nd.mixed_partial(1.0, y, &|x, yy| x * yy * yy);
1433        let exact = 2.0 * y;
1434        assert!(
1435            (d - exact).abs() < 1e-7,
1436            "mixed partial: {d:.6} vs {exact:.6}"
1437        );
1438    }
1439
1440    #[test]
1441    fn test_first_deriv_accuracy_order4() {
1442        // Richardson should give O(h^4) accuracy
1443        let nd_coarse = NumericalDifferentiation::new(1e-2, 1);
1444        let nd_fine = NumericalDifferentiation::new(1e-2, 4);
1445        let x = 1.2;
1446        let exact = x.diff_exact(); // 3x^2
1447        let d_coarse = nd_coarse.first_deriv(x, &|t| t * t * t);
1448        let d_fine = nd_fine.first_deriv(x, &|t| t * t * t);
1449        // Fine should be much more accurate
1450        assert!(
1451            (d_fine - exact).abs() < (d_coarse - exact).abs(),
1452            "Richardson should improve: coarse err={:.2e} fine err={:.2e}",
1453            (d_coarse - exact).abs(),
1454            (d_fine - exact).abs()
1455        );
1456    }
1457
1458    #[test]
1459    fn test_nth_deriv_third_order() {
1460        let nd = NumericalDifferentiation::new(1e-3, 4);
1461        // f(x) = x^4, f'''(x) = 24x
1462        let x = 1.0;
1463        let d3 = nd.nth_deriv(x, 3, &|t| t.powi(4));
1464        let exact = 24.0 * x;
1465        assert!(
1466            (d3 - exact).abs() < 1e-5,
1467            "d^3/dx^3 x^4 at 1: {d3:.6} vs {exact:.6}"
1468        );
1469    }
1470
1471    #[test]
1472    fn test_nth_deriv_fourth_order() {
1473        let nd = NumericalDifferentiation::new(1e-3, 4);
1474        // f(x) = x^4, f''''(x) = 24
1475        let x = 1.0;
1476        let d4 = nd.nth_deriv(x, 4, &|t| t.powi(4));
1477        assert!((d4 - 24.0).abs() < 1e-2, "d^4/dx^4 x^4 at 1: {d4:.6}");
1478    }
1479}