Skip to main content

zenith_float_num/
complex_airy.rs

1//! Complex Airy \(\mathrm{Ai}\) / \(\mathrm{Bi}\).
2//!
3//! Software-limb [`ExactComplex`] arithmetic. The real line uses the real
4//! kernels. Off the real axis the Taylor pair is used for small \(\lvert z\rvert\);
5//! large \(\lvert z\rvert\) uses the decaying/growing asymptotic in
6//! \(\lvert\mathrm{arg}\,z\rvert\le 2\pi/3\) (Ai) or \(\pi/3\) (Bi), otherwise
7//! the \(\omega\)-connection. The real series is not evaluated at \(\lvert z\rvert\).
8
9use crate::common::util::round_p;
10use crate::complex_special::half_c;
11use crate::complex_special::neg_c;
12use crate::complex_special::pi_c;
13use crate::complex_special::series_term_cap;
14use crate::complex_special::term_negligible;
15use crate::complex_special::two_c;
16use crate::complex_special::ziv_complex;
17use crate::Consts;
18use crate::ExactComplex;
19use crate::ExactNum;
20use crate::RoundingMode;
21
22/// \(\lvert z\rvert\) below this uses the Taylor pair \((f,g)\).
23const AIRY_SERIES_THRESHOLD: u32 = 8;
24
25fn abs_below(z: &ExactComplex, bound: u32, p: usize) -> bool {
26    let a = z.abs(p, RoundingMode::None);
27    let b = ExactNum::from_u32(bound, p);
28    matches!(a.cmp(&b), Some(c) if c < 0)
29}
30
31fn arg_abs_le(z: &ExactComplex, bound: &ExactNum, p: usize, cc: &mut Consts) -> bool {
32    let a = z.arg(p, RoundingMode::None, cc).abs();
33    matches!(a.cmp(bound), Some(c) if c <= 0)
34}
35
36fn omega_pair(p: usize) -> (ExactComplex, ExactComplex) {
37    let half = ExactNum::from_u8(1, p).div(&ExactNum::from_u8(2, p), p, RoundingMode::None);
38    let s3h = ExactNum::from_u8(3, p)
39        .sqrt(p, RoundingMode::None)
40        .mul(&half, p, RoundingMode::None);
41    let re = half.neg();
42    (
43        ExactComplex::new(re.clone(), s3h.clone()),
44        ExactComplex::new(re, s3h.neg()),
45    )
46}
47
48fn exp_i_pi_6(p: usize, cc: &mut Consts) -> (ExactComplex, ExactComplex) {
49    let a = cc
50        .pi(p, RoundingMode::None)
51        .div(&ExactNum::from_u8(6, p), p, RoundingMode::None);
52    let (s, c) = a.sin_cos(p, RoundingMode::None, cc);
53    (
54        ExactComplex::new(c.clone(), s.clone()),
55        ExactComplex::new(c, s.neg()),
56    )
57}
58
59fn two_pi_over_three(p: usize, cc: &mut Consts) -> ExactNum {
60    two_c(p)
61        .re()
62        .mul(&cc.pi(p, RoundingMode::None), p, RoundingMode::None)
63        .div(&ExactNum::from_u8(3, p), p, RoundingMode::None)
64}
65
66fn pi_over_three(p: usize, cc: &mut Consts) -> ExactNum {
67    cc.pi(p, RoundingMode::None)
68        .div(&ExactNum::from_u8(3, p), p, RoundingMode::None)
69}
70
71fn airy_cs_real(p: usize, cc: &mut Consts) -> (ExactNum, ExactNum, ExactNum) {
72    let one = ExactNum::from_u8(1, p);
73    let two = ExactNum::from_u8(2, p);
74    let three = ExactNum::from_u8(3, p);
75    let third = one.div(&three, p, RoundingMode::None);
76    let two_third = two.div(&three, p, RoundingMode::None);
77    let g23 = two_third.gamma(p, RoundingMode::None, cc);
78    let g13 = third.gamma(p, RoundingMode::None, cc);
79    let c1 =
80        three
81            .pow(&two_third.neg(), p, RoundingMode::None, cc)
82            .div(&g23, p, RoundingMode::None);
83    let c2 = three
84        .pow(&third.neg(), p, RoundingMode::None, cc)
85        .div(&g13, p, RoundingMode::None);
86    let sqrt3 = three.sqrt(p, RoundingMode::None);
87    (c1, c2, sqrt3)
88}
89
90impl ExactComplex {
91    /// Airy \(\mathrm{Ai}(z)\). Entire. NaN in → NaN out.
92    ///
93    /// # Precision
94    ///
95    /// - Algorithm: series for `|z| < AIRY_SERIES_THRESHOLD` (`8`); asymptotic otherwise.
96    /// - Bound: Ziv on each part (`MAX_PREC_RETRY`).
97    /// - MPFR oracle: no.
98    pub fn ai(&self, p: usize, rm: RoundingMode, cc: &mut Consts) -> Self {
99        if self.is_nan() {
100            return ExactComplex::new(self.re().clone(), self.im().clone());
101        }
102        let dest = round_p(p);
103        ziv_complex(dest, rm, |pw| self.ai_at(pw, dest, cc, true))
104    }
105
106    /// Airy \(\mathrm{Bi}(z)\). Entire. NaN in → NaN out.
107    ///
108    /// # Precision
109    ///
110    /// - Algorithm: same `AIRY_SERIES_THRESHOLD = 8` as [`Self::ai`].
111    /// - Bound: Ziv on each part (`MAX_PREC_RETRY`).
112    /// - MPFR oracle: no.
113    pub fn bi(&self, p: usize, rm: RoundingMode, cc: &mut Consts) -> Self {
114        if self.is_nan() {
115            return ExactComplex::new(self.re().clone(), self.im().clone());
116        }
117        let dest = round_p(p);
118        ziv_complex(dest, rm, |pw| self.bi_at(pw, dest, cc))
119    }
120
121    fn ai_at(&self, work_p: usize, dest_p: usize, cc: &mut Consts, connect: bool) -> Self {
122        if self.im().is_zero() {
123            return ExactComplex::from_real(self.re().ai(work_p, RoundingMode::None, cc), work_p);
124        }
125        if abs_below(self, AIRY_SERIES_THRESHOLD, dest_p) {
126            return self.airy_series_ai(work_p, cc);
127        }
128        let sector = two_pi_over_three(work_p, cc);
129        if arg_abs_le(self, &sector, work_p, cc) {
130            return self.airy_asymp_ai(work_p, cc);
131        }
132        if connect {
133            let (w, w2) = omega_pair(work_p);
134            let zw = self.mul(&w, work_p, RoundingMode::None);
135            let zw2 = self.mul(&w2, work_p, RoundingMode::None);
136            let t1 = w.mul(
137                &zw.ai_at(work_p, dest_p, cc, false),
138                work_p,
139                RoundingMode::None,
140            );
141            let t2 = w2.mul(
142                &zw2.ai_at(work_p, dest_p, cc, false),
143                work_p,
144                RoundingMode::None,
145            );
146            return neg_c(&t1.add(&t2, work_p, RoundingMode::None));
147        }
148        self.airy_asymp_ai(work_p, cc)
149    }
150
151    fn bi_at(&self, work_p: usize, dest_p: usize, cc: &mut Consts) -> Self {
152        if self.im().is_zero() {
153            return ExactComplex::from_real(self.re().bi(work_p, RoundingMode::None, cc), work_p);
154        }
155        if abs_below(self, AIRY_SERIES_THRESHOLD, dest_p) {
156            return self.airy_series_bi(work_p, cc);
157        }
158        let sector = pi_over_three(work_p, cc);
159        if arg_abs_le(self, &sector, work_p, cc) {
160            return self.airy_asymp_bi(work_p, cc);
161        }
162        let (w, w2) = omega_pair(work_p);
163        let (ep, em) = exp_i_pi_6(work_p, cc);
164        let a1 = self
165            .mul(&w2, work_p, RoundingMode::None)
166            .ai_at(work_p, dest_p, cc, true);
167        let a2 = self
168            .mul(&w, work_p, RoundingMode::None)
169            .ai_at(work_p, dest_p, cc, true);
170        ep.mul(&a1, work_p, RoundingMode::None).add(
171            &em.mul(&a2, work_p, RoundingMode::None),
172            work_p,
173            RoundingMode::None,
174        )
175    }
176
177    fn airy_series_pair(&self, p: usize) -> (ExactComplex, ExactComplex) {
178        let x3 = self
179            .mul(self, p, RoundingMode::None)
180            .mul(self, p, RoundingMode::None);
181        let one = ExactComplex::one(p);
182        let mut tf = one.clone();
183        let mut f = one;
184        let mut tg = self.clone();
185        let mut g = self.clone();
186        let cap = series_term_cap(p);
187        for k in 1..=cap {
188            let k3 = ExactComplex::from_real(ExactNum::from_u32((3 * k) as u32, p), p);
189            let k3m1 = ExactComplex::from_real(ExactNum::from_u32((3 * k - 1) as u32, p), p);
190            let k3p1 = ExactComplex::from_real(ExactNum::from_u32((3 * k + 1) as u32, p), p);
191            tf = tf.mul(&x3, p, RoundingMode::None).div(
192                &k3.mul(&k3m1, p, RoundingMode::None),
193                p,
194                RoundingMode::None,
195            );
196            f = f.add(&tf, p, RoundingMode::None);
197            tg = tg.mul(&x3, p, RoundingMode::None).div(
198                &k3p1.mul(&k3, p, RoundingMode::None),
199                p,
200                RoundingMode::None,
201            );
202            g = g.add(&tg, p, RoundingMode::None);
203            if term_negligible(&tf, p) && term_negligible(&tg, p) {
204                break;
205            }
206        }
207        (f, g)
208    }
209
210    fn airy_series_ai(&self, p: usize, cc: &mut Consts) -> Self {
211        let (c1, c2, _) = airy_cs_real(p, cc);
212        let (f, g) = self.airy_series_pair(p);
213        let c1c = ExactComplex::from_real(c1, p);
214        let c2c = ExactComplex::from_real(c2, p);
215        c1c.mul(&f, p, RoundingMode::None).sub(
216            &c2c.mul(&g, p, RoundingMode::None),
217            p,
218            RoundingMode::None,
219        )
220    }
221
222    fn airy_series_bi(&self, p: usize, cc: &mut Consts) -> Self {
223        let (c1, c2, sqrt3) = airy_cs_real(p, cc);
224        let (f, g) = self.airy_series_pair(p);
225        let c1c = ExactComplex::from_real(c1, p);
226        let c2c = ExactComplex::from_real(c2, p);
227        let s3 = ExactComplex::from_real(sqrt3, p);
228        s3.mul(
229            &c1c.mul(&f, p, RoundingMode::None).add(
230                &c2c.mul(&g, p, RoundingMode::None),
231                p,
232                RoundingMode::None,
233            ),
234            p,
235            RoundingMode::None,
236        )
237    }
238
239    fn airy_xi_z(&self, p: usize, cc: &mut Consts) -> (ExactComplex, ExactComplex) {
240        let two_thirds = two_c(p).div(
241            &ExactComplex::from_real(ExactNum::from_u8(3, p), p),
242            p,
243            RoundingMode::None,
244        );
245        let sz = self.sqrt(p, RoundingMode::None, cc);
246        let xi = two_thirds.mul(&self.mul(&sz, p, RoundingMode::None), p, RoundingMode::None);
247        let z14 = sz.sqrt(p, RoundingMode::None, cc);
248        (xi, z14)
249    }
250
251    fn airy_u_sum(&self, xi: &ExactComplex, p: usize, alt: bool) -> ExactComplex {
252        let one = ExactComplex::one(p);
253        let mut u = one.clone();
254        let mut sum = one;
255        let mut prev_abs = ExactNum::from_u8(1, p);
256        let mut xi_pow = xi.clone();
257        let cap = series_term_cap(p).min(p.saturating_add(8));
258        for k in 1..=cap {
259            let num = ExactNum::from_u32((6 * k - 5) as u32, p).mul(
260                &ExactNum::from_u32((6 * k - 1) as u32, p),
261                p,
262                RoundingMode::None,
263            );
264            let den = ExactNum::from_u32(72, p).mul(
265                &ExactNum::from_u32(k as u32, p),
266                p,
267                RoundingMode::None,
268            );
269            let ratio = ExactComplex::from_real(num.div(&den, p, RoundingMode::None), p);
270            u = u.mul(&ratio, p, RoundingMode::None);
271            let mut t = u.div(&xi_pow, p, RoundingMode::None);
272            if alt && k % 2 == 1 {
273                t = neg_c(&t);
274            }
275            let ta = t.abs(p, RoundingMode::None);
276            if matches!(ta.cmp(&prev_abs), Some(c) if c > 0) {
277                break;
278            }
279            sum = sum.add(&t, p, RoundingMode::None);
280            if term_negligible(&t, p) {
281                break;
282            }
283            prev_abs = ta;
284            xi_pow = xi_pow.mul(xi, p, RoundingMode::None);
285        }
286        sum
287    }
288
289    fn airy_asymp_ai(&self, p: usize, cc: &mut Consts) -> Self {
290        let (xi, z14) = self.airy_xi_z(p, cc);
291        let half = half_c(p);
292        let pi = pi_c(p, cc);
293        let pi_m12 =
294            ExactComplex::one(p).div(&pi.sqrt(p, RoundingMode::None, cc), p, RoundingMode::None);
295        let z_m14 = ExactComplex::one(p).div(&z14, p, RoundingMode::None);
296        let exp_m = neg_c(&xi).exp(p, RoundingMode::None, cc);
297        let su = self.airy_u_sum(&xi, p, true);
298        half.mul(&pi_m12, p, RoundingMode::None)
299            .mul(&z_m14, p, RoundingMode::None)
300            .mul(&exp_m, p, RoundingMode::None)
301            .mul(&su, p, RoundingMode::None)
302    }
303
304    fn airy_asymp_bi(&self, p: usize, cc: &mut Consts) -> Self {
305        let (xi, z14) = self.airy_xi_z(p, cc);
306        let pi = pi_c(p, cc);
307        let pi_m12 =
308            ExactComplex::one(p).div(&pi.sqrt(p, RoundingMode::None, cc), p, RoundingMode::None);
309        let z_m14 = ExactComplex::one(p).div(&z14, p, RoundingMode::None);
310        let exp_p = xi.exp(p, RoundingMode::None, cc);
311        let su = self.airy_u_sum(&xi, p, false);
312        pi_m12
313            .mul(&z_m14, p, RoundingMode::None)
314            .mul(&exp_p, p, RoundingMode::None)
315            .mul(&su, p, RoundingMode::None)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    fn near_bits(a: &ExactNum, b: &ExactNum, p: usize, slack: i32) -> bool {
324        let d = a.sub(b, p, RoundingMode::None).abs();
325        d.is_zero() || d.exponent().is_some_and(|e| e < -((p as i32) / slack))
326    }
327
328    fn tiny(x: &ExactNum, p: usize) -> bool {
329        x.is_zero() || x.exponent().is_some_and(|e| e < -((p as i32) / 4))
330    }
331
332    #[test]
333    fn test_complex_airy_golds() {
334        let p = 256;
335        let rm = RoundingMode::ToEven;
336        let mut cc = Consts::new().unwrap();
337
338        let z0 = ExactComplex::zero(p);
339        let a0 = z0.ai(p, rm, &mut cc);
340        let r0 = ExactNum::new(p).ai(p, rm, &mut cc);
341        assert!(near_bits(a0.re(), &r0, p, 4));
342        assert!(tiny(a0.im(), p));
343        let b0 = z0.bi(p, rm, &mut cc);
344        let rb0 = ExactNum::new(p).bi(p, rm, &mut cc);
345        assert!(near_bits(b0.re(), &rb0, p, 4));
346        assert!(tiny(b0.im(), p));
347
348        let one = ExactComplex::one(p);
349        let a1 = one.ai(p, rm, &mut cc);
350        let r1 = ExactNum::from_u8(1, p).ai(p, rm, &mut cc);
351        assert!(near_bits(a1.re(), &r1, p, 4));
352        assert!(tiny(a1.im(), p));
353
354        let i = ExactComplex::i(p);
355        let zi = i.ai(p, rm, &mut cc);
356        assert!(!zi.is_nan());
357        let (w, w2) = omega_pair(p);
358        // Ai(z) + ω Ai(ωz) + ω² Ai(ω²z) = 0 at z = i (series region).
359        let t0 = zi;
360        let t1 = w.mul(&i.mul(&w, p, rm).ai(p, rm, &mut cc), p, rm);
361        let t2 = w2.mul(&i.mul(&w2, p, rm).ai(p, rm, &mut cc), p, rm);
362        let sum = t0.add(&t1, p, rm).add(&t2, p, rm);
363        assert!(
364            tiny(sum.re(), p) && tiny(sum.im(), p),
365            "connection identity"
366        );
367
368        let z = ExactComplex::new(ExactNum::from_u8(1, p), ExactNum::from_u8(1, p));
369        let az = z.ai(p, rm, &mut cc);
370        let bz = z.bi(p, rm, &mut cc);
371        assert!(!az.is_nan() && !bz.is_nan());
372
373        let nan = ExactComplex::new(crate::NAN.clone(), ExactNum::new(p));
374        assert!(nan.ai(p, rm, &mut cc).is_nan());
375        assert!(nan.bi(p, rm, &mut cc).is_nan());
376    }
377}