Skip to main content

zenith_float_num/
chebyshev.rs

1//! Chebyshev interpolation and Clenshaw evaluation on [`ExactNum`].
2
3use crate::defs::WORD_BIT_SIZE;
4use crate::Consts;
5use crate::ExactNum;
6use crate::RoundingMode;
7use crate::NAN;
8use alloc::vec::Vec;
9
10/// Maximum number of Chebyshev coefficients [`chebyshev_coeffs`] will compute.
11pub const CHEBYSHEV_MAX_DEGREE: usize = 256;
12
13fn work_p(p: usize) -> usize {
14    p.saturating_add(WORD_BIT_SIZE)
15}
16
17fn finite(x: &ExactNum) -> bool {
18    !x.is_nan() && !x.is_inf()
19}
20
21/// Map `x ∈ [a, b]` to `t ∈ [-1, 1]`. `None` if `b ≤ a` or a part is non-finite.
22fn map_to_unit(
23    x: &ExactNum,
24    a: &ExactNum,
25    b: &ExactNum,
26    p: usize,
27    rm: RoundingMode,
28) -> Option<ExactNum> {
29    if !finite(x) || !finite(a) || !finite(b) {
30        return None;
31    }
32    if a.cmp(b) != Some(-1) {
33        return None;
34    }
35    let two = ExactNum::from_u8(2, p);
36    let num = two.mul(x, p, rm).sub(a, p, rm).sub(b, p, rm);
37    let den = b.sub(a, p, rm);
38    if den.is_zero() {
39        return None;
40    }
41    Some(num.div(&den, p, rm))
42}
43
44fn map_from_unit(t: &ExactNum, a: &ExactNum, b: &ExactNum, p: usize, rm: RoundingMode) -> ExactNum {
45    let two = ExactNum::from_u8(2, p);
46    let mid = a.add(b, p, rm).div(&two, p, rm);
47    let half = b.sub(a, p, rm).div(&two, p, rm);
48    mid.add(&half.mul(t, p, rm), p, rm)
49}
50
51/// Chebyshev–Gauss node `t_k = cos(π (2k+1) / (2n))` on `[-1, 1]`.
52fn unit_node(k: usize, n: usize, p: usize, rm: RoundingMode, cc: &mut Consts) -> ExactNum {
53    let pi = cc.pi(p, rm);
54    let num = ExactNum::from_u32((2 * k + 1) as u32, p);
55    let den = ExactNum::from_u32((2 * n) as u32, p);
56    let theta = pi.mul(&num, p, rm).div(&den, p, rm);
57    theta.cos(p, rm, cc)
58}
59
60/// Discrete cosine of order `j` at node `k`: `cos(π j (2k+1) / (2n))`.
61fn node_cos_j(
62    j: usize,
63    k: usize,
64    n: usize,
65    p: usize,
66    rm: RoundingMode,
67    cc: &mut Consts,
68) -> ExactNum {
69    if j == 0 {
70        return ExactNum::from_u8(1, p);
71    }
72    let pi = cc.pi(p, rm);
73    let num = ExactNum::from_u32((j * (2 * k + 1)) as u32, p);
74    let den = ExactNum::from_u32((2 * n) as u32, p);
75    let theta = pi.mul(&num, p, rm).div(&den, p, rm);
76    theta.cos(p, rm, cc)
77}
78
79/// Interpolation coefficients of `f` on `[a, b]` at the `n` Chebyshev–Gauss nodes.
80///
81/// `c_0 = (1/n) Σ f(x_k)`, `c_j = (2/n) Σ f(x_k) T_j(t_k)` for `j ≥ 1`,
82/// so `f(x) ≈ Σ_{j=0}^{n-1} c_j T_j(t(x))` with `t` the affine map to `[-1, 1]`.
83///
84/// `None` if `n` is 0 or greater than [`CHEBYSHEV_MAX_DEGREE`], or if the
85/// interval is not a finite `a < b`.
86pub fn chebyshev_coeffs<F>(
87    mut f: F,
88    n: usize,
89    a: &ExactNum,
90    b: &ExactNum,
91    p: usize,
92    rm: RoundingMode,
93    cc: &mut Consts,
94) -> Option<Vec<ExactNum>>
95where
96    F: FnMut(&ExactNum, usize, RoundingMode, &mut Consts) -> ExactNum,
97{
98    if n == 0 || n > CHEBYSHEV_MAX_DEGREE {
99        return None;
100    }
101    if !finite(a) || !finite(b) || a.cmp(b) != Some(-1) {
102        return None;
103    }
104    let wrk = work_p(p);
105    let mut fx = Vec::with_capacity(n);
106    for k in 0..n {
107        let t = unit_node(k, n, wrk, RoundingMode::None, cc);
108        let x = map_from_unit(&t, a, b, wrk, RoundingMode::None);
109        let y = f(&x, wrk, RoundingMode::None, cc);
110        if !finite(&y) {
111            return None;
112        }
113        fx.push(y);
114    }
115    let n_f = ExactNum::from_u32(n as u32, wrk);
116    let two = ExactNum::from_u8(2, wrk);
117    let mut coeffs = Vec::with_capacity(n);
118    for j in 0..n {
119        let mut s = ExactNum::new(wrk);
120        for k in 0..n {
121            let w = node_cos_j(j, k, n, wrk, RoundingMode::None, cc);
122            s = s.add(
123                &fx[k].mul(&w, wrk, RoundingMode::None),
124                wrk,
125                RoundingMode::None,
126            );
127        }
128        let scale = if j == 0 {
129            ExactNum::from_u8(1, wrk).div(&n_f, wrk, RoundingMode::None)
130        } else {
131            two.div(&n_f, wrk, RoundingMode::None)
132        };
133        let mut c = s.mul(&scale, wrk, RoundingMode::None);
134        let _ = c.set_precision(p, rm);
135        coeffs.push(c);
136    }
137    Some(coeffs)
138}
139
140/// Clenshaw recurrence for `Σ_{k=0}^{n-1} c_k T_k(x)` at precision `p`.
141///
142/// `x` is the Chebyshev variable on `[-1, 1]` (no interval map). Empty
143/// `coeffs` is zero. A non-finite coefficient or `x` yields `NaN`.
144pub fn clenshaw(coeffs: &[ExactNum], x: &ExactNum, p: usize, rm: RoundingMode) -> ExactNum {
145    if coeffs.is_empty() {
146        return ExactNum::new(p);
147    }
148    if !finite(x) || coeffs.iter().any(|c| !finite(c)) {
149        return NAN;
150    }
151    let wrk = work_p(p);
152    let two = ExactNum::from_u8(2, wrk);
153    let mut b1 = ExactNum::new(wrk);
154    let mut b2 = ExactNum::new(wrk);
155    for c in coeffs.iter().skip(1).rev() {
156        let t = two
157            .mul(x, wrk, RoundingMode::None)
158            .mul(&b1, wrk, RoundingMode::None)
159            .sub(&b2, wrk, RoundingMode::None)
160            .add(c, wrk, RoundingMode::None);
161        b2 = b1;
162        b1 = t;
163    }
164    let mut y = x
165        .mul(&b1, wrk, RoundingMode::None)
166        .sub(&b2, wrk, RoundingMode::None)
167        .add(&coeffs[0], wrk, RoundingMode::None);
168    let _ = y.set_precision(p, rm);
169    y
170}
171
172/// Evaluate the Chebyshev expansion of `coeffs` at `x ∈ [a, b]`.
173///
174/// Maps `x` to `[-1, 1]` and calls [`clenshaw`]. A bad interval or
175/// non-finite input is `NaN`.
176pub fn chebyshev_eval(
177    coeffs: &[ExactNum],
178    x: &ExactNum,
179    a: &ExactNum,
180    b: &ExactNum,
181    p: usize,
182    rm: RoundingMode,
183) -> ExactNum {
184    match map_to_unit(x, a, b, p, rm) {
185        Some(t) => clenshaw(coeffs, &t, p, rm),
186        None => NAN,
187    }
188}
189
190/// ℓ¹ tail `Σ_{k≥1} |c_k|` at precision `p`.
191///
192/// On `[-1, 1]`, `|T_k| ≤ 1`, so this bounds `|Σ c_k T_k(x) - c_0|`.
193/// Empty or a single coefficient is zero.
194pub fn chebyshev_error_bound(coeffs: &[ExactNum], p: usize) -> ExactNum {
195    let mut acc = ExactNum::new(p);
196    for c in coeffs.iter().skip(1) {
197        acc = acc.add(&c.abs(), p, RoundingMode::ToEven);
198    }
199    acc
200}
201
202/// Direct `Σ c_k T_k(x)` via the three-term recurrence (test / comparison).
203#[cfg(test)]
204fn chebyshev_sum_direct(coeffs: &[ExactNum], x: &ExactNum, p: usize, rm: RoundingMode) -> ExactNum {
205    if coeffs.is_empty() {
206        return ExactNum::new(p);
207    }
208    let wrk = work_p(p);
209    let one = ExactNum::from_u8(1, wrk);
210    let two = ExactNum::from_u8(2, wrk);
211    let mut t_prev = one;
212    let mut acc = coeffs[0].clone();
213    let _ = acc.set_precision(wrk, RoundingMode::None);
214    if coeffs.len() == 1 {
215        let _ = acc.set_precision(p, rm);
216        return acc;
217    }
218    let mut t_cur = x.clone();
219    let _ = t_cur.set_precision(wrk, RoundingMode::None);
220    acc = acc.add(
221        &coeffs[1].mul(&t_cur, wrk, RoundingMode::None),
222        wrk,
223        RoundingMode::None,
224    );
225    for c in coeffs.iter().skip(2) {
226        let t_next = two
227            .mul(x, wrk, RoundingMode::None)
228            .mul(&t_cur, wrk, RoundingMode::None)
229            .sub(&t_prev, wrk, RoundingMode::None);
230        acc = acc.add(
231            &c.mul(&t_next, wrk, RoundingMode::None),
232            wrk,
233            RoundingMode::None,
234        );
235        t_prev = t_cur;
236        t_cur = t_next;
237    }
238    let _ = acc.set_precision(p, rm);
239    acc
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::Consts;
246
247    /// Plan gold: 20-term `exp` on `[-1, 1]`.
248    const CHEBYSHEV_EXP_TERMS: usize = 20;
249    /// Plan gold: error smaller than `10^{-15}`.
250    const CHEBYSHEV_EXP_ERR_DIGITS: isize = 15;
251
252    fn gold_p() -> (usize, RoundingMode) {
253        (256, RoundingMode::ToEven)
254    }
255
256    #[test]
257    fn chebyshev_exp_nodes_clenshaw() {
258        let (p, rm) = gold_p();
259        let mut cc = Consts::new().expect("consts");
260        let a = ExactNum::from_i64(-1, p);
261        let b = ExactNum::from_u8(1, p);
262        let coeffs = chebyshev_coeffs(
263            |x, p, rm, cc| x.exp(p, rm, cc),
264            CHEBYSHEV_EXP_TERMS,
265            &a,
266            &b,
267            p,
268            rm,
269            &mut cc,
270        )
271        .expect("coeffs");
272        assert_eq!(coeffs.len(), CHEBYSHEV_EXP_TERMS);
273
274        let ten = ExactNum::from_u8(10, p);
275        let tol = ExactNum::from_u8(1, p).div(&ten.powsi(CHEBYSHEV_EXP_ERR_DIGITS, p, rm), p, rm);
276        for &xi in &[-1i64, 0, 1] {
277            let x = ExactNum::from_i64(xi, p);
278            let approx = chebyshev_eval(&coeffs, &x, &a, &b, p, rm);
279            let exact = x.exp(p, rm, &mut cc);
280            let err = approx.sub(&exact, p, rm).abs();
281            assert!(
282                err.is_zero() || err.cmp(&tol) == Some(-1),
283                "exp({}) error not < 10^{{-15}}",
284                xi
285            );
286        }
287
288        let node_slack =
289            ExactNum::from_u8(1, p).ldexp(-((p as i32) - (crate::WORD_BIT_SIZE as i32)), p, rm);
290        for k in 0..CHEBYSHEV_EXP_TERMS {
291            let t = unit_node(k, CHEBYSHEV_EXP_TERMS, p, rm, &mut cc);
292            let x = map_from_unit(&t, &a, &b, p, rm);
293            let approx = chebyshev_eval(&coeffs, &x, &a, &b, p, rm);
294            let exact = x.exp(p, rm, &mut cc);
295            let err = approx.sub(&exact, p, rm).abs();
296            assert!(
297                err.is_zero() || err.cmp(&node_slack) == Some(-1),
298                "node {k} interpolant farther than a working word from exp"
299            );
300        }
301
302        const IDENTITY_NODES: usize = 4;
303        let id_c = chebyshev_coeffs(
304            |x, _p, _rm, _cc| x.clone(),
305            IDENTITY_NODES,
306            &a,
307            &b,
308            p,
309            rm,
310            &mut cc,
311        )
312        .expect("identity coeffs");
313        let one = ExactNum::from_u8(1, p);
314        assert_eq!(id_c[1].cmp(&one), Some(0));
315        for k in 0..IDENTITY_NODES {
316            let t = unit_node(k, IDENTITY_NODES, p, rm, &mut cc);
317            let x = map_from_unit(&t, &a, &b, p, rm);
318            let approx = chebyshev_eval(&id_c, &x, &a, &b, p, rm);
319            let err = approx.sub(&x, p, rm).abs();
320            assert!(
321                err.is_zero() || err.cmp(&node_slack) == Some(-1),
322                "identity node {k} farther than a working word from x"
323            );
324        }
325
326        let c = [ExactNum::from_u8(1, p), ExactNum::from_u8(2, p), ExactNum::from_u8(3, p)];
327        let half = ExactNum::from_u8(1, p).div(&ExactNum::from_u8(2, p), p, rm);
328        let via_clenshaw = clenshaw(&c, &half, p, rm);
329        let via_direct = chebyshev_sum_direct(&c, &half, p, rm);
330        assert_eq!(via_clenshaw.cmp(&half), Some(0));
331        assert_eq!(via_direct.cmp(&half), Some(0));
332        assert_eq!(via_clenshaw.cmp(&via_direct), Some(0));
333
334        assert!(
335            chebyshev_coeffs(|x, p, rm, cc| x.exp(p, rm, cc), 0, &a, &b, p, rm, &mut cc).is_none()
336        );
337        assert!(chebyshev_error_bound(&c, p).cmp(&ExactNum::from_u8(5, p)) == Some(0));
338    }
339}