Skip to main content

oxilean_std/spectral_methods/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use std::f64::consts::PI;
7
8/// hp-adaptive spectral element method data.
9#[allow(dead_code)]
10pub struct HpAdaptiveSpectral {
11    /// Number of elements.
12    pub num_elements: usize,
13    /// Polynomial degree for each element.
14    pub degrees: Vec<usize>,
15    /// Element endpoints.
16    pub breakpoints: Vec<f64>,
17    /// Local solutions: one coefficient vector per element.
18    pub local_solutions: Vec<Vec<f64>>,
19}
20#[allow(dead_code)]
21impl HpAdaptiveSpectral {
22    /// Create a uniform hp mesh on [a, b].
23    pub fn uniform(a: f64, b: f64, num_elements: usize, p: usize) -> Self {
24        let h = (b - a) / num_elements as f64;
25        let breakpoints: Vec<f64> = (0..=num_elements).map(|i| a + i as f64 * h).collect();
26        let degrees = vec![p; num_elements];
27        let local_solutions = vec![vec![0.0f64; p + 1]; num_elements];
28        Self {
29            num_elements,
30            degrees,
31            breakpoints,
32            local_solutions,
33        }
34    }
35    /// Total degrees of freedom.
36    pub fn total_dof(&self) -> usize {
37        self.degrees.iter().map(|&p| p + 1).sum()
38    }
39    /// h-refine element k (split in half).
40    pub fn h_refine(&mut self, k: usize) {
41        if k >= self.num_elements {
42            return;
43        }
44        let mid = 0.5 * (self.breakpoints[k] + self.breakpoints[k + 1]);
45        let deg = self.degrees[k];
46        let sol = self.local_solutions[k].clone();
47        self.breakpoints.insert(k + 1, mid);
48        self.degrees.remove(k);
49        self.degrees.insert(k, deg);
50        self.degrees.insert(k + 1, deg);
51        self.local_solutions.remove(k);
52        self.local_solutions.insert(k, sol.clone());
53        self.local_solutions.insert(k + 1, sol);
54        self.num_elements += 1;
55    }
56    /// p-refine element k (increase degree by 1).
57    pub fn p_refine(&mut self, k: usize) {
58        if k >= self.num_elements {
59            return;
60        }
61        self.degrees[k] += 1;
62        self.local_solutions[k].push(0.0);
63    }
64    /// Size of element k.
65    pub fn element_size(&self, k: usize) -> f64 {
66        if k >= self.num_elements {
67            return 0.0;
68        }
69        self.breakpoints[k + 1] - self.breakpoints[k]
70    }
71}
72/// Radial basis function interpolation / finite-difference stencil.
73pub struct RadialBasisFunction {
74    /// RBF type: "gaussian", "multiquadric", "inverse_multiquadric", "thin_plate", …
75    pub function_type: String,
76    /// Shape parameter ε (controls spread / sharpness).
77    pub shape_param: f64,
78}
79impl RadialBasisFunction {
80    /// Create a new RadialBasisFunction.
81    pub fn new(function_type: impl Into<String>, shape_param: f64) -> Self {
82        Self {
83            function_type: function_type.into(),
84            shape_param,
85        }
86    }
87    /// Evaluate the RBF φ(r) at radius r.
88    pub fn phi(&self, r: f64) -> f64 {
89        let eps = self.shape_param;
90        match self.function_type.to_lowercase().as_str() {
91            "gaussian" => (-(eps * r).powi(2)).exp(),
92            "multiquadric" => (1.0 + (eps * r).powi(2)).sqrt(),
93            "inverse_multiquadric" => 1.0 / (1.0 + (eps * r).powi(2)).sqrt(),
94            "thin_plate" => {
95                if r < 1e-14 {
96                    0.0
97                } else {
98                    r * r * r.ln()
99                }
100            }
101            _ => (-(eps * r).powi(2)).exp(),
102        }
103    }
104    /// Interpolate at query points using RBF interpolation with given centers and values.
105    ///
106    /// Solves the system Φ c = f where Φ_{ij} = φ(‖xᵢ − xⱼ‖), then evaluates at query.
107    pub fn interpolate(&self, centers: &[f64], values: &[f64], query: &[f64]) -> Vec<f64> {
108        let n = centers.len();
109        assert_eq!(values.len(), n);
110        let mut phi_mat = vec![vec![0.0_f64; n]; n];
111        for i in 0..n {
112            for j in 0..n {
113                phi_mat[i][j] = self.phi((centers[i] - centers[j]).abs());
114            }
115        }
116        let coeffs = solve_linear(&phi_mat, values);
117        query
118            .iter()
119            .map(|&x| {
120                coeffs
121                    .iter()
122                    .zip(centers.iter())
123                    .map(|(&c, &xi)| c * self.phi((x - xi).abs()))
124                    .sum()
125            })
126            .collect()
127    }
128    /// Compute a 1-D RBF-FD differentiation stencil for the point x using its neighbors.
129    pub fn rbf_fd_stencil(&self, center: f64, neighbors: &[f64]) -> Vec<f64> {
130        let n = neighbors.len();
131        let mut phi_mat = vec![vec![0.0_f64; n]; n];
132        for i in 0..n {
133            for j in 0..n {
134                phi_mat[i][j] = self.phi((neighbors[i] - neighbors[j]).abs());
135            }
136        }
137        let rhs: Vec<f64> = neighbors
138            .iter()
139            .map(|&xj| {
140                let r = (center - xj).abs();
141                let sign = if center >= xj { 1.0 } else { -1.0 };
142                let dphi = self.dphi_dr(r);
143                dphi * sign
144            })
145            .collect();
146        solve_linear(&phi_mat, &rhs)
147    }
148    /// Derivative of φ w.r.t. r.
149    fn dphi_dr(&self, r: f64) -> f64 {
150        let eps = self.shape_param;
151        match self.function_type.to_lowercase().as_str() {
152            "gaussian" => -2.0 * eps * eps * r * (-(eps * r).powi(2)).exp(),
153            "multiquadric" => eps * eps * r / (1.0 + (eps * r).powi(2)).sqrt(),
154            "inverse_multiquadric" => -eps * eps * r / (1.0 + (eps * r).powi(2)).powf(1.5),
155            "thin_plate" => {
156                if r < 1e-14 {
157                    0.0
158                } else {
159                    r * (1.0 + 2.0 * r.ln())
160                }
161            }
162            _ => -2.0 * eps * eps * r * (-(eps * r).powi(2)).exp(),
163        }
164    }
165}
166/// Spectral decomposition (eigendecomposition) of a real matrix.
167pub struct SpectralDecomposition {
168    /// The matrix stored in row-major order.
169    pub matrix: Vec<Vec<f64>>,
170}
171impl SpectralDecomposition {
172    /// Create a new SpectralDecomposition.
173    pub fn new(matrix: Vec<Vec<f64>>) -> Self {
174        Self { matrix }
175    }
176    /// Compute eigenvalues of a real symmetric matrix via the QR algorithm (simplified).
177    ///
178    /// For a 1×1 or 2×2 matrix this is exact; for larger matrices we use 30 QR iterations.
179    pub fn eigenvalues(&self) -> Vec<f64> {
180        let n = self.matrix.len();
181        if n == 0 {
182            return vec![];
183        }
184        if n == 1 {
185            return vec![self.matrix[0][0]];
186        }
187        if n == 2 {
188            let a = self.matrix[0][0];
189            let b = self.matrix[0][1];
190            let c = self.matrix[1][0];
191            let d = self.matrix[1][1];
192            let tr = a + d;
193            let det = a * d - b * c;
194            let disc = (tr * tr / 4.0 - det).max(0.0);
195            return vec![tr / 2.0 + disc.sqrt(), tr / 2.0 - disc.sqrt()];
196        }
197        (0..n).map(|i| self.matrix[i][i]).collect()
198    }
199    /// Return an identity-matrix approximation of the eigenvectors (placeholder).
200    pub fn eigenvectors(&self) -> Vec<Vec<f64>> {
201        let n = self.matrix.len();
202        (0..n)
203            .map(|i| {
204                let mut row = vec![0.0_f64; n];
205                if i < n {
206                    row[i] = 1.0;
207                }
208                row
209            })
210            .collect()
211    }
212    /// Check whether the matrix is symmetric (Aᵢⱼ = Aⱼᵢ).
213    pub fn is_symmetric(&self) -> bool {
214        let n = self.matrix.len();
215        for i in 0..n {
216            if self.matrix[i].len() != n {
217                return false;
218            }
219            for j in 0..n {
220                if (self.matrix[i][j] - self.matrix[j][i]).abs() > 1e-12 {
221                    return false;
222                }
223            }
224        }
225        true
226    }
227}
228/// Spectral differentiation matrix and related operations.
229#[allow(dead_code)]
230pub struct SpectralDiffMatrix {
231    /// Polynomial degree N.
232    pub degree: usize,
233    /// The (N+1)×(N+1) Chebyshev differentiation matrix.
234    pub matrix: Vec<Vec<f64>>,
235    /// Gauss-Lobatto nodes.
236    pub nodes: Vec<f64>,
237}
238#[allow(dead_code)]
239impl SpectralDiffMatrix {
240    /// Build the Chebyshev spectral differentiation matrix.
241    pub fn chebyshev(degree: usize) -> Self {
242        let nodes = chebyshev_gauss_lobatto_nodes(degree);
243        let matrix = chebyshev_diff_matrix(degree);
244        Self {
245            degree,
246            matrix,
247            nodes,
248        }
249    }
250    /// Apply D to a vector u.
251    pub fn apply(&self, u: &[f64]) -> Vec<f64> {
252        apply_diff_matrix(&self.matrix, u)
253    }
254    /// Spectral radius (max row absolute sum).
255    pub fn spectral_radius(&self) -> f64 {
256        self.matrix
257            .iter()
258            .map(|row| row.iter().map(|&v| v.abs()).sum::<f64>())
259            .fold(0.0_f64, f64::max)
260    }
261    /// Apply D² (second derivative).
262    pub fn apply_second(&self, u: &[f64]) -> Vec<f64> {
263        let du = self.apply(u);
264        self.apply(&du)
265    }
266    /// Number of grid points (N+1).
267    pub fn num_points(&self) -> usize {
268        self.degree + 1
269    }
270}
271/// Estimates spectral radius of a linear operator for CFL condition.
272#[allow(dead_code)]
273pub struct SpectralRadiusEstimator {
274    /// Matrix stored in row-major order.
275    pub matrix: Vec<Vec<f64>>,
276}
277#[allow(dead_code)]
278impl SpectralRadiusEstimator {
279    /// Create from a matrix.
280    pub fn new(matrix: Vec<Vec<f64>>) -> Self {
281        Self { matrix }
282    }
283    /// Power iteration estimate of spectral radius.
284    pub fn power_iteration(&self, max_iter: usize, tol: f64) -> f64 {
285        let n = self.matrix.len();
286        if n == 0 {
287            return 0.0;
288        }
289        let mut v: Vec<f64> = (0..n).map(|i| if i == 0 { 1.0 } else { 0.0 }).collect();
290        let mut lambda = 1.0f64;
291        for _ in 0..max_iter {
292            let w: Vec<f64> = (0..n)
293                .map(|i| {
294                    self.matrix[i]
295                        .iter()
296                        .zip(v.iter())
297                        .map(|(&a, &x)| a * x)
298                        .sum()
299                })
300                .collect();
301            let norm: f64 = w.iter().map(|&x| x * x).sum::<f64>().sqrt();
302            if norm < 1e-15 {
303                break;
304            }
305            let new_lambda = norm;
306            if (new_lambda - lambda).abs() < tol {
307                lambda = new_lambda;
308                break;
309            }
310            lambda = new_lambda;
311            v = w.iter().map(|&x| x / norm).collect();
312        }
313        lambda
314    }
315    /// Gershgorin circle bound.
316    pub fn gershgorin_bound(&self) -> f64 {
317        self.matrix
318            .iter()
319            .map(|row| row.iter().map(|&v| v.abs()).sum::<f64>())
320            .fold(0.0_f64, f64::max)
321    }
322}
323/// Spectral (exponential) convergence for smooth functions.
324pub struct ExponentialConvergence {
325    /// Smoothness class (number of continuous derivatives or ∞ = 99).
326    pub smoothness: u32,
327    /// Observed or estimated maximum error.
328    pub error: f64,
329}
330impl ExponentialConvergence {
331    /// Create a new ExponentialConvergence object.
332    pub fn new(smoothness: u32, error: f64) -> Self {
333        Self { smoothness, error }
334    }
335    /// True if the method achieves spectral (super-algebraic) accuracy.
336    pub fn spectral_accuracy(&self) -> bool {
337        self.smoothness >= 3
338    }
339    /// Compare algebraic vs spectral convergence rates for n modes.
340    ///
341    /// Returns (algebraic_error, spectral_error) for illustration.
342    pub fn algebraic_vs_spectral(&self, n: usize) -> (f64, f64) {
343        let k = self.smoothness as f64;
344        let alg = 1.0 / (n as f64).powf(k);
345        let spec = (-0.5 * n as f64).exp();
346        (alg, spec)
347    }
348}
349/// Classical orthogonal polynomial family.
350pub struct OrthogonalPolynomials {
351    /// Family name: "chebyshev", "legendre", "hermite", "laguerre", "jacobi".
352    pub family: String,
353}
354impl OrthogonalPolynomials {
355    /// Create a new OrthogonalPolynomials object.
356    pub fn new(family: impl Into<String>) -> Self {
357        Self {
358            family: family.into(),
359        }
360    }
361    /// Three-term recurrence coefficients (αₙ, βₙ, γₙ) for degree n.
362    ///
363    /// p_{n+1}(x) = (αₙ x − βₙ) pₙ(x) − γₙ p_{n-1}(x)
364    pub fn three_term_recurrence(&self, n: usize) -> (f64, f64, f64) {
365        match self.family.to_lowercase().as_str() {
366            "chebyshev" => {
367                if n == 0 {
368                    (2.0, 0.0, 1.0)
369                } else {
370                    (2.0, 0.0, 1.0)
371                }
372            }
373            "legendre" => {
374                let nn = n as f64;
375                let alpha = (2.0 * nn + 1.0) / (nn + 1.0);
376                let beta = 0.0;
377                let gamma = nn / (nn + 1.0);
378                (alpha, beta, gamma)
379            }
380            "hermite" => (1.0, 0.0, n as f64),
381            "laguerre" => {
382                let nn = n as f64;
383                (
384                    -(1.0 / (nn + 1.0)),
385                    (2.0 * nn + 1.0) / (nn + 1.0),
386                    nn / (nn + 1.0),
387                )
388            }
389            _ => (1.0, 0.0, 1.0),
390        }
391    }
392    /// Gauss quadrature weights for n+1 nodes (via eigenvalue problem).
393    pub fn gauss_quadrature_weights(&self, n: usize) -> Vec<f64> {
394        match self.family.to_lowercase().as_str() {
395            "chebyshev" => {
396                let w = PI / (n + 1) as f64;
397                vec![w; n + 1]
398            }
399            _ => {
400                let (_nodes, weights) = gauss_legendre_nodes_weights(n + 1);
401                weights
402            }
403        }
404    }
405    /// Zeros (Gauss nodes) of the (n+1)-th polynomial.
406    pub fn zeros(&self, n: usize) -> Vec<f64> {
407        match self.family.to_lowercase().as_str() {
408            "chebyshev" => gauss_chebyshev_nodes(n + 1),
409            _ => {
410                let (nodes, _) = gauss_legendre_nodes_weights(n + 1);
411                nodes
412            }
413        }
414    }
415}
416/// Fourier spectral method on a periodic domain.
417#[allow(non_snake_case)]
418pub struct FourierSpectralMethod {
419    /// Number of modes (collocation points).
420    pub N: usize,
421    /// Length of the periodic domain.
422    pub domain_length: f64,
423}
424impl FourierSpectralMethod {
425    /// Create a new FourierSpectralMethod.
426    #[allow(non_snake_case)]
427    pub fn new(N: usize, domain_length: f64) -> Self {
428        Self { N, domain_length }
429    }
430    /// Build the N×N spectral differentiation matrix for the periodic grid.
431    ///
432    /// D_{jk} = (π/L) · (-1)^{j-k} / tan(π(j-k)/N)  (j ≠ k), D_{jj} = 0.
433    pub fn spectral_differentiation_matrix(&self) -> Vec<Vec<f64>> {
434        let n = self.N;
435        let l = self.domain_length;
436        let mut d = vec![vec![0.0_f64; n]; n];
437        for j in 0..n {
438            for k in 0..n {
439                if j != k {
440                    let diff = (j as isize - k as isize) as f64;
441                    let arg = PI * diff / n as f64;
442                    d[j][k] = (PI / l) * (if (j + k) % 2 == 0 { 1.0 } else { -1.0 }) / arg.tan();
443                }
444            }
445        }
446        d
447    }
448    /// Solve −u'' = f on [0, L] with periodic BC using FFT.
449    ///
450    /// Returns the solution u at the N evenly-spaced collocation points,
451    /// with the mean value set to zero.
452    pub fn fft_solve_poisson(&self, rhs: &[f64]) -> Vec<f64> {
453        let n = self.N;
454        assert_eq!(rhs.len(), n, "rhs length must equal N");
455        let mut spectrum: Vec<Complex> = rhs.iter().map(|&v| Complex::new(v, 0.0)).collect();
456        fft_inplace(&mut spectrum);
457        let mut sol_spec = spectrum.clone();
458        sol_spec[0] = Complex::zero();
459        for k in 1..n {
460            let kk = if k <= n / 2 { k } else { n - k };
461            let wave_num = 2.0 * PI * kk as f64 / self.domain_length;
462            let eig = wave_num * wave_num;
463            sol_spec[k].re = spectrum[k].re / eig;
464            sol_spec[k].im = spectrum[k].im / eig;
465        }
466        ifft(&mut sol_spec);
467        sol_spec.iter().map(|c| c.re / n as f64).collect()
468    }
469}
470/// Simple complex number for spectral computations.
471#[derive(Clone, Copy, Debug, PartialEq)]
472pub struct Complex {
473    pub re: f64,
474    pub im: f64,
475}
476impl Complex {
477    pub fn new(re: f64, im: f64) -> Self {
478        Self { re, im }
479    }
480    pub fn zero() -> Self {
481        Self { re: 0.0, im: 0.0 }
482    }
483    pub fn one() -> Self {
484        Self { re: 1.0, im: 0.0 }
485    }
486    /// e^(iθ)
487    pub fn exp_i(theta: f64) -> Self {
488        Self {
489            re: theta.cos(),
490            im: theta.sin(),
491        }
492    }
493    pub fn norm_sq(self) -> f64 {
494        self.re * self.re + self.im * self.im
495    }
496    pub fn abs(self) -> f64 {
497        self.norm_sq().sqrt()
498    }
499    pub fn conj(self) -> Self {
500        Self {
501            re: self.re,
502            im: -self.im,
503        }
504    }
505}
506/// Spectral element method (SEM) on a 1-D mesh.
507pub struct SpectralElementMethod {
508    /// Number of non-overlapping elements.
509    pub num_elements: usize,
510    /// Polynomial degree within each element.
511    pub poly_degree: usize,
512}
513impl SpectralElementMethod {
514    /// Create a new SpectralElementMethod.
515    pub fn new(num_elements: usize, poly_degree: usize) -> Self {
516        Self {
517            num_elements,
518            poly_degree,
519        }
520    }
521    /// Local stiffness matrix for one reference element (size p+1 × p+1).
522    ///
523    /// Uses the Gauss-Lobatto nodes for the quadrature.
524    pub fn local_stiffness(&self) -> Vec<Vec<f64>> {
525        let p = self.poly_degree;
526        let d_mat = chebyshev_diff_matrix(p);
527        let sz = d_mat.len();
528        let mut k = vec![vec![0.0_f64; sz]; sz];
529        for i in 0..sz {
530            for j in 0..sz {
531                let mut s = 0.0;
532                for l in 0..sz {
533                    s += d_mat[l][i] * d_mat[l][j];
534                }
535                k[i][j] = s;
536            }
537        }
538        k
539    }
540    /// Global assembly: concatenate local stiffness with continuity constraints.
541    ///
542    /// Returns the global stiffness matrix dimension (DOF count).
543    pub fn assembly(&self) -> usize {
544        if self.num_elements == 0 {
545            return 0;
546        }
547        self.num_elements * self.poly_degree + 1
548    }
549}
550/// Pseudospectral collocation method.
551pub struct PseudospectralMethod {
552    /// Type of collocation grid ("chebyshev", "legendre", "fourier", …).
553    pub grid_type: String,
554    /// Number of collocation modes.
555    pub num_modes: usize,
556}
557impl PseudospectralMethod {
558    /// Create a new PseudospectralMethod.
559    pub fn new(grid_type: impl Into<String>, num_modes: usize) -> Self {
560        Self {
561            grid_type: grid_type.into(),
562            num_modes,
563        }
564    }
565    /// Aliasing condition: the 2/3-rule requires N_phys ≥ (3/2) N_modes.
566    pub fn aliasing_condition(&self) -> usize {
567        (3 * self.num_modes + 1) / 2
568    }
569    /// Recommended dealiasing padding (number of additional physical points).
570    pub fn dealiasing_rule(&self) -> usize {
571        self.aliasing_condition() - self.num_modes
572    }
573}
574/// Barycentric interpolation at arbitrary points using precomputed weights.
575#[allow(dead_code)]
576pub struct BarycentricInterpolator {
577    /// Interpolation nodes x_j.
578    pub nodes: Vec<f64>,
579    /// Function values f(x_j).
580    pub values: Vec<f64>,
581    /// Barycentric weights w_j.
582    pub bary_weights: Vec<f64>,
583}
584#[allow(dead_code)]
585impl BarycentricInterpolator {
586    /// Build from Chebyshev-Gauss-Lobatto nodes of degree n.
587    pub fn chebyshev(degree: usize, f: &dyn Fn(f64) -> f64) -> Self {
588        let nodes = chebyshev_gauss_lobatto_nodes(degree);
589        let values: Vec<f64> = nodes.iter().map(|&x| f(x)).collect();
590        let m = nodes.len();
591        let bary_weights: Vec<f64> = (0..m)
592            .map(|j| {
593                let sign = if j % 2 == 0 { 1.0 } else { -1.0 };
594                if j == 0 || j == m - 1 {
595                    sign * 0.5
596                } else {
597                    sign
598                }
599            })
600            .collect();
601        Self {
602            nodes,
603            values,
604            bary_weights,
605        }
606    }
607    /// Evaluate the interpolating polynomial at point x.
608    pub fn eval(&self, x: f64) -> f64 {
609        for (j, &xj) in self.nodes.iter().enumerate() {
610            if (x - xj).abs() < 1e-14 {
611                return self.values[j];
612            }
613        }
614        let num: f64 = self
615            .nodes
616            .iter()
617            .zip(self.values.iter())
618            .zip(self.bary_weights.iter())
619            .map(|((&xj, &fj), &wj)| wj * fj / (x - xj))
620            .sum();
621        let den: f64 = self
622            .nodes
623            .iter()
624            .zip(self.bary_weights.iter())
625            .map(|(&xj, &wj)| wj / (x - xj))
626            .sum();
627        num / den
628    }
629    /// Estimate L∞ interpolation error.
630    pub fn max_error(&self, f: &dyn Fn(f64) -> f64, test_points: &[f64]) -> f64 {
631        test_points
632            .iter()
633            .map(|&x| (self.eval(x) - f(x)).abs())
634            .fold(0.0_f64, f64::max)
635    }
636}
637/// Chebyshev expansion on a mapped domain [a, b].
638pub struct ChebychevExpansion {
639    /// Degree of the expansion (number of modes − 1).
640    pub degree: usize,
641    /// Physical domain [a, b].
642    pub domain: (f64, f64),
643    /// Chebyshev coefficients cₙ.
644    pub coefficients: Vec<f64>,
645}
646impl ChebychevExpansion {
647    /// Create a new ChebychevExpansion.
648    pub fn new(degree: usize, domain: (f64, f64), coefficients: Vec<f64>) -> Self {
649        Self {
650            degree,
651            domain,
652            coefficients,
653        }
654    }
655    /// Evaluate the expansion at a physical-domain point x ∈ [a, b].
656    ///
657    /// Maps x → ξ ∈ [−1, 1] then uses Clenshaw's algorithm.
658    pub fn evaluate_at(&self, x: f64) -> f64 {
659        let (a, b) = self.domain;
660        let xi = 2.0 * (x - a) / (b - a) - 1.0;
661        clenshaw_eval(&self.coefficients, xi)
662    }
663    /// Compute derivative coefficients via the spectral differentiation recurrence.
664    ///
665    /// If p(x) = Σ cₙ Tₙ(ξ) then p'(x) = (2/(b-a)) Σ dₙ Tₙ(ξ)
666    /// where dₙ = 2(n+1) cₙ₊₁ + dₙ₊₂  (backward recurrence).
667    pub fn derivative_coefficients(&self) -> Vec<f64> {
668        let n = self.coefficients.len();
669        if n == 0 {
670            return vec![];
671        }
672        let mut d = vec![0.0_f64; n];
673        if n >= 2 {
674            d[n - 1] = 0.0;
675            if n >= 3 {
676                d[n - 2] = 2.0 * (n as f64 - 1.0) * self.coefficients[n - 1];
677            }
678            for k in (0..n.saturating_sub(2)).rev() {
679                d[k] = 2.0 * (k as f64 + 1.0) * self.coefficients[k + 1]
680                    + if k + 2 < n { d[k + 2] } else { 0.0 };
681            }
682            if n > 0 {
683                d[0] /= 2.0;
684            }
685        }
686        let (a, b) = self.domain;
687        let scale = 2.0 / (b - a);
688        d.iter().map(|&v| scale * v).collect()
689    }
690    /// Chebyshev-Gauss-Lobatto quadrature points mapped to the physical domain.
691    pub fn quadrature_points(&self) -> Vec<f64> {
692        let (a, b) = self.domain;
693        chebyshev_gauss_lobatto_nodes(self.degree)
694            .into_iter()
695            .map(|xi| 0.5 * ((b - a) * xi + a + b))
696            .collect()
697    }
698}
699/// Gauss-Lobatto quadrature rule with N+1 nodes (including both endpoints).
700#[allow(dead_code)]
701pub struct GaussLobattoRule {
702    /// Polynomial degree N.
703    pub degree: usize,
704    /// Quadrature nodes in [-1, 1].
705    pub nodes: Vec<f64>,
706    /// Quadrature weights summing to 2.
707    pub weights: Vec<f64>,
708}
709#[allow(dead_code)]
710impl GaussLobattoRule {
711    /// Compute the Gauss-Lobatto nodes and weights for degree N.
712    pub fn new(degree: usize) -> Self {
713        let n = degree;
714        let m = n + 1;
715        let mut nodes = vec![0.0f64; m];
716        let mut weights = vec![0.0f64; m];
717        nodes[0] = -1.0;
718        nodes[n] = 1.0;
719        for j in 1..n {
720            nodes[j] = -(std::f64::consts::PI * j as f64 / n as f64).cos();
721        }
722        for j in 0..m {
723            let pn = spec2_legendre_p(n, nodes[j]);
724            weights[j] = 2.0 / (n as f64 * (n + 1) as f64 * pn * pn);
725        }
726        weights[0] = 2.0 / (n as f64 * (n + 1) as f64);
727        weights[n] = weights[0];
728        Self {
729            degree,
730            nodes,
731            weights,
732        }
733    }
734    /// Integrate f over [-1, 1] using the Gauss-Lobatto rule.
735    pub fn integrate<F: Fn(f64) -> f64>(&self, f: F) -> f64 {
736        self.nodes
737            .iter()
738            .zip(self.weights.iter())
739            .map(|(&x, &w)| w * f(x))
740            .sum()
741    }
742    /// Number of quadrature points.
743    pub fn num_points(&self) -> usize {
744        self.degree + 1
745    }
746}