treams_core/special/
bessel.rs

1//! Bessel functions and radial jets with analytic argument derivatives.
2//!
3//! Cylindrical and spherical Bessel functions of every kind ([`bessel`]), regular and
4//! singular radial jets ([`spherical_radial`], [`cylindrical_radial`]), and their
5//! broadcasting residual. They use AMOS (the `complex_bessel` crate), small-argument
6//! power series, and a qualified positive-real outgoing Hankel recurrence.
7//!
8//! Upstream: `treams.special.jv`, `yv`, `hankel1`, `hankel2`, `spherical_jn` and
9//! `spherical_yn` (scipy's functions), and `jv_d`, `yv_d`, `hankel1_d`, `hankel2_d`,
10//! `spherical_hankel1`, `spherical_hankel2` and their `_d` derivatives (`_bessel.pyx`).
11
12use std::f64::consts::PI;
13
14use super::{MAX_ORDER, SERIES_RADIUS, log_factorial};
15use crate::{
16    Complex, Error, Result,
17    numerics::{
18        Jet,
19        broadcast::{self, element},
20        finite,
21        parallel::Parallel,
22        ratio,
23    },
24};
25
26/// Elementwise Bessel evaluations and pullbacks run in parallel from this many elements.
27const PARALLEL: Parallel = Parallel::AtLeast(64);
28
29/// Cylindrical Bessel solution, also used at half order for spherical functions.
30///
31/// Upstream: `treams.special.jv` (`J`), `yv` (`Y`), `hankel1` (`H1`) and `hankel2`
32/// (`H2`).
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Bessel {
35    /// Regular first-kind solution.
36    J,
37    /// Second-kind solution.
38    Y,
39    /// Outgoing Hankel solution.
40    H1,
41    /// Incoming Hankel solution.
42    H2,
43}
44
45impl From<Radial> for Bessel {
46    /// Regular waves are J, singular (outgoing) waves H1.
47    fn from(radial: Radial) -> Self {
48        match radial {
49            Radial::Regular => Self::J,
50            Radial::Singular => Self::H1,
51        }
52    }
53}
54
55fn amos_error(error: complex_bessel::Error) -> Error {
56    Error::SpecialFunction(error.to_string())
57}
58
59/// `sqrt(pi / 2z)`, which turns cylindrical functions of order `l + 1/2` into
60/// spherical functions of order `l`.
61///
62/// On the negative real axis AMOS takes the limit from above for either sign of a
63/// zero imaginary part (it tests `Im z < 0`), where `pi / 2z` approaches the axis from
64/// below. Real-by-complex division gives `pi / 2z` a `+0` imaginary part there, so the
65/// prefactor takes that side explicitly; the spherical functions are single-valued,
66/// and only the two factors' agreement matters.
67fn spherical_prefactor(z: Complex) -> Complex {
68    let mut w = PI / (2.0 * z);
69    if z.im == 0.0 && z.re < 0.0 {
70        w.im = -0.0;
71    }
72    crate::numerics::complex_sqrt(w)
73}
74
75/// One AMOS evaluation of a cylindrical function, or of a spherical one at order + 1/2.
76#[inline]
77fn bessel_raw(order: f64, z: Complex, kind: Bessel, spherical: bool) -> Result<Complex> {
78    let order = if spherical { order + 0.5 } else { order };
79    let value = match kind {
80        Bessel::J => complex_bessel::besselj(order, z),
81        Bessel::Y => complex_bessel::bessely(order, z),
82        Bessel::H1 => complex_bessel::hankel1(order, z),
83        Bessel::H2 => complex_bessel::hankel2(order, z),
84    }
85    .map_err(amos_error)?;
86    Ok(if spherical {
87        spherical_prefactor(z) * value
88    } else {
89        value
90    })
91}
92
93/// Cylindrical functions of orders `start, start + 1, ...` from one AMOS recurrence.
94fn sequence(kind: Bessel, start: f64, z: Complex, length: usize) -> Result<Vec<Complex>> {
95    let unscaled = complex_bessel::Scaling::Unscaled;
96    match kind {
97        Bessel::J => complex_bessel::besselj_seq(start, z, length, unscaled),
98        Bessel::Y => complex_bessel::bessely_seq(start, z, length, unscaled),
99        Bessel::H1 => complex_bessel::hankel1_seq(start, z, length, unscaled),
100        Bessel::H2 => complex_bessel::hankel2_seq(start, z, length, unscaled),
101    }
102    .map(|sequence| sequence.values)
103    .map_err(amos_error)
104}
105
106/// The two values of a two-term sequence.
107fn pair(values: Vec<Complex>) -> Result<[Complex; 2]> {
108    <[Complex; 2]>::try_from(values)
109        .map_err(|_| Error::SpecialFunction("invalid Bessel sequence length".into()))
110}
111
112/// Outgoing cylindrical functions `[H_(m-1)(z), H_m(z)]` from one AMOS sequence, with
113/// `H_-1 = -H_1`: the derivative `u^m H_(m-1)(u)` of `u^m H_m(u)` without the
114/// cancelling product rule.
115pub(crate) fn hankel_below(order: u32, z: Complex) -> Result<[Complex; 2]> {
116    if order == 0 {
117        let [value, upper] = pair(sequence(Bessel::H1, 0.0, z, 2)?)?;
118        return Ok([-upper, value]);
119    }
120    pair(sequence(Bessel::H1, f64::from(order - 1), z, 2)?)
121}
122
123/// Spherical functions of orders `order` and `order + 1`. Hankel functions share one
124/// AMOS sequence, which agrees with single evaluations to 2e-14 relative. J and Y
125/// sequences differ from single evaluations by up to 1e-12, so these stay single.
126fn spherical_pair(kind: Bessel, order: f64, z: Complex) -> Result<[Complex; 2]> {
127    let values = if matches!(kind, Bessel::H1 | Bessel::H2) {
128        pair(sequence(kind, order + 0.5, z, 2)?)?
129    } else {
130        let single = |order: f64| bessel_raw(order + 0.5, z, kind, false);
131        [single(order)?, single(order + 1.0)?]
132    };
133    let prefactor = spherical_prefactor(z);
134    Ok(values.map(|value| prefactor * value))
135}
136
137/// `jet`, or an overflow error where one of its parts is not finite. Near zero the
138/// prefactor `sqrt(pi / 2z)` and the derivatives' powers of `1/z` overflow where AMOS
139/// does not, as for `h_0` below `|z| = 1e-103`.
140fn checked(jet: RadialJet) -> Result<RadialJet> {
141    if finite(jet.value) && finite(jet.first) && finite(jet.second) {
142        Ok(jet)
143    } else {
144        Err(Error::SpecialFunction(
145            "radial function or derivative overflow".into(),
146        ))
147    }
148}
149
150/// Derivatives of a spherical function from the next order, `f' = (l/z) f - f_(l+1)`,
151/// and the spherical Bessel equation.
152fn jet_from_next_order(order: f64, z: Complex, [value, next]: [Complex; 2]) -> RadialJet {
153    let first = order * ratio(value, z) - next;
154    let second = -2.0 * ratio(first, z) + order * (order + 1.0) * ratio(ratio(value, z), z) - value;
155    RadialJet {
156        value,
157        first,
158        second,
159    }
160}
161
162/// Bessel value or one of its first two complex-argument derivatives.
163///
164/// `kind` selects J, Y, H1 or H2. `spherical` selects the spherical function of order
165/// `order` (`j`, `y`, `h1` or `h2`): the cylindrical function of order `order + 1/2`
166/// times `sqrt(pi / (2z))`. `derivative` is 0, 1 or 2, always with respect to `z`.
167/// Spherical regular functions use analytic origin limits.
168///
169/// Upstream: `treams.special.jv`, `yv`, `hankel1`, `hankel2` and with `spherical`
170/// `spherical_jn`, `spherical_yn`, `spherical_hankel1`, `spherical_hankel2`; their `_d`
171/// variants for `derivative = 1`. treams has no second derivative.
172#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // Integral order in 0..=MAX_ORDER checked before conversion.
173#[inline]
174pub fn bessel(
175    order: f64,
176    z: Complex,
177    kind: Bessel,
178    spherical: bool,
179    derivative: u8,
180) -> Result<Complex> {
181    if !order.is_finite() || !finite(z) || derivative > 2 {
182        return Err(Error::InvalidInput(
183            "finite order/argument and derivative 0, 1 or 2 required".into(),
184        ));
185    }
186    // AMOS-based kernels lose accuracy for subnormal orders (including NaNs in
187    // SciPy). Their zero-order limit is identical at double precision: even at
188    // extreme finite z, order * log(z) is far below a representable correction.
189    let order = if order.is_subnormal() { 0.0 } else { order };
190    let jet = |jet: RadialJet| match derivative {
191        0 => jet.value,
192        1 => jet.first,
193        _ => jet.second,
194    };
195    let evaluate = |v| bessel_raw(v, z, kind, spherical);
196    let value = if spherical
197        && kind == Bessel::J
198        && z.norm() < SERIES_RADIUS
199        && (0.0..=f64::from(MAX_ORDER)).contains(&order)
200        && order.fract() == 0.0
201    {
202        jet(spherical_radial(order as u32, z, Radial::Regular)?)
203    } else if derivative == 0 {
204        evaluate(order)?
205    } else if spherical {
206        jet(jet_from_next_order(
207            order,
208            z,
209            spherical_pair(kind, order, z)?,
210        ))
211    } else if matches!(kind, Bessel::H1 | Bessel::H2) && (order != 0.0 || derivative == 2) {
212        let length = usize::from(2 * derivative + 1);
213        match sequence(kind, order - f64::from(derivative), z, length)?.as_slice() {
214            [left, _, right] => 0.5 * (left - right),
215            [left, _, middle, _, right] => 0.25 * (left - 2.0 * middle + right),
216            _ => {
217                return Err(Error::SpecialFunction(
218                    "invalid Hankel sequence length".into(),
219                ));
220            }
221        }
222    } else if derivative == 1 {
223        if order == 0.0 {
224            -evaluate(1.0)?
225        } else {
226            0.5 * (evaluate(order - 1.0)? - evaluate(order + 1.0)?)
227        }
228    } else {
229        0.25 * (evaluate(order - 2.0)? - 2.0 * evaluate(order)? + evaluate(order + 2.0)?)
230    };
231    if !finite(value) {
232        return Err(Error::SpecialFunction("non-finite Bessel result".into()));
233    }
234    Ok(value)
235}
236
237/// What [`bessel_array`] saves for its pullback: the orders, the arguments and the
238/// function. Only the arguments get gradients.
239#[derive(Debug)]
240pub struct BesselResidual {
241    orders: Vec<f64>,
242    arguments: Vec<Complex>,
243    kind: Bessel,
244    spherical: bool,
245    derivative: u8,
246    size: usize,
247}
248
249/// Evaluate elementwise Bessel functions, broadcasting either scalar input.
250///
251/// `kind` and `spherical` select the function as in [`bessel`]. `derivative` is 0 or 1,
252/// one below the cap of [`bessel`], because the pullback evaluates derivative + 1.
253///
254/// Upstream: the functions of [`bessel`] over arrays.
255pub fn bessel_array(
256    orders: Vec<f64>,
257    arguments: Vec<Complex>,
258    kind: Bessel,
259    spherical: bool,
260    derivative: u8,
261) -> Result<(Vec<Complex>, BesselResidual)> {
262    let residual = BesselResidual::new(orders, arguments, kind, spherical, derivative)?;
263    let value = broadcast::map(residual.size, PARALLEL, |i| {
264        residual.evaluate(i, derivative)
265    })?;
266    Ok((value, residual))
267}
268
269impl BesselResidual {
270    /// Keep the broadcast inputs needed by derivatives without evaluating values.
271    pub fn new(
272        orders: Vec<f64>,
273        arguments: Vec<Complex>,
274        kind: Bessel,
275        spherical: bool,
276        derivative: u8,
277    ) -> Result<Self> {
278        let message =
279            "Bessel arrays must have equal lengths or scalar inputs, and derivative 0 or 1";
280        if derivative > 1 {
281            return Err(Error::InvalidInput(message.into()));
282        }
283        let size = broadcast::size(&[orders.len(), arguments.len()], message)?;
284        Ok(Self {
285            orders,
286            arguments,
287            kind,
288            spherical,
289            derivative,
290            size,
291        })
292    }
293
294    fn evaluate(&self, i: usize, derivative: u8) -> Result<Complex> {
295        bessel(
296            element(&self.orders, i),
297            element(&self.arguments, i),
298            self.kind,
299            self.spherical,
300            derivative,
301        )
302    }
303
304    /// Directional derivative of every value with respect to its complex argument.
305    pub fn pushforward(&self, tangents: [&[Complex]; 1]) -> Result<Vec<Complex>> {
306        broadcast::pushforward(
307            tangents,
308            self.size,
309            "Bessel tangent must be finite and broadcast to output",
310            PARALLEL,
311            |i, [tangent]| {
312                if tangent == Complex::default() {
313                    Ok(tangent)
314                } else {
315                    Ok(tangent * self.evaluate(i, self.derivative + 1)?)
316                }
317            },
318        )
319    }
320
321    /// The gradient with respect to the arguments, given the gradient `cotangent` with
322    /// respect to the values: `cotangent` times the conjugate of the next derivative. An
323    /// argument given as one value for all elements receives the sum of its element
324    /// gradients; the orders have no gradient.
325    pub fn pullback(&self, cotangent: &[Complex]) -> Result<Vec<Complex>> {
326        let [gradient] = broadcast::pullback(
327            cotangent,
328            self.size,
329            "Bessel cotangent must be finite and match output",
330            [self.arguments.len()],
331            PARALLEL,
332            |i, &g| {
333                if g == Complex::default() {
334                    Ok([g])
335                } else {
336                    Ok([g * self.evaluate(i, self.derivative + 1)?.conj()])
337                }
338            },
339        )?;
340        Ok(gradient)
341    }
342}
343
344/// Radial kind of multipole waves: regular or singular.
345///
346/// Upstream: the treams `modetype` argument, `'regular'` or `'singular'`.
347#[derive(Clone, Copy, Debug, PartialEq, Eq)]
348pub enum Radial {
349    /// Bessel function of the first kind, regular at the origin (treams modetype
350    /// "regular").
351    Regular,
352    /// Outgoing Hankel function of the first kind (treams modetype "singular"); also used
353    /// for cylindrical waves.
354    #[doc(alias = "outgoing")]
355    Singular,
356}
357
358/// A radial function and its first two argument derivatives: a second-order jet in one
359/// complex variable, unlike the crate's first-order jets `numerics::Jet` in several.
360#[derive(Clone, Copy, Debug)]
361pub struct RadialJet {
362    /// Function value.
363    pub value: Complex,
364    /// First derivative with respect to the complex argument.
365    pub first: Complex,
366    /// Second derivative with respect to the complex argument.
367    pub second: Complex,
368}
369
370/// Terms of the small-argument power series of [`spherical_radial`] and
371/// [`cylindrical_radial`]. Below `|z| = SERIES_RADIUS` (0.5) each term is at most
372/// `1/(16 q^2)` of its predecessor, so the last one is below 1e-40 of the first.
373const SERIES_TERMS: u32 = 16;
374
375/// The even power series `sum_q t_q` with `t_0 = leading z^power` and
376/// `t_q = factor(q) z^2 t_(q-1)`, differentiated term by term: every term reuses its
377/// predecessor's powers.
378fn even_series(power: u32, leading: f64, z: Complex, factor: impl Fn(f64) -> f64) -> RadialJet {
379    let mut term = RadialJet {
380        value: leading * z.powu(power),
381        first: if power > 0 {
382            leading * f64::from(power) * z.powu(power - 1)
383        } else {
384            Complex::default()
385        },
386        second: if power > 1 {
387            leading * f64::from(power * (power - 1)) * z.powu(power - 2)
388        } else {
389            Complex::default()
390        },
391    };
392    let mut sum = term;
393    let square = z * z;
394    for q in 1..SERIES_TERMS {
395        let factor = factor(f64::from(q));
396        term = RadialJet {
397            value: factor * square * term.value,
398            first: factor * (square * term.first + 2.0 * z * term.value),
399            second: factor * (square * term.second + 4.0 * z * term.first + 2.0 * term.value),
400        };
401        sum.value += term.value;
402        sum.first += term.first;
403        sum.second += term.second;
404    }
405    sum
406}
407
408/// Evaluate a spherical radial function and its first two derivatives: `j_l` for
409/// regular and `h1_l` for singular waves.
410///
411/// The regular solution uses its convergent power series near zero; away from
412/// zero, complex Bessel evaluation and the spherical differential equation apply.
413/// Singular (outgoing) waves fail at zero; near it, where the value or a derivative
414/// overflows, they fail with an overflow error.
415///
416/// Upstream: `treams.special.spherical_jn` and `spherical_hankel1` with their `_d`
417/// derivatives.
418pub fn spherical_radial(l: u32, z: Complex, radial: Radial) -> Result<RadialJet> {
419    if l > MAX_ORDER.unsigned_abs() || !finite(z) {
420        return Err(Error::InvalidInput(
421            // 256 is MAX_ORDER.
422            "require finite argument and 0 <= l <= 256".into(),
423        ));
424    }
425    if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
426        // j_l(z) = sum_q (-z^2/2)^q z^l / (q! (2l + 2q + 1)!!).
427        let double_factorial: f64 = (0..=l).map(|i| f64::from(2 * i + 1)).product();
428        let degree = f64::from(l);
429        return Ok(even_series(l, double_factorial.recip(), z, |q| {
430            -1.0 / (2.0 * q * (2.0 * (degree + q) + 1.0))
431        }));
432    }
433    if z == Complex::default() {
434        return Err(Error::InvalidInput(
435            "outgoing spherical waves are singular at zero".into(),
436        ));
437    }
438    let order = f64::from(l);
439    checked(jet_from_next_order(
440        order,
441        z,
442        spherical_pair(radial.into(), order, z)?,
443    ))
444}
445
446/// [`spherical_radial`] jets of every degree `0..=order` at one argument. Regular jets
447/// share power-series terms or single AMOS J evaluations. Singular jets share one
448/// [`spherical_hankels`] sequence, using its positive-real recurrence where qualified
449/// and AMOS elsewhere. Against 30-digit values at `|Im z| <= 5`, the AMOS sequence
450/// stays within 2e-13 relative up to order 80 (two-term sequences: 8e-14); below
451/// `Im z = 0` it loses up to a digit to two-term sequences, still within 1e-13 of each
452/// jet's size up to order 40.
453pub(crate) fn spherical_radial_sequence(
454    order: u32,
455    z: Complex,
456    radial: Radial,
457) -> Result<Vec<RadialJet>> {
458    if order > MAX_ORDER.unsigned_abs() || !finite(z) {
459        return Err(Error::InvalidInput(
460            // 256 is MAX_ORDER.
461            "require finite argument and 0 <= l <= 256".into(),
462        ));
463    }
464    if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
465        return (0..=order)
466            .map(|l| spherical_radial(l, z, radial))
467            .collect();
468    }
469    if z == Complex::default() {
470        return Err(Error::InvalidInput(
471            "outgoing spherical waves are singular at zero".into(),
472        ));
473    }
474    let values = match radial {
475        Radial::Regular => {
476            let prefactor = spherical_prefactor(z);
477            (0..=order + 1)
478                .map(|l| Ok(prefactor * bessel_raw(f64::from(l) + 0.5, z, Bessel::J, false)?))
479                .collect::<Result<Vec<_>>>()?
480        }
481        Radial::Singular => spherical_hankels(0, order + 1, z)?,
482    };
483    (0..=order)
484        .zip(values.iter().zip(values.iter().skip(1)))
485        .map(|(l, (&value, &next))| checked(jet_from_next_order(f64::from(l), z, [value, next])))
486        .collect()
487}
488
489/// Spherical Hankel functions `h_p(z)` of the first kind for `p = first..=last` at a
490/// nonzero argument. Real positive arguments use the elementary outgoing seed and
491/// upward recurrence (DLMF 10.49.6, 10.51.1). Unlike regular J, H contains the dominant
492/// irregular solution, so this direction is stable below the turning point too.
493/// The bounded domain keeps every intermediate finite and agrees with AMOS to the
494/// existing 1e-13 sequence tolerance; other arguments retain one AMOS sequence.
495pub(crate) fn spherical_hankels(first: u32, last: u32, z: Complex) -> Result<Vec<Complex>> {
496    let count = usize::try_from(last.saturating_sub(first) + 1)
497        .map_err(|_| Error::InvalidInput("invalid radial order".into()))?;
498    if first <= last && last <= 64 && z.im == 0.0 && (0.5..=1e4).contains(&z.re) {
499        let inverse = z.re.recip();
500        let (sin, cos) = z.re.sin_cos();
501        // h_-1 = exp(ix)/x and h_0 = -i exp(ix)/x.
502        let mut previous = Complex::new(cos * inverse, sin * inverse);
503        let mut current = Complex::new(sin * inverse, -cos * inverse);
504        let mut values = Vec::with_capacity(count);
505        for degree in 0..=last {
506            if degree >= first {
507                values.push(current);
508            }
509            if degree < last {
510                let next = (f64::from(2 * degree + 1) * inverse) * current - previous;
511                previous = current;
512                current = next;
513            }
514        }
515        return Ok(values);
516    }
517    let prefactor = spherical_prefactor(z);
518    Ok(sequence(Bessel::H1, f64::from(first) + 0.5, z, count)?
519        .into_iter()
520        .map(|h| prefactor * h)
521        .collect())
522}
523
524/// Integer-order cylindrical J (regular) or outgoing H1 (singular), with its first two
525/// derivatives.
526///
527/// Upstream: `treams.special.jv` and `hankel1` with their `_d` derivatives.
528pub fn cylindrical_radial(order: i32, z: Complex, radial: Radial) -> Result<RadialJet> {
529    if order.unsigned_abs() > MAX_ORDER.unsigned_abs() || !finite(z) {
530        return Err(Error::InvalidInput(
531            // 256 is MAX_ORDER.
532            "require finite argument and |m| <= 256".into(),
533        ));
534    }
535    let m = order.unsigned_abs();
536    let sign = if order < 0 && m % 2 == 1 { -1.0 } else { 1.0 };
537    if z == Complex::default() {
538        if radial == Radial::Singular {
539            return Err(Error::InvalidInput(
540                "outgoing cylindrical waves are singular at zero".into(),
541            ));
542        }
543        return Ok(RadialJet {
544            value: Complex::new(if m == 0 { 1.0 } else { 0.0 }, 0.0),
545            first: Complex::new(if m == 1 { 0.5 * sign } else { 0.0 }, 0.0),
546            second: Complex::new(
547                if m == 0 {
548                    -0.5
549                } else if m == 2 {
550                    0.25
551                } else {
552                    0.0
553                },
554                0.0,
555            ),
556        });
557    }
558    let degree = f64::from(m);
559    if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
560        // J_m(z) = sum_q (-z^2/4)^q (z/2)^m / (q! (m + q)!).
561        let leading = sign * (-log_factorial(order.abs())).exp() / 2.0_f64.powf(degree);
562        return Ok(even_series(m, leading, z, |q| {
563            -1.0 / (4.0 * q * (degree + q))
564        }));
565    }
566    let [value, upper] = pair(sequence(radial.into(), degree, z, 2)?)?;
567    let value = sign * value;
568    let first = degree * value / z - sign * upper;
569    let second = (degree.powi(2) / z.powu(2) - 1.0) * value - first / z;
570    Ok(RadialJet {
571        value,
572        first,
573        second,
574    })
575}
576
577/// A spherical or cylindrical radial function of integer `order` at a jet argument:
578/// without derivatives a single value-only Bessel evaluation, otherwise the value
579/// and first derivative from [`spherical_radial`] or [`cylindrical_radial`].
580pub(crate) fn radial_jet<const N: usize>(
581    order: i32,
582    z: Jet<N>,
583    radial: Radial,
584    spherical: bool,
585) -> Result<Jet<N>> {
586    if N == 0 {
587        let value = bessel(f64::from(order), z.value, radial.into(), spherical, 0)?;
588        return Ok(Jet::constant(value));
589    }
590    let jet = if spherical {
591        spherical_radial(order.unsigned_abs(), z.value, radial)?
592    } else {
593        cylindrical_radial(order, z.value, radial)?
594    };
595    Ok(z.chain(jet.value, jet.first))
596}
597
598#[cfg(test)]
599mod tests {
600    //! Radial jets and sequences against single Bessel evaluations; the Bessel identities
601    //! of the public functions are in `properties/special.rs`.
602
603    use proptest::{prelude::*, test_runner::TestCaseError};
604
605    use super::{
606        Bessel, Radial, SERIES_RADIUS, bessel, cylindrical_radial, spherical_hankels,
607        spherical_radial, spherical_radial_sequence,
608    };
609    use crate::{
610        Complex, Error,
611        numerics::ratio,
612        test_support::{ALGEBRA_CASES, log_polar, prop_assert_close, radial, table},
613    };
614
615    proptest! {
616        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
617
618        #[test]
619        fn spherical_radial_sequences_match_single_degrees(
620            order in 0_u32..=40,
621            // Just below and above the series switch.
622            z in prop_oneof![
623                log_polar(-3.0..(0.996 * SERIES_RADIUS).log10(), -3.0..3.0),
624                log_polar(SERIES_RADIUS.log10()..2.0, -1.2..1.2),
625            ],
626            radial in radial(),
627        ) {
628            check_spherical_radial_sequence(order, z, radial)?;
629        }
630
631        #[test]
632        fn cylindrical_jets_match_bessel(
633            order in -20_i32..=20,
634            z in log_polar(-5.0..1.3, -3.0..3.0),
635            radial in radial(),
636        ) {
637            check_cylindrical_jet(order, z, radial)?;
638        }
639    }
640
641    /// Each jet of a spherical sequence equals the single-degree jet: bit for bit for
642    /// regular waves, whose J evaluations are shared, and to 1e-13 of the jet's size for
643    /// singular waves (in the right half-plane of translation arguments), whose Hankel
644    /// functions come from one AMOS sequence. The jet's size covers values that are
645    /// small next to their neighbours, where both evaluations lose relative accuracy.
646    fn check_spherical_radial_sequence(
647        order: u32,
648        z: Complex,
649        radial: Radial,
650    ) -> Result<(), TestCaseError> {
651        let sequence = spherical_radial_sequence(order, z, radial).unwrap();
652        prop_assert_eq!(sequence.len(), usize::try_from(order + 1).unwrap());
653        for (l, jet) in (0..).zip(sequence) {
654            let single = spherical_radial(l, z, radial).unwrap();
655            let pairs = [
656                (jet.value, single.value),
657                (jet.first, single.first),
658                (jet.second, single.second),
659            ];
660            let size = single.value.norm() + single.first.norm() + single.second.norm();
661            for (d, (actual, expected)) in pairs.into_iter().enumerate() {
662                if radial == Radial::Regular {
663                    prop_assert_eq!(actual, expected, "degree {} derivative {}", l, d);
664                } else {
665                    let tolerance = 1e-13 * size;
666                    prop_assert_close!(
667                        actual,
668                        expected,
669                        tolerance,
670                        "degree {} derivative {}",
671                        l,
672                        d
673                    );
674                }
675            }
676        }
677        Ok(())
678    }
679
680    #[test]
681    fn real_hankel_sequences_match_amos_at_domain_edges_and_turning_points() {
682        for x in [
683            0.5_f64.next_down(),
684            0.5,
685            0.5_f64.next_up(),
686            1.0,
687            3.0,
688            7.4,
689            16.0,
690            31.5,
691            59.6,
692            64.0,
693            128.0,
694            1e3,
695            1e4,
696            1e4_f64.next_up(),
697        ] {
698            for last in [0, 3, 16, 64, 65] {
699                for first in [0, last / 2, last] {
700                    let z = Complex::new(x, 0.0);
701                    let values = spherical_hankels(first, last, z).unwrap();
702                    assert_eq!(values.len(), (last - first + 1) as usize);
703                    for (degree, actual) in (first..=last).zip(values) {
704                        let expected = bessel(f64::from(degree), z, Bessel::H1, true, 0).unwrap();
705                        assert!(
706                            (actual - expected).norm() <= 1e-13 * expected.norm(),
707                            "h_{degree}({x}): {actual} != {expected}"
708                        );
709                    }
710                }
711            }
712        }
713    }
714
715    #[test]
716    fn real_hankel_sequences_match_high_precision() {
717        // Test values directly so degree 64 exercises the recurrence (a degree-64
718        // jet needs an order-65 fallback).
719        let cases = table::<String, f64>(include_str!(concat!(
720            env!("CARGO_MANIFEST_DIR"),
721            "/references/spherical_hankel.txt"
722        )));
723        assert_eq!(cases.len(), 6);
724        for (key, value) in cases {
725            let degree = key[0].parse().unwrap();
726            let x = key[1].parse().unwrap();
727            let actual = spherical_hankels(degree, degree, Complex::new(x, 0.0)).unwrap()[0];
728            let expected = Complex::new(value[0], value[1]);
729            assert!(
730                (actual - expected).norm() <= 1e-13 * expected.norm(),
731                "h_{degree}({x}): {actual} != {expected}"
732            );
733        }
734    }
735
736    /// The cylindrical jet is a power series below `|z| = SERIES_RADIUS` (0.5) and a
737    /// two-term AMOS sequence above it. Both match single-order [`bessel`] values and
738    /// their order-difference or Hankel-sequence derivatives.
739    fn check_cylindrical_jet(order: i32, z: Complex, radial: Radial) -> Result<(), TestCaseError> {
740        let jet = cylindrical_radial(order, z, radial).unwrap();
741        let kind = Bessel::from(radial);
742        // Two-term J sequences differ from single J evaluations by up to 4e-13.
743        let tolerance = if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
744            2e-13
745        } else {
746            2e-12
747        };
748        for (d, actual) in [0, 1, 2]
749            .into_iter()
750            .zip([jet.value, jet.first, jet.second])
751        {
752            let expected = bessel(f64::from(order), z, kind, false, d).unwrap();
753            prop_assert_close!(
754                actual,
755                expected,
756                tolerance * expected.norm(),
757                "derivative {}",
758                d
759            );
760        }
761        Ok(())
762    }
763
764    #[test]
765    fn cylindrical_tiny_nonzero_arguments_keep_representable_terms() {
766        for z in [Complex::new(1e-200, 0.0), Complex::new(1e-200, -2e-200)] {
767            let j0 = cylindrical_radial(0, z, Radial::Regular).unwrap();
768            let j1 = cylindrical_radial(1, z, Radial::Regular).unwrap();
769            let j2 = cylindrical_radial(2, z, Radial::Regular).unwrap();
770            assert!((ratio(j1.value, 0.5 * z) - 1.0).norm() < 1e-14);
771            assert!((ratio(j0.first, -0.5 * z) - 1.0).norm() < 1e-14);
772            assert!((ratio(j2.first, 0.25 * z) - 1.0).norm() < 1e-14);
773            assert!((ratio(j1.second, -0.375 * z) - 1.0).norm() < 1e-14);
774        }
775    }
776
777    #[test]
778    fn regular_jets_at_the_origin_and_singular_outgoing_waves() {
779        // j_l^(k)(0) = k! [z^k] j_l: 1, 0, -1/3 for l = 0; 0, 1/3, 0 for l = 1; 0, 0, 2/15 for l = 2.
780        let exact = [
781            [1.0, 0.0, -1.0 / 3.0],
782            [0.0, 1.0 / 3.0, 0.0],
783            [0.0, 0.0, 2.0 / 15.0],
784        ];
785        for (l, expected) in (0..).zip(exact) {
786            let jet = spherical_radial(l, Complex::default(), Radial::Regular).unwrap();
787            for (actual, expected) in [jet.value, jet.first, jet.second].into_iter().zip(expected) {
788                assert!(
789                    (actual - expected).norm() < 1e-16,
790                    "l = {l}: {actual} != {expected}"
791                );
792            }
793        }
794        // J_m^(k)(0) = k! [z^k] J_m: 1, 0, -1/2 for m = 0; 0, 1/2, 0 for m = 1; 0, 0, 1/4 for m = 2.
795        let exact = [[1.0, 0.0, -0.5], [0.0, 0.5, 0.0], [0.0, 0.0, 0.25]];
796        for (m, expected) in (0..).zip(exact) {
797            for (order, sign) in [(m, 1.0), (-m, if m % 2 == 0 { 1.0 } else { -1.0 })] {
798                let jet = cylindrical_radial(order, Complex::default(), Radial::Regular).unwrap();
799                for (actual, expected) in
800                    [jet.value, jet.first, jet.second].into_iter().zip(expected)
801                {
802                    assert_eq!(actual, Complex::new(sign * expected, 0.0), "m = {order}");
803                }
804            }
805        }
806        assert!(spherical_radial(1, Complex::default(), Radial::Singular).is_err());
807        assert!(cylindrical_radial(1, Complex::default(), Radial::Singular).is_err());
808        // Only zero itself is singular. Near it the jets overflow instead, in AMOS (h_2 ~
809        // 3i/z^3) or in the derivatives' powers of 1/z: h_0 = -i e^(iz)/z has h_0'' ~
810        // 2i/z^3, beyond 1e308 below |z| = 1e-103.
811        let overflows = |result: crate::Result<()>, case: &str| {
812            let Err(Error::SpecialFunction(message)) = result else {
813                panic!("{case}: expected an overflow, got {result:?}");
814            };
815            assert!(message.contains("overflow"), "{case}: {message}");
816        };
817        for (radius, phase) in [(1e-200, -0.8), (1e-200, 2.9), (1e-120, 0.3), (1e-120, -2.2)] {
818            let tiny = Complex::from_polar(radius, phase);
819            for l in 0..=2 {
820                let case = format!("h_{l}({tiny:e})");
821                overflows(spherical_radial(l, tiny, Radial::Singular).map(drop), &case);
822                overflows(
823                    spherical_radial_sequence(l, tiny, Radial::Singular).map(drop),
824                    &case,
825                );
826            }
827        }
828    }
829}