Skip to main content

ruvector_math/spectral/
chebyshev.rs

1//! Chebyshev Polynomials
2//!
3//! Efficient polynomial approximation using Chebyshev basis.
4//! Key for matrix function approximation without eigendecomposition.
5
6use std::f64::consts::PI;
7
8/// Chebyshev polynomial of the first kind
9#[derive(Debug, Clone)]
10pub struct ChebyshevPolynomial {
11    /// Polynomial degree
12    pub degree: usize,
13}
14
15impl ChebyshevPolynomial {
16    /// Create Chebyshev polynomial T_n
17    pub fn new(degree: usize) -> Self {
18        Self { degree }
19    }
20
21    /// Evaluate T_n(x) using recurrence
22    /// T_0(x) = 1, T_1(x) = x, T_{n+1}(x) = 2x·T_n(x) - T_{n-1}(x)
23    pub fn eval(&self, x: f64) -> f64 {
24        if self.degree == 0 {
25            return 1.0;
26        }
27        if self.degree == 1 {
28            return x;
29        }
30
31        let mut t_prev = 1.0;
32        let mut t_curr = x;
33
34        for _ in 2..=self.degree {
35            let t_next = 2.0 * x * t_curr - t_prev;
36            t_prev = t_curr;
37            t_curr = t_next;
38        }
39
40        t_curr
41    }
42
43    /// Evaluate all Chebyshev polynomials T_0(x) through T_n(x)
44    pub fn eval_all(x: f64, max_degree: usize) -> Vec<f64> {
45        if max_degree == 0 {
46            return vec![1.0];
47        }
48
49        let mut result = Vec::with_capacity(max_degree + 1);
50        result.push(1.0);
51        result.push(x);
52
53        for k in 2..=max_degree {
54            let t_k = 2.0 * x * result[k - 1] - result[k - 2];
55            result.push(t_k);
56        }
57
58        result
59    }
60
61    /// Chebyshev nodes for interpolation: x_k = cos((2k+1)π/(2n))
62    pub fn nodes(n: usize) -> Vec<f64> {
63        (0..n)
64            .map(|k| ((2 * k + 1) as f64 * PI / (2 * n) as f64).cos())
65            .collect()
66    }
67
68    /// Derivative: T'_n(x) = n * U_{n-1}(x) where U is Chebyshev of second kind
69    pub fn derivative(&self, x: f64) -> f64 {
70        if self.degree == 0 {
71            return 0.0;
72        }
73        if self.degree == 1 {
74            return 1.0;
75        }
76
77        // Use: T'_n(x) = n * U_{n-1}(x)
78        // where U_0 = 1, U_1 = 2x, U_{n+1} = 2x*U_n - U_{n-1}
79        let n = self.degree;
80        let mut u_prev = 1.0;
81        let mut u_curr = 2.0 * x;
82
83        for _ in 2..n {
84            let u_next = 2.0 * x * u_curr - u_prev;
85            u_prev = u_curr;
86            u_curr = u_next;
87        }
88
89        n as f64 * if n == 1 { u_prev } else { u_curr }
90    }
91}
92
93/// Chebyshev expansion of a function
94/// f(x) ≈ Σ c_k T_k(x)
95#[derive(Debug, Clone)]
96pub struct ChebyshevExpansion {
97    /// Chebyshev coefficients c_k
98    pub coefficients: Vec<f64>,
99}
100
101impl ChebyshevExpansion {
102    /// Create from coefficients
103    pub fn new(coefficients: Vec<f64>) -> Self {
104        Self { coefficients }
105    }
106
107    /// Approximate function on [-1, 1] using n+1 Chebyshev nodes
108    pub fn from_function<F: Fn(f64) -> f64>(f: F, degree: usize) -> Self {
109        let n = degree + 1;
110        let nodes = ChebyshevPolynomial::nodes(n);
111
112        // Evaluate function at nodes
113        let f_values: Vec<f64> = nodes.iter().map(|&x| f(x)).collect();
114
115        // Compute coefficients via DCT-like formula
116        let mut coefficients = Vec::with_capacity(n);
117
118        for k in 0..n {
119            let mut c_k = 0.0;
120            for (j, &f_j) in f_values.iter().enumerate() {
121                let t_k_at_node = ChebyshevPolynomial::new(k).eval(nodes[j]);
122                c_k += f_j * t_k_at_node;
123            }
124            c_k *= 2.0 / n as f64;
125            if k == 0 {
126                c_k *= 0.5;
127            }
128            coefficients.push(c_k);
129        }
130
131        Self { coefficients }
132    }
133
134    /// Approximate exp(-t*x) for heat kernel (x in [0, 2])
135    /// Maps [0, 2] to [-1, 1] via x' = x - 1
136    pub fn heat_kernel(t: f64, degree: usize) -> Self {
137        Self::from_function(
138            |x| {
139                let exponent = -t * (x + 1.0);
140                // Clamp to prevent overflow (exp(709) ≈ max f64, exp(-745) ≈ 0)
141                let clamped = exponent.clamp(-700.0, 700.0);
142                clamped.exp()
143            },
144            degree,
145        )
146    }
147
148    /// Approximate low-pass filter: 1 if λ < cutoff, 0 otherwise
149    /// Smooth transition via sigmoid-like function
150    pub fn low_pass(cutoff: f64, degree: usize) -> Self {
151        let steepness = 10.0 / cutoff.max(0.1);
152        Self::from_function(
153            |x| {
154                let lambda = (x + 1.0) / 2.0 * 2.0; // Map [-1,1] to [0,2]
155                let exponent = steepness * (lambda - cutoff);
156                // Clamp to prevent overflow
157                let clamped = exponent.clamp(-700.0, 700.0);
158                1.0 / (1.0 + clamped.exp())
159            },
160            degree,
161        )
162    }
163
164    /// Evaluate expansion at point x using Clenshaw recurrence
165    /// More numerically stable than direct summation
166    pub fn eval(&self, x: f64) -> f64 {
167        if self.coefficients.is_empty() {
168            return 0.0;
169        }
170        if self.coefficients.len() == 1 {
171            return self.coefficients[0];
172        }
173
174        // Clenshaw recurrence
175        let n = self.coefficients.len();
176        let mut b_next = 0.0;
177        let mut b_curr = 0.0;
178
179        for k in (1..n).rev() {
180            let b_prev = 2.0 * x * b_curr - b_next + self.coefficients[k];
181            b_next = b_curr;
182            b_curr = b_prev;
183        }
184
185        self.coefficients[0] + x * b_curr - b_next
186    }
187
188    /// Evaluate expansion on vector: apply filter to each component
189    pub fn eval_vector(&self, x: &[f64]) -> Vec<f64> {
190        x.iter().map(|&xi| self.eval(xi)).collect()
191    }
192
193    /// Degree of expansion
194    pub fn degree(&self) -> usize {
195        self.coefficients.len().saturating_sub(1)
196    }
197
198    /// Truncate to lower degree
199    pub fn truncate(&self, new_degree: usize) -> Self {
200        let n = (new_degree + 1).min(self.coefficients.len());
201        Self {
202            coefficients: self.coefficients[..n].to_vec(),
203        }
204    }
205
206    /// Add two expansions
207    pub fn add(&self, other: &Self) -> Self {
208        let max_len = self.coefficients.len().max(other.coefficients.len());
209        let mut coefficients = vec![0.0; max_len];
210
211        for (i, &c) in self.coefficients.iter().enumerate() {
212            coefficients[i] += c;
213        }
214        for (i, &c) in other.coefficients.iter().enumerate() {
215            coefficients[i] += c;
216        }
217
218        Self { coefficients }
219    }
220
221    /// Scale by constant
222    pub fn scale(&self, s: f64) -> Self {
223        Self {
224            coefficients: self.coefficients.iter().map(|&c| c * s).collect(),
225        }
226    }
227
228    /// Derivative expansion
229    /// d/dx Σ c_k T_k(x) = Σ c'_k T_k(x)
230    pub fn derivative(&self) -> Self {
231        let n = self.coefficients.len();
232        if n <= 1 {
233            return Self::new(vec![0.0]);
234        }
235
236        let mut d_coeffs = vec![0.0; n - 1];
237
238        // Backward recurrence for derivative coefficients
239        for k in (0..n - 1).rev() {
240            d_coeffs[k] = 2.0 * (k + 1) as f64 * self.coefficients[k + 1];
241            if k + 2 < n {
242                d_coeffs[k] += if k == 0 { 0.0 } else { d_coeffs[k + 2] };
243            }
244        }
245
246        // First coefficient needs halving
247        if !d_coeffs.is_empty() {
248            d_coeffs[0] *= 0.5;
249        }
250
251        Self {
252            coefficients: d_coeffs,
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_chebyshev_polynomial() {
263        // T_0(x) = 1
264        assert!((ChebyshevPolynomial::new(0).eval(0.5) - 1.0).abs() < 1e-10);
265
266        // T_1(x) = x
267        assert!((ChebyshevPolynomial::new(1).eval(0.5) - 0.5).abs() < 1e-10);
268
269        // T_2(x) = 2x² - 1
270        let t2_at_half = 2.0 * 0.5 * 0.5 - 1.0;
271        assert!((ChebyshevPolynomial::new(2).eval(0.5) - t2_at_half).abs() < 1e-10);
272
273        // T_3(x) = 4x³ - 3x
274        let t3_at_half = 4.0 * 0.5_f64.powi(3) - 3.0 * 0.5;
275        assert!((ChebyshevPolynomial::new(3).eval(0.5) - t3_at_half).abs() < 1e-10);
276    }
277
278    #[test]
279    fn test_eval_all() {
280        let x = 0.5;
281        let all = ChebyshevPolynomial::eval_all(x, 5);
282
283        assert_eq!(all.len(), 6);
284        for (k, &t_k) in all.iter().enumerate() {
285            let expected = ChebyshevPolynomial::new(k).eval(x);
286            assert!((t_k - expected).abs() < 1e-10);
287        }
288    }
289
290    #[test]
291    fn test_chebyshev_nodes() {
292        let nodes = ChebyshevPolynomial::nodes(4);
293        assert_eq!(nodes.len(), 4);
294
295        // All nodes should be in [-1, 1]
296        for &x in &nodes {
297            assert!(x >= -1.0 && x <= 1.0);
298        }
299    }
300
301    #[test]
302    fn test_expansion_constant() {
303        let expansion = ChebyshevExpansion::from_function(|_| 5.0, 3);
304
305        // Should approximate 5.0 everywhere
306        for x in [-0.9, -0.5, 0.0, 0.5, 0.9] {
307            assert!((expansion.eval(x) - 5.0).abs() < 0.1);
308        }
309    }
310
311    #[test]
312    fn test_expansion_linear() {
313        let expansion = ChebyshevExpansion::from_function(|x| 2.0 * x + 1.0, 5);
314
315        for x in [-0.8, -0.3, 0.0, 0.4, 0.7] {
316            let expected = 2.0 * x + 1.0;
317            assert!(
318                (expansion.eval(x) - expected).abs() < 0.1,
319                "x={}, expected={}, got={}",
320                x,
321                expected,
322                expansion.eval(x)
323            );
324        }
325    }
326
327    #[test]
328    fn test_heat_kernel() {
329        let heat = ChebyshevExpansion::heat_kernel(1.0, 10);
330
331        // At x = -1 (λ = 0): exp(0) = 1
332        let at_zero = heat.eval(-1.0);
333        assert!((at_zero - 1.0).abs() < 0.1);
334
335        // At x = 1 (λ = 2): exp(-2) ≈ 0.135
336        let at_two = heat.eval(1.0);
337        assert!((at_two - (-2.0_f64).exp()).abs() < 0.1);
338    }
339
340    #[test]
341    fn test_clenshaw_stability() {
342        // High degree expansion should still be numerically stable
343        let expansion = ChebyshevExpansion::from_function(|x| x.sin(), 20);
344
345        for x in [-0.9, 0.0, 0.9] {
346            let approx = expansion.eval(x);
347            let exact = x.sin();
348            assert!(
349                (approx - exact).abs() < 0.01,
350                "x={}, approx={}, exact={}",
351                x,
352                approx,
353                exact
354            );
355        }
356    }
357}