complex_bessel/lib.rs
1//! Pure Rust implementation of complex Bessel functions based on Amos Algorithm 644 (ACM TOMS 644).
2//!
3//! # Features
4//!
5//! - **f32 & f64** — all functions accept `Complex<f64>` or `Complex<f32>`
6//! - **Complete function set** — J, Y, I, K, H<sup>(1)</sup>, H<sup>(2)</sup>, Ai, Ai′, Bi, Bi′
7//! - **Consecutive orders** — `_seq` and `_seq_into` variants evaluate at orders ν, ν+1, …, ν+n−1 in one call
8//! - **Exponential scaling** — `_scaled` variants prevent overflow/underflow
9//! - **Negative orders** — supports ν < 0 via DLMF reflection formulas (not in Amos)
10//! - **`no_std`** — no allocator needed; only the `Vec`-returning `_seq` variants require `std`
11//!
12//! # Quick start
13//!
14//! ```
15//! use complex_bessel::*;
16//! use num_complex::Complex;
17//!
18//! let z = Complex::new(1.0, 2.0);
19//!
20//! let j = besselj(0.5, z).unwrap();
21//! let k = besselk(1.0, z).unwrap();
22//! let h = hankel1(0.0, z).unwrap();
23//!
24//! // Scaled versions prevent overflow/underflow
25//! let k_scaled = besselk_scaled(1.0, z).unwrap();
26//!
27//! // Airy functions
28//! let ai = airy(z).unwrap();
29//! let ai_prime = airyprime(z).unwrap();
30//! ```
31//!
32//! # Functions
33//!
34//! | Function | Description | Scaled variant returns |
35//! |----------|-------------|------------------------|
36//! | [`besselj`]`(ν, z)`<br>[`besselj_scaled`]`(ν, z)`<br>[`besselj_seq`]`(ν, z, n, scaling)`<br>[`besselj_seq_into`]`(ν, z, scaling, out)` | J<sub>ν</sub>(z), Bessel first kind | exp(−\|Im(z)\|) · J<sub>ν</sub>(z) |
37//! | [`bessely`]`(ν, z)`<br>[`bessely_scaled`]`(ν, z)`<br>[`bessely_seq`]`(ν, z, n, scaling)`<br>[`bessely_seq_into`]`(ν, z, scaling, out)` | Y<sub>ν</sub>(z), Bessel second kind | exp(−\|Im(z)\|) · Y<sub>ν</sub>(z) |
38//! | [`besseli`]`(ν, z)`<br>[`besseli_scaled`]`(ν, z)`<br>[`besseli_seq`]`(ν, z, n, scaling)`<br>[`besseli_seq_into`]`(ν, z, scaling, out)` | I<sub>ν</sub>(z), modified first kind | exp(−\|Re(z)\|) · I<sub>ν</sub>(z) |
39//! | [`besselk`]`(ν, z)`<br>[`besselk_scaled`]`(ν, z)`<br>[`besselk_seq`]`(ν, z, n, scaling)`<br>[`besselk_seq_into`]`(ν, z, scaling, out)` | K<sub>ν</sub>(z), modified second kind | exp(z) · K<sub>ν</sub>(z) |
40//! | [`hankel1`]`(ν, z)`<br>[`hankel1_scaled`]`(ν, z)`<br>[`hankel1_seq`]`(ν, z, n, scaling)`<br>[`hankel1_seq_into`]`(ν, z, scaling, out)` | H<sub>ν</sub><sup>(1)</sup>(z), Hankel first kind | exp(−iz) · H<sub>ν</sub><sup>(1)</sup>(z) |
41//! | [`hankel2`]`(ν, z)`<br>[`hankel2_scaled`]`(ν, z)`<br>[`hankel2_seq`]`(ν, z, n, scaling)`<br>[`hankel2_seq_into`]`(ν, z, scaling, out)` | H<sub>ν</sub><sup>(2)</sup>(z), Hankel second kind | exp(iz) · H<sub>ν</sub><sup>(2)</sup>(z) |
42//! | [`airy`]`(z)`<br>[`airy_scaled`]`(z)`<br>[`airy_raw`]`(z, scaling)` | Ai(z), Airy first kind | exp(ζ) · Ai(z) |
43//! | [`airyprime`]`(z)`<br>[`airyprime_scaled`]`(z)`<br>[`airyprime_raw`]`(z, scaling)` | Ai′(z), derivative of Airy first kind | exp(ζ) · Ai′(z) |
44//! | [`biry`]`(z)`<br>[`biry_scaled`]`(z)`<br>[`biry_raw`]`(z, scaling)` | Bi(z), Airy second kind | exp(−\|Re(ζ)\|) · Bi(z) |
45//! | [`biryprime`]`(z)`<br>[`biryprime_scaled`]`(z)`<br>[`biryprime_raw`]`(z, scaling)` | Bi′(z), derivative of Airy second kind | exp(−\|Re(ζ)\|) · Bi′(z) |
46//!
47//! where ζ = (2/3) z√z.
48//!
49//! Functions with a branch cut along the negative real axis (J and I of
50//! non-integer order, Y, K, H, and the factor exp(ζ) of the scaled Ai and Ai′)
51//! take their values there from above the cut (arg z = π), as in Amos: an
52//! imaginary part of −0 is treated like +0.
53//!
54//! ## Exponential scaling
55//!
56//! Each `_scaled` variant multiplies the result by an exponential factor that
57//! cancels the asymptotic growth (or decay), keeping values in a representable
58//! floating-point range. The exact factor is listed in the "Scaled variant
59//! returns" column above. Use scaled functions whenever |z| or |Im(z)| is
60//! large enough that the unscaled result would overflow or underflow.
61//!
62//! # Negative orders
63//!
64//! All functions accept any real order, including negative values.
65//! DLMF reflection formulas are applied automatically:
66//!
67//! - **J**: J<sub>−ν</sub>(z) = cos(νπ) J<sub>ν</sub>(z) − sin(νπ) Y<sub>ν</sub>(z) (DLMF 10.2.3)
68//! - **Y**: Y<sub>−ν</sub>(z) = sin(νπ) J<sub>ν</sub>(z) + cos(νπ) Y<sub>ν</sub>(z) (DLMF 10.2.3)
69//! - **I**: I<sub>−ν</sub>(z) = I<sub>ν</sub>(z) + (2/π) sin(νπ) K<sub>ν</sub>(z) (DLMF 10.27.2)
70//! - **K**: K<sub>−ν</sub>(z) = K<sub>ν</sub>(z) (DLMF 10.27.3)
71//! - **H<sup>(1)</sup>**: H<sup>(1)</sup><sub>−ν</sub>(z) = exp(νπi) H<sup>(1)</sup><sub>ν</sub>(z) (DLMF 10.4.6)
72//! - **H<sup>(2)</sup>**: H<sup>(2)</sup><sub>−ν</sub>(z) = exp(−νπi) H<sup>(2)</sup><sub>ν</sub>(z) (DLMF 10.4.6)
73//!
74//! For integer orders, simplified identities are used (e.g., J<sub>−n</sub>(z) = (−1)<sup>n</sup> J<sub>n</sub>(z)).
75//! For half-integer orders, cos(νπ) = 0, so only one function is computed:
76//! J<sub>−ν</sub>(z) = −sin(νπ) Y<sub>ν</sub>(z) and Y<sub>−ν</sub>(z) = sin(νπ) J<sub>ν</sub>(z).
77//!
78//! # Accuracy on the axes
79//!
80//! Amos's algorithm is accurate normwise: the error is small relative to |f(z)|.
81//! On the real and imaginary axes some values combine two real functions of
82//! very different size, e.g. H<sup>(1)</sup><sub>ν</sub>(x) = J<sub>ν</sub>(x) + iY<sub>ν</sub>(x)
83//! with J ≪ Y for x < ν, and Amos's small component can then be off entirely.
84//! The crate recomputes such a component from the two real functions:
85//!
86//! - H<sup>(1)</sup>, H<sup>(2)</sup> on the positive and negative real axis
87//! - Y on the negative real axis (also scaled)
88//! - K on the imaginary axis
89//! - H<sup>(2)</sup> on the positive and H<sup>(1)</sup> on the negative imaginary axis (also scaled)
90//!
91//! For Y and H<sup>(2)</sup> on the negative real axis this includes parts for
92//! |ν| < x: Re Y<sub>ν</sub>(−x) = cos(νπ) Y<sub>ν</sub>(x) for orders close to a
93//! half-integer, and, for |ν| < 18 next to their zeros, parts that are
94//! Y<sub>|ν|</sub> alone: Re Y<sub>ν</sub>(−x) for ν ≥ 0, and parts at or close to
95//! integer and half-integer orders, e.g. Im H<sup>(2)</sup><sub>n</sub>(−x) =
96//! (−1)<sup>n+1</sup> Y<sub>n</sub>(x). J and Y are exactly real on the positive
97//! real axis.
98//!
99//! A remaining limitation: in the other functions, a small component that is
100//! a nearly vanishing cos(νπ) or sin(νπ) times one real function, for orders
101//! close to an integer or a half-integer, is still only accurate relative to
102//! |f(z)|; its relative error grows roughly like ε/|cos(νπ)| (or
103//! ε/|sin(νπ)|). An example is Re J<sub>ν</sub>(−x) = cos(νπ) J<sub>ν</sub>(x).
104//!
105//! Only values exactly on an axis are recomputed. Next to an axis (e.g.
106//! Im z = 10⁻¹⁰·|z|) the same cancellation remains, less deep the farther z
107//! is from the axis, and the parts are accurate only relative to |f(z)|, as
108//! in Amos.
109//!
110//! For f32, J<sub>ν</sub>(x) on the real axis with ν ≳ 10 and 35 ≲ x ≲ 45 errs
111//! by up to about 7e-3·|H<sub>ν</sub>(x)|, as in Amos's single-precision CBESJ.
112//!
113//! # Function variants
114//!
115//! ## Consecutive orders
116//!
117//! The `_seq` variants ([`besselj_seq`], [`besselk_seq`], …) compute values at
118//! consecutive orders ν, ν+1, …, ν+n−1 in a single call and return
119//! a [`BesselResult`] that includes an [`Accuracy`] field.
120//! The `_seq_into` variants ([`besselj_seq_into`], [`besselk_seq_into`], …)
121//! write the sequence into a caller-provided slice and return a [`SeqInfo`]
122//! with [`Accuracy`]; they never allocate.
123//! The `_raw` Airy variants ([`airy_raw`], [`biry_raw`], …) similarly return
124//! an [`AiryResult`] with [`Accuracy`].
125//! All other functions return only the computed value without [`Accuracy`].
126//!
127//! **[`Accuracy`]:**
128//!
129//! | Status | Meaning |
130//! |--------|---------|
131//! | [`Normal`](Accuracy::Normal) | Full machine precision |
132//! | [`Reduced`](Accuracy::Reduced) | More than half of significant digits may be lost |
133//!
134//! [`Reduced`](Accuracy::Reduced) is extremely rare in practice. SciPy's Bessel wrappers also silently
135//! discard the equivalent Amos IERR=3 flag by default.
136//!
137//! To check the accuracy status, use a `_seq`/`_seq_into` function (Bessel) or a
138//! `_raw` function (Airy):
139//!
140//! ```
141//! use complex_bessel::*;
142//! use num_complex::Complex;
143//!
144//! let z = Complex::new(1.0, 2.0);
145//! let mut out = [Complex::new(0.0, 0.0); 1];
146//! let info = besselk_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
147//! assert!(matches!(info.status, Accuracy::Normal));
148//!
149//! let result = airy_raw(z, Scaling::Unscaled).unwrap();
150//! assert!(matches!(result.status, Accuracy::Normal));
151//! ```
152//!
153//! # Error handling
154//!
155//! All functions return `Result<_, Error>`. The four error variants are:
156//!
157//! | Variant | Cause |
158//! |---------|-------|
159//! | [`InvalidInput`](Error::InvalidInput) | non-finite z or ν; z = 0 for K, H, Y (except at negative half-integer orders) and for J, I of negative non-integer order; n < 1 (empty `out`) |
160//! | [`Overflow`](Error::Overflow) | \|z\| or \|ν\| too large (or \|z\| too small) for a finite result |
161//! | [`TotalPrecisionLoss`](Error::TotalPrecisionLoss) | \|z\| or \|ν\| too large for meaningful computation |
162//! | [`ConvergenceFailure`](Error::ConvergenceFailure) | internal algorithm did not converge |
163//!
164//! # Using without `std`
165//!
166//! Turn off the default `std` feature:
167//!
168//! ```toml
169//! complex-bessel = { version = "0.5", default-features = false }
170//! ```
171//!
172//! All functions except the six `_seq` variants (which return a `Vec`) remain
173//! available, and none of them allocate. Math functions then come from `libm`
174//! instead of the platform library.
175
176#![warn(missing_docs)]
177// Without `std`, the docs still link to the `std`-only `_seq` functions.
178#![cfg_attr(not(feature = "std"), allow(rustdoc::broken_intra_doc_links))]
179#![no_std]
180
181#[cfg(feature = "std")]
182extern crate alloc;
183#[cfg(feature = "std")]
184extern crate std;
185
186pub(crate) mod airy;
187pub(crate) mod algo;
188pub(crate) mod axis;
189pub(crate) mod besh;
190pub(crate) mod besi;
191pub(crate) mod besj;
192pub(crate) mod besk;
193pub(crate) mod besy;
194pub mod machine;
195pub mod types;
196pub(crate) mod utils;
197
198pub use machine::BesselFloat;
199#[cfg(feature = "std")]
200pub use types::BesselResult;
201pub use types::{Accuracy, AiryResult, Error, Scaling, SeqInfo};
202
203use num_complex::Complex;
204use types::{AiryDerivative, HankelKind};
205use utils::{Workspace, with_workspace};
206
207// ── Helper: integer order detection ──
208
209/// Check if `nu` is a non-negative integer. Returns `Some(n)` if so.
210#[inline]
211fn as_integer<T: BesselFloat>(nu: T) -> Option<i64> {
212 if nu == nu.floor() {
213 // Safe conversion: orders beyond i64 range are not practical
214 nu.to_i64()
215 } else {
216 None
217 }
218}
219
220// ── Element-wise reflection helpers (shared by single-value and _seq) ──
221
222/// J_{-ν}(z) = cos(νπ)·J_ν(z) − sin(νπ)·Y_ν(z) (DLMF 10.2.3)
223#[inline]
224fn reflect_j_element<T: BesselFloat>(order: T, j: Complex<T>, y: Complex<T>) -> Complex<T> {
225 j * utils::cospi(order) - y * utils::sinpi(order)
226}
227
228/// Y_{-ν}(z) = sin(νπ)·J_ν(z) + cos(νπ)·Y_ν(z) (DLMF 10.2.3)
229#[inline]
230fn reflect_y_element<T: BesselFloat>(order: T, j: Complex<T>, y: Complex<T>) -> Complex<T> {
231 j * utils::sinpi(order) + y * utils::cospi(order)
232}
233
234/// I_{-ν}(z) = I_ν(z) + (2/π)·sin(νπ)·K_ν(z) (DLMF 10.27.2)
235/// `k_val` must already have scaling correction applied by the caller.
236#[inline]
237fn reflect_i_element<T: BesselFloat>(order: T, i_val: Complex<T>, k_val: Complex<T>) -> Complex<T> {
238 let pi = T::from_f64(core::f64::consts::PI);
239 let two = T::from_f64(2.0);
240 utils::mul_add_scalar(k_val, two / pi * utils::sinpi(order), i_val)
241}
242
243/// H^(m)_{-ν}(z) = exp(±νπi)·H^(m)_ν(z) (DLMF 10.4.6/7)
244#[inline]
245fn reflect_h_element<T: BesselFloat>(order: T, kind: HankelKind, h: Complex<T>) -> Complex<T> {
246 let cos_nu_pi = utils::cospi(order);
247 let sin_nu_pi = utils::sinpi(order);
248 let rotation = match kind {
249 HankelKind::First => Complex::new(cos_nu_pi, sin_nu_pi),
250 HankelKind::Second => Complex::new(cos_nu_pi, -sin_nu_pi),
251 };
252 h * rotation
253}
254
255/// Convert K scaling to I scaling for the reflection formula.
256/// I_scaled = exp(-|Re(z)|)·I, K_scaled = exp(z)·K.
257/// factor = exp(-|Re(z)|) / exp(z) = exp(-i·Im(z)) · [exp(-2·Re(z)) if Re(z)>0]
258#[inline]
259fn k_to_i_scaling_correction<T: BesselFloat>(z: Complex<T>, k_val: Complex<T>) -> Complex<T> {
260 let two = T::from_f64(2.0);
261 let (sin_a, cos_a) = (-z.im).sin_cos();
262 let mut result = k_val * Complex::new(cos_a, sin_a);
263 if z.re > T::zero() {
264 result = result * (-two * z.re).exp();
265 }
266 result
267}
268
269/// (-1)^n sign factor for integer order reflection.
270#[inline]
271fn integer_sign<T: BesselFloat>(n: i64) -> T {
272 if n % 2 == 0 { T::one() } else { -T::one() }
273}
274
275// ── Internal: compute with given scaling for negative order support ──
276
277#[inline]
278fn besselj_value<T: BesselFloat>(
279 nu: T,
280 z: Complex<T>,
281 scaling: Scaling,
282) -> Result<Complex<T>, Error> {
283 let zero = T::zero();
284 let czero = Complex::new(zero, zero);
285 if nu >= zero {
286 let mut y = [czero];
287 besj::zbesj(z, nu, scaling, &mut y)?;
288 return Ok(y[0]);
289 }
290
291 let abs_nu = nu.abs();
292
293 // Integer shortcut: J_{-n}(z) = (-1)^n * J_n(z)
294 if let Some(n) = as_integer(abs_nu) {
295 let mut y = [czero];
296 besj::zbesj(z, abs_nu, scaling, &mut y)?;
297 return Ok(y[0] * integer_sign::<T>(n));
298 }
299
300 // Half-integer |ν|: cos(νπ) = 0, so J_ν is not needed
301 if utils::cospi(abs_nu) == zero {
302 let mut y_buf = [czero];
303 besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
304 return Ok(reflect_j_element(abs_nu, czero, y_buf[0]));
305 }
306
307 // General case: need both J and Y at positive |ν|
308 let mut j_buf = [czero];
309 let mut y_buf = [czero];
310 besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
311 besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
312
313 Ok(reflect_j_element(abs_nu, j_buf[0], y_buf[0]))
314}
315
316#[inline]
317fn bessely_value<T: BesselFloat>(
318 nu: T,
319 z: Complex<T>,
320 scaling: Scaling,
321) -> Result<Complex<T>, Error> {
322 let zero = T::zero();
323 let czero = Complex::new(zero, zero);
324 if nu >= zero {
325 let mut y = [czero];
326 besy::zbesy(z, nu, scaling, &mut y)?;
327 return Ok(y[0]);
328 }
329
330 let abs_nu = nu.abs();
331
332 // Integer shortcut: Y_{-n}(z) = (-1)^n * Y_n(z)
333 if let Some(n) = as_integer(abs_nu) {
334 let mut y = [czero];
335 besy::zbesy(z, abs_nu, scaling, &mut y)?;
336 return Ok(y[0] * integer_sign::<T>(n));
337 }
338
339 // Half-integer |ν|: cos(νπ) = 0, so Y_ν is not needed (it can overflow,
340 // or be undefined at z = 0, where Y_{−ν} = ±J_ν is finite)
341 if utils::cospi(abs_nu) == zero {
342 let mut j_buf = [czero];
343 besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
344 return Ok(reflect_y_element(abs_nu, j_buf[0], czero));
345 }
346
347 // General case: need both J and Y at positive |ν|
348 let mut j_buf = [czero];
349 let mut y_buf = [czero];
350 besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
351 besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
352
353 Ok(reflect_y_element(abs_nu, j_buf[0], y_buf[0]))
354}
355
356#[inline]
357fn besseli_internal<T: BesselFloat>(
358 nu: T,
359 z: Complex<T>,
360 scaling: Scaling,
361) -> Result<Complex<T>, Error> {
362 let zero = T::zero();
363 let czero = Complex::new(zero, zero);
364 if nu >= zero {
365 let mut y = [czero];
366 besi::zbesi(z, nu, scaling, &mut y)?;
367 return Ok(y[0]);
368 }
369
370 let abs_nu = nu.abs();
371
372 // Integer shortcut: I_{-n}(z) = I_n(z)
373 if as_integer(abs_nu).is_some() {
374 let mut y = [czero];
375 besi::zbesi(z, abs_nu, scaling, &mut y)?;
376 return Ok(y[0]);
377 }
378
379 // General case: need both I and K at positive |ν|
380 let mut i_buf = [czero];
381 let mut k_buf = [czero];
382 besi::zbesi(z, abs_nu, scaling, &mut i_buf)?;
383 besk::zbesk(z, abs_nu, scaling, &mut k_buf)?;
384
385 let mut k_val = k_buf[0];
386 if scaling == Scaling::Exponential {
387 k_val = k_to_i_scaling_correction(z, k_val);
388 }
389
390 Ok(reflect_i_element(abs_nu, i_buf[0], k_val))
391}
392
393#[inline]
394fn besselk_value<T: BesselFloat>(
395 nu: T,
396 z: Complex<T>,
397 scaling: Scaling,
398) -> Result<Complex<T>, Error> {
399 // K_{-ν}(z) = K_ν(z) (DLMF 10.27.3)
400 let abs_nu = nu.abs();
401 let zero = T::zero();
402 let mut y = [Complex::new(zero, zero)];
403 besk::zbesk(z, abs_nu, scaling, &mut y)?;
404 Ok(y[0])
405}
406
407#[inline]
408fn hankel_value<T: BesselFloat>(
409 kind: HankelKind,
410 nu: T,
411 z: Complex<T>,
412 scaling: Scaling,
413) -> Result<Complex<T>, Error> {
414 let zero = T::zero();
415 if nu >= zero {
416 let mut y = [Complex::new(zero, zero)];
417 besh::zbesh(z, nu, kind, scaling, &mut y)?;
418 return Ok(y[0]);
419 }
420
421 let abs_nu = nu.abs();
422 let mut y = [Complex::new(zero, zero)];
423 besh::zbesh(z, abs_nu, kind, scaling, &mut y)?;
424
425 Ok(reflect_h_element(abs_nu, kind, y[0]))
426}
427
428// The functions as computed by Amos (with the reflection formulas for ν < 0),
429// then with the components on the axes made accurate (see `axis`).
430
431#[inline]
432fn besselj_internal<T: BesselFloat>(
433 nu: T,
434 z: Complex<T>,
435 scaling: Scaling,
436) -> Result<Complex<T>, Error> {
437 Ok(axis::real_on_positive_axis(
438 z,
439 besselj_value(nu, z, scaling)?,
440 ))
441}
442
443#[inline]
444fn bessely_internal<T: BesselFloat>(
445 nu: T,
446 z: Complex<T>,
447 scaling: Scaling,
448) -> Result<Complex<T>, Error> {
449 let v = axis::real_on_positive_axis(z, bessely_value(nu, z, scaling)?);
450 Ok(axis::fix_single(axis::AxisFn::Y, nu, z, scaling, v))
451}
452
453#[inline]
454fn besselk_internal<T: BesselFloat>(
455 nu: T,
456 z: Complex<T>,
457 scaling: Scaling,
458) -> Result<Complex<T>, Error> {
459 let v = besselk_value(nu, z, scaling)?;
460 Ok(axis::fix_single(axis::AxisFn::K, nu, z, scaling, v))
461}
462
463#[inline]
464fn hankel_internal<T: BesselFloat>(
465 kind: HankelKind,
466 nu: T,
467 z: Complex<T>,
468 scaling: Scaling,
469) -> Result<Complex<T>, Error> {
470 let v = hankel_value(kind, nu, z, scaling)?;
471 Ok(axis::fix_single(
472 axis::AxisFn::Hankel(kind),
473 nu,
474 z,
475 scaling,
476 v,
477 ))
478}
479
480// ── Single-value convenience functions ──
481
482/// Bessel function of the first kind, J<sub>ν</sub>(z).
483///
484/// Computes a single value of J_ν(z) for complex z and real order ν
485/// (any real value, including negative).
486///
487/// For negative ν, the DLMF 10.2.3 reflection formula is applied:
488/// `J_{-ν}(z) = cos(νπ) J_ν(z) - sin(νπ) Y_ν(z)`.
489/// For negative integer n, `J_{−n}(z) = (−1)^n J_n(z)`; for half-integer ν,
490/// `J_{−ν}(z) = −sin(νπ) Y_ν(z)`.
491///
492/// # Example
493///
494/// ```
495/// use complex_bessel::besselj;
496/// use num_complex::Complex;
497///
498/// let z = Complex::new(1.0_f64, 0.0);
499/// let j0 = besselj(0.0, z).unwrap();
500/// assert!((j0.re - 0.7652).abs() < 1e-3); // J_0(1) ≈ 0.7652
501/// ```
502///
503/// # Errors
504///
505/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
506/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
507/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
508/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
509#[inline]
510pub fn besselj<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
511 besselj_internal(nu, z, Scaling::Unscaled)
512}
513
514/// Bessel function of the second kind, Y<sub>ν</sub>(z).
515///
516/// Computes a single value of Y_ν(z) for complex z and real order ν
517/// (any real value, including negative).
518///
519/// For negative ν, the DLMF 10.2.3 reflection formula is applied:
520/// `Y_{-ν}(z) = sin(νπ) J_ν(z) + cos(νπ) Y_ν(z)`.
521/// For negative integer n, `Y_{−n}(z) = (−1)^n Y_n(z)`; for half-integer ν,
522/// `Y_{−ν}(z) = sin(νπ) J_ν(z)`.
523///
524/// # Example
525///
526/// ```
527/// use complex_bessel::bessely;
528/// use num_complex::Complex;
529///
530/// let z = Complex::new(1.0_f64, 0.0);
531/// let y0 = bessely(0.0, z).unwrap();
532/// assert!((y0.re - 0.0883).abs() < 1e-3); // Y_0(1) ≈ 0.0883
533/// ```
534///
535/// # Errors
536///
537/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 (except at negative half-integer ν, where Y_ν(0) = 0).
538/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
539/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
540/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
541#[inline]
542pub fn bessely<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
543 bessely_internal(nu, z, Scaling::Unscaled)
544}
545
546/// Modified Bessel function of the first kind, I<sub>ν</sub>(z).
547///
548/// Computes a single value of I_ν(z) for complex z and real order ν
549/// (any real value, including negative).
550///
551/// For negative ν, the DLMF 10.27.2 reflection formula is applied:
552/// `I_{-ν}(z) = I_ν(z) + (2/π) sin(νπ) K_ν(z)`.
553/// For negative integer n, `I_{−n}(z) = I_n(z)`.
554///
555/// # Example
556///
557/// ```
558/// use complex_bessel::besseli;
559/// use num_complex::Complex;
560///
561/// let z = Complex::new(1.0_f64, 0.0);
562/// let i0 = besseli(0.0, z).unwrap();
563/// assert!((i0.re - 1.2661).abs() < 1e-3); // I_0(1) ≈ 1.2661
564/// ```
565///
566/// # Errors
567///
568/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
569/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
570/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
571/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
572#[inline]
573pub fn besseli<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
574 besseli_internal(nu, z, Scaling::Unscaled)
575}
576
577/// Modified Bessel function of the second kind, K<sub>ν</sub>(z).
578///
579/// Computes a single value of K_ν(z) for complex z and real order ν
580/// (any real value, including negative).
581///
582/// For negative ν, K_{-ν}(z) = K_ν(z) (DLMF 10.27.3).
583///
584/// # Example
585///
586/// ```
587/// use complex_bessel::besselk;
588/// use num_complex::Complex;
589///
590/// let z = Complex::new(1.0_f64, 0.0);
591/// let k0 = besselk(0.0, z).unwrap();
592/// assert!((k0.re - 0.4211).abs() < 1e-3); // K_0(1) ≈ 0.4211
593/// ```
594///
595/// # Errors
596///
597/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
598/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
599/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
600/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
601#[inline]
602pub fn besselk<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
603 besselk_internal(nu, z, Scaling::Unscaled)
604}
605
606/// Hankel function of the first kind, H<sub>ν</sub><sup>(1)</sup>(z).
607///
608/// Computes a single value of H_ν^(1)(z) for complex z and real order ν
609/// (any real value, including negative).
610///
611/// For negative ν, the DLMF 10.4.6 reflection formula is applied:
612/// `H^(1)_{-ν}(z) = exp(νπi) H^(1)_ν(z)`.
613/// For negative integer n, `H^(1)_{−n}(z) = (−1)^n H^(1)_n(z)`.
614///
615/// # Example
616///
617/// ```
618/// use complex_bessel::hankel1;
619/// use num_complex::Complex;
620///
621/// let z = Complex::new(1.0_f64, 0.0);
622/// let h = hankel1(0.0, z).unwrap();
623/// assert!((h.re - 0.7652).abs() < 1e-3); // Re = J_0(1) ≈ 0.7652
624/// assert!((h.im - 0.0883).abs() < 1e-3); // Im = Y_0(1) ≈ 0.0883
625/// ```
626///
627/// # Errors
628///
629/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
630/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
631/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
632/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
633#[inline]
634pub fn hankel1<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
635 hankel_internal(HankelKind::First, nu, z, Scaling::Unscaled)
636}
637
638/// Hankel function of the second kind, H<sub>ν</sub><sup>(2)</sup>(z).
639///
640/// Computes a single value of H_ν^(2)(z) for complex z and real order ν
641/// (any real value, including negative).
642///
643/// For negative ν, the DLMF 10.4.6 reflection formula is applied:
644/// `H^(2)_{-ν}(z) = exp(-νπi) H^(2)_ν(z)`.
645/// For negative integer n, `H^(2)_{−n}(z) = (−1)^n H^(2)_n(z)`.
646///
647/// # Example
648///
649/// ```
650/// use complex_bessel::hankel2;
651/// use num_complex::Complex;
652///
653/// let z = Complex::new(1.0_f64, 0.0);
654/// let h = hankel2(0.0, z).unwrap();
655/// assert!((h.re - 0.7652).abs() < 1e-3); // Re = J_0(1) ≈ 0.7652
656/// assert!((h.im + 0.0883).abs() < 1e-3); // Im = -Y_0(1) ≈ -0.0883
657/// ```
658///
659/// # Errors
660///
661/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
662/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
663/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
664/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
665#[inline]
666pub fn hankel2<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
667 hankel_internal(HankelKind::Second, nu, z, Scaling::Unscaled)
668}
669
670/// Airy function Ai(z).
671///
672/// Computes the Airy function Ai(z) for complex z.
673/// Ai(z) is a solution to the differential equation w′′ − z·w = 0
674/// that decays super-exponentially for large positive real z.
675///
676/// # Example
677///
678/// ```
679/// use complex_bessel::airy;
680/// use num_complex::Complex;
681///
682/// let z = Complex::new(0.0_f64, 0.0);
683/// let ai = airy(z).unwrap();
684/// assert!((ai.re - 0.3550).abs() < 1e-3); // Ai(0) ≈ 0.3550
685/// ```
686///
687/// # Errors
688///
689/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
690/// - [`Error::Overflow`] if |z| is too large for a finite result.
691/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
692/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
693#[inline]
694pub fn airy<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
695 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Value, Scaling::Unscaled)?;
696 Ok(result)
697}
698
699/// Derivative of the Airy function, Ai'(z).
700///
701/// Computes the derivative of Ai(z) for complex z.
702/// Ai'(z) decays super-exponentially for large positive real z, just as Ai(z) does.
703/// Satisfies the differential equation Ai′′(z) = z · Ai(z).
704///
705/// # Example
706///
707/// ```
708/// use complex_bessel::airyprime;
709/// use num_complex::Complex;
710///
711/// let z = Complex::new(0.0_f64, 0.0);
712/// let ai_prime = airyprime(z).unwrap();
713/// assert!((ai_prime.re + 0.2588).abs() < 1e-3); // Ai'(0) ≈ -0.2588
714/// ```
715///
716/// # Errors
717///
718/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
719/// - [`Error::Overflow`] if |z| is too large for a finite result.
720/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
721/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
722#[inline]
723pub fn airyprime<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
724 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Derivative, Scaling::Unscaled)?;
725 Ok(result)
726}
727
728/// Airy function of the second kind, Bi(z).
729///
730/// Computes the Airy function Bi(z) for complex z.
731/// Bi(z) is the solution to w′′ − z·w = 0 that grows super-exponentially
732/// for large positive real z.
733///
734/// # Example
735///
736/// ```
737/// use complex_bessel::biry;
738/// use num_complex::Complex;
739///
740/// let z = Complex::new(0.0_f64, 0.0);
741/// let bi = biry(z).unwrap();
742/// assert!((bi.re - 0.6149).abs() < 1e-3); // Bi(0) ≈ 0.6149
743/// ```
744///
745/// # Errors
746///
747/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
748/// - [`Error::Overflow`] if |z| is too large for a finite result.
749/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
750/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
751#[inline]
752pub fn biry<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
753 let (result, _status) = airy::zbiry(z, AiryDerivative::Value, Scaling::Unscaled)?;
754 Ok(result)
755}
756
757/// Derivative of the Airy function of the second kind, Bi'(z).
758///
759/// Computes the derivative of Bi(z) for complex z.
760/// Bi'(z) grows super-exponentially for large positive real z, just as Bi(z) does.
761/// Satisfies the differential equation Bi′′(z) = z · Bi(z).
762///
763/// # Example
764///
765/// ```
766/// use complex_bessel::biryprime;
767/// use num_complex::Complex;
768///
769/// let z = Complex::new(0.0_f64, 0.0);
770/// let bi_prime = biryprime(z).unwrap();
771/// assert!((bi_prime.re - 0.4483).abs() < 1e-3); // Bi'(0) ≈ 0.4483
772/// ```
773///
774/// # Errors
775///
776/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
777/// - [`Error::Overflow`] if |z| is too large for a finite result.
778/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
779/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
780#[inline]
781pub fn biryprime<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
782 let (result, _status) = airy::zbiry(z, AiryDerivative::Derivative, Scaling::Unscaled)?;
783 Ok(result)
784}
785
786// ── Scaled single-value functions ──
787
788/// Scaled Bessel function of the first kind, exp(−|Im(z)|) · J<sub>ν</sub>(z).
789///
790/// The exponential factor cancels the asymptotic growth of J for large imaginary
791/// arguments, keeping results in a representable floating-point range.
792///
793/// Supports negative ν via the same reflection formula as [`besselj`].
794///
795/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
796///
797/// # Example
798///
799/// ```
800/// use complex_bessel::besselj_scaled;
801/// use num_complex::Complex;
802///
803/// let z = Complex::new(0.0_f64, 10.0);
804/// let j_s = besselj_scaled(0.0, z).unwrap();
805/// assert!((j_s.re - 0.1278).abs() < 1e-3); // exp(-|Im(z)|) * J_0(10i) ≈ 0.1278
806/// ```
807///
808/// # Errors
809///
810/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
811/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
812/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
813/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
814#[inline]
815pub fn besselj_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
816 besselj_internal(nu, z, Scaling::Exponential)
817}
818
819/// Scaled Bessel function of the second kind, exp(−|Im(z)|) · Y<sub>ν</sub>(z).
820///
821/// The exponential factor cancels the asymptotic growth of Y for large imaginary
822/// arguments, keeping results in a representable floating-point range.
823///
824/// Supports negative ν via the same reflection formula as [`bessely`].
825///
826/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
827///
828/// # Example
829///
830/// ```
831/// use complex_bessel::bessely_scaled;
832/// use num_complex::Complex;
833///
834/// let z = Complex::new(0.0_f64, 10.0);
835/// let y_s = bessely_scaled(0.0, z).unwrap();
836/// assert!((y_s.im - 0.1278).abs() < 1e-3); // exp(-|Im(z)|) * Y_0(10i), Im ≈ 0.1278
837/// ```
838///
839/// # Errors
840///
841/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 (except at negative half-integer ν, where Y_ν(0) = 0).
842/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
843/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
844/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
845#[inline]
846pub fn bessely_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
847 bessely_internal(nu, z, Scaling::Exponential)
848}
849
850/// Scaled modified Bessel function of the first kind, exp(−|Re(z)|) · I<sub>ν</sub>(z).
851///
852/// I_ν(z) grows exponentially for large |Re(z)|, so the unscaled value can
853/// easily overflow. The scaling factor exp(−|Re(z)|) keeps the result finite.
854///
855/// Supports negative ν via the same reflection formula as [`besseli`].
856///
857/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
858///
859/// # Example
860///
861/// ```
862/// use complex_bessel::besseli_scaled;
863/// use num_complex::Complex;
864///
865/// let z = Complex::new(10.0_f64, 0.0);
866/// let i_s = besseli_scaled(0.0, z).unwrap();
867/// assert!((i_s.re - 0.1278).abs() < 1e-3); // exp(-10) * I_0(10) ≈ 0.1278
868/// ```
869///
870/// # Errors
871///
872/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
873/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
874/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
875/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
876#[inline]
877pub fn besseli_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
878 besseli_internal(nu, z, Scaling::Exponential)
879}
880
881/// Scaled modified Bessel function of the second kind, exp(z) · K<sub>ν</sub>(z).
882///
883/// K_ν(z) decays exponentially for large Re(z), so unscaled values can underflow
884/// to zero. The scaling factor exp(z) keeps the result in a normal range.
885///
886/// Supports negative ν via the same reflection formula as [`besselk`].
887///
888/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
889///
890/// # Example
891///
892/// ```
893/// use complex_bessel::besselk_scaled;
894/// use num_complex::Complex;
895///
896/// let z = Complex::new(500.0_f64, 0.0);
897///
898/// // K_0(500) would underflow to 0, but the scaled version stays finite:
899/// let k_s = besselk_scaled(0.0, z).unwrap();
900/// assert!((k_s.re - 0.0560).abs() < 1e-3); // exp(500) * K_0(500) ≈ 0.0560
901/// ```
902///
903/// # Errors
904///
905/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
906/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
907/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
908/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
909#[inline]
910pub fn besselk_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
911 besselk_internal(nu, z, Scaling::Exponential)
912}
913
914/// Scaled Hankel function of the first kind, exp(−iz) · H<sub>ν</sub><sup>(1)</sup>(z).
915///
916/// H^(1) grows exponentially in the lower half-plane;
917/// the scaling factor removes this growth, preventing overflow.
918///
919/// Supports negative ν via the same reflection formula as [`hankel1`].
920///
921/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
922///
923/// # Example
924///
925/// ```
926/// use complex_bessel::hankel1_scaled;
927/// use num_complex::Complex;
928///
929/// let z = Complex::new(1.0_f64, 0.0);
930/// let h_s = hankel1_scaled(0.0, z).unwrap();
931/// assert!((h_s.re - 0.4877).abs() < 1e-3); // exp(-i) * H^(1)_0(1), Re ≈ 0.4877
932/// ```
933///
934/// # Errors
935///
936/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
937/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
938/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
939/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
940#[inline]
941pub fn hankel1_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
942 hankel_internal(HankelKind::First, nu, z, Scaling::Exponential)
943}
944
945/// Scaled Hankel function of the second kind, exp(iz) · H<sub>ν</sub><sup>(2)</sup>(z).
946///
947/// H^(2) grows exponentially in the upper half-plane;
948/// the scaling factor removes this growth, preventing overflow.
949///
950/// Supports negative ν via the same reflection formula as [`hankel2`].
951///
952/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
953///
954/// # Example
955///
956/// ```
957/// use complex_bessel::hankel2_scaled;
958/// use num_complex::Complex;
959///
960/// let z = Complex::new(1.0_f64, 0.0);
961/// let h_s = hankel2_scaled(0.0, z).unwrap();
962/// assert!((h_s.re - 0.4877).abs() < 1e-3); // exp(i) * H^(2)_0(1), Re ≈ 0.4877
963/// ```
964///
965/// # Errors
966///
967/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
968/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
969/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
970/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
971#[inline]
972pub fn hankel2_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
973 hankel_internal(HankelKind::Second, nu, z, Scaling::Exponential)
974}
975
976/// Scaled Airy function, exp(ζ) · Ai(z), where ζ = (2/3) z√z.
977///
978/// Ai(z) decays super-exponentially for large positive real z.
979/// The scaling factor exp(ζ) keeps the result representable.
980///
981/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
982///
983/// # Example
984///
985/// ```
986/// use complex_bessel::airy_scaled;
987/// use num_complex::Complex;
988///
989/// let z = Complex::new(5.0_f64, 0.0);
990/// let ai_s = airy_scaled(z).unwrap();
991/// assert!((ai_s.re - 0.1870).abs() < 1e-3); // exp(ζ) * Ai(5) ≈ 0.1870
992/// ```
993///
994/// # Errors
995///
996/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
997/// - [`Error::Overflow`] if |z| is too large for a finite result.
998/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
999/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1000#[inline]
1001pub fn airy_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
1002 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Value, Scaling::Exponential)?;
1003 Ok(result)
1004}
1005
1006/// Scaled derivative of the Airy function, exp(ζ) · Ai'(z), where ζ = (2/3) z√z.
1007///
1008/// Ai'(z) decays super-exponentially for large positive real z, just as Ai(z) does.
1009/// The scaling factor exp(ζ) keeps the result representable.
1010///
1011/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
1012///
1013/// # Example
1014///
1015/// ```
1016/// use complex_bessel::airyprime_scaled;
1017/// use num_complex::Complex;
1018///
1019/// let z = Complex::new(5.0_f64, 0.0);
1020/// let ai_s = airyprime_scaled(z).unwrap();
1021/// assert!((ai_s.re + 0.4270).abs() < 1e-3); // exp(ζ) * Ai'(5) ≈ -0.4270
1022/// ```
1023///
1024/// # Errors
1025///
1026/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1027/// - [`Error::Overflow`] if |z| is too large for a finite result.
1028/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1029/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1030#[inline]
1031pub fn airyprime_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
1032 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Derivative, Scaling::Exponential)?;
1033 Ok(result)
1034}
1035
1036/// Scaled Airy function of the second kind, exp(−|Re(ζ)|) · Bi(z), where ζ = (2/3) z√z.
1037///
1038/// Bi(z) grows super-exponentially for large positive real z.
1039/// The scaling factor exp(−|Re(ζ)|) keeps the result representable.
1040///
1041/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
1042///
1043/// # Example
1044///
1045/// ```
1046/// use complex_bessel::biry_scaled;
1047/// use num_complex::Complex;
1048///
1049/// let z = Complex::new(5.0_f64, 0.0);
1050/// let bi_s = biry_scaled(z).unwrap();
1051/// assert!((bi_s.re - 0.3811).abs() < 1e-3); // exp(-|Re(ζ)|) * Bi(5) ≈ 0.3811
1052/// ```
1053///
1054/// # Errors
1055///
1056/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1057/// - [`Error::Overflow`] if |z| is too large for a finite result.
1058/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1059/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1060#[inline]
1061pub fn biry_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
1062 let (result, _status) = airy::zbiry(z, AiryDerivative::Value, Scaling::Exponential)?;
1063 Ok(result)
1064}
1065
1066/// Scaled derivative of the Airy function of the second kind, exp(−|Re(ζ)|) · Bi'(z), where ζ = (2/3) z√z.
1067///
1068/// Bi'(z) grows super-exponentially for large positive real z, just as Bi(z) does.
1069/// The scaling factor exp(−|Re(ζ)|) keeps the result representable.
1070///
1071/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
1072///
1073/// # Example
1074///
1075/// ```
1076/// use complex_bessel::biryprime_scaled;
1077/// use num_complex::Complex;
1078///
1079/// let z = Complex::new(5.0_f64, 0.0);
1080/// let bi_s = biryprime_scaled(z).unwrap();
1081/// assert!((bi_s.re - 0.8319).abs() < 1e-3); // exp(-|Re(ζ)|) * Bi'(5) ≈ 0.8319
1082/// ```
1083///
1084/// # Errors
1085///
1086/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1087/// - [`Error::Overflow`] if |z| is too large for a finite result.
1088/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1089/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1090#[inline]
1091pub fn biryprime_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
1092 let (result, _status) = airy::zbiry(z, AiryDerivative::Derivative, Scaling::Exponential)?;
1093 Ok(result)
1094}
1095
1096// ── Airy _raw functions (expose Accuracy) ──
1097
1098/// Airy function Ai(z) with accuracy status.
1099///
1100/// Like [`airy`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1101/// - [`Accuracy::Normal`] — no significant precision loss
1102/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1103///
1104/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1105/// see [crate-level docs](crate#exponential-scaling) for details.
1106///
1107/// # Example
1108///
1109/// ```
1110/// use complex_bessel::*;
1111/// use num_complex::Complex;
1112///
1113/// let z = Complex::new(0.0_f64, 0.0);
1114/// let result = airy_raw(z, Scaling::Unscaled).unwrap();
1115/// assert!((result.value.re - 0.3550).abs() < 1e-3); // Ai(0) ≈ 0.3550
1116/// assert!(matches!(result.status, Accuracy::Normal));
1117/// ```
1118///
1119/// # Errors
1120///
1121/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1122/// - [`Error::Overflow`] if |z| is too large for a finite result.
1123/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1124/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1125#[inline]
1126pub fn airy_raw<T: BesselFloat>(z: Complex<T>, scaling: Scaling) -> Result<AiryResult<T>, Error> {
1127 let (value, _nz, status) = airy::zairy(z, AiryDerivative::Value, scaling)?;
1128 Ok(AiryResult { value, status })
1129}
1130
1131/// Derivative of the Airy function Ai'(z) with accuracy status.
1132///
1133/// Like [`airyprime`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1134/// - [`Accuracy::Normal`] — no significant precision loss
1135/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1136///
1137/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1138/// see [crate-level docs](crate#exponential-scaling) for details.
1139///
1140/// # Example
1141///
1142/// ```
1143/// use complex_bessel::*;
1144/// use num_complex::Complex;
1145///
1146/// let z = Complex::new(0.0_f64, 0.0);
1147/// let result = airyprime_raw(z, Scaling::Unscaled).unwrap();
1148/// assert!((result.value.re + 0.2588).abs() < 1e-3); // Ai'(0) ≈ -0.2588
1149/// assert!(matches!(result.status, Accuracy::Normal));
1150/// ```
1151///
1152/// # Errors
1153///
1154/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1155/// - [`Error::Overflow`] if |z| is too large for a finite result.
1156/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1157/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1158#[inline]
1159pub fn airyprime_raw<T: BesselFloat>(
1160 z: Complex<T>,
1161 scaling: Scaling,
1162) -> Result<AiryResult<T>, Error> {
1163 let (value, _nz, status) = airy::zairy(z, AiryDerivative::Derivative, scaling)?;
1164 Ok(AiryResult { value, status })
1165}
1166
1167/// Airy function of the second kind Bi(z) with accuracy status.
1168///
1169/// Like [`biry`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1170/// - [`Accuracy::Normal`] — no significant precision loss
1171/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1172///
1173/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1174/// see [crate-level docs](crate#exponential-scaling) for details.
1175///
1176/// # Example
1177///
1178/// ```
1179/// use complex_bessel::*;
1180/// use num_complex::Complex;
1181///
1182/// let z = Complex::new(0.0_f64, 0.0);
1183/// let result = biry_raw(z, Scaling::Unscaled).unwrap();
1184/// assert!((result.value.re - 0.6149).abs() < 1e-3); // Bi(0) ≈ 0.6149
1185/// assert!(matches!(result.status, Accuracy::Normal));
1186/// ```
1187///
1188/// # Errors
1189///
1190/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1191/// - [`Error::Overflow`] if |z| is too large for a finite result.
1192/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1193/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1194#[inline]
1195pub fn biry_raw<T: BesselFloat>(z: Complex<T>, scaling: Scaling) -> Result<AiryResult<T>, Error> {
1196 let (value, status) = airy::zbiry(z, AiryDerivative::Value, scaling)?;
1197 Ok(AiryResult { value, status })
1198}
1199
1200/// Derivative of the Airy function of the second kind Bi'(z) with accuracy status.
1201///
1202/// Like [`biryprime`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1203/// - [`Accuracy::Normal`] — no significant precision loss
1204/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1205///
1206/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1207/// see [crate-level docs](crate#exponential-scaling) for details.
1208///
1209/// # Example
1210///
1211/// ```
1212/// use complex_bessel::*;
1213/// use num_complex::Complex;
1214///
1215/// let z = Complex::new(0.0_f64, 0.0);
1216/// let result = biryprime_raw(z, Scaling::Unscaled).unwrap();
1217/// assert!((result.value.re - 0.4483).abs() < 1e-3); // Bi'(0) ≈ 0.4483
1218/// assert!(matches!(result.status, Accuracy::Normal));
1219/// ```
1220///
1221/// # Errors
1222///
1223/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1224/// - [`Error::Overflow`] if |z| is too large for a finite result.
1225/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1226/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1227#[inline]
1228pub fn biryprime_raw<T: BesselFloat>(
1229 z: Complex<T>,
1230 scaling: Scaling,
1231) -> Result<AiryResult<T>, Error> {
1232 let (value, status) = airy::zbiry(z, AiryDerivative::Derivative, scaling)?;
1233 Ok(AiryResult { value, status })
1234}
1235
1236// ── Sequence functions ──
1237
1238/// Take the worse of two statuses (Reduced > Normal).
1239#[inline]
1240fn worse_status(a: Accuracy, b: Accuracy) -> Accuracy {
1241 match (a, b) {
1242 (Accuracy::Reduced, _) | (_, Accuracy::Reduced) => Accuracy::Reduced,
1243 _ => Accuracy::Normal,
1244 }
1245}
1246
1247/// Underflow count of a reflected sequence: its zero elements. Underflow
1248/// can be at either end (large |orders| below zero, high orders above), so
1249/// all of them are counted (Amos also counts the exact zeros at z = 0).
1250#[inline]
1251fn recount_underflow<T: BesselFloat>(values: &[Complex<T>]) -> usize {
1252 let zero = Complex::new(T::zero(), T::zero());
1253 values.iter().filter(|v| **v == zero).count()
1254}
1255
1256/// Compute the number of negative-order elements (before crossing zero).
1257/// For orders ν, ν+1, …, ν+n−1, this is the count where ν+j < 0.
1258#[inline]
1259fn neg_count<T: BesselFloat>(nu: T, n: usize) -> usize {
1260 if nu >= T::zero() {
1261 return 0;
1262 }
1263 // Number of j in [0,n) with nu + j < 0
1264 let abs_nu = nu.abs();
1265 // ceil(|nu|) gives the first j where nu+j >= 0
1266 let c = abs_nu.ceil();
1267 let cn = if let Some(v) = c.to_usize() { v } else { n };
1268 cn.min(n)
1269}
1270
1271/// Upper-interface signature shared by the sequence helpers below:
1272/// `f(start, buf)` fills `buf` with the function at orders start, start+1, ….
1273type SeqFn<'a, T> = dyn FnMut(T, &mut [Complex<T>]) -> Result<(usize, Accuracy), Error> + 'a;
1274
1275// ── Negative-order sequences ──
1276//
1277// Orders ν, ν+1, …, ν+n−1 with ν < 0 are computed at |order| and reflected.
1278// Everything happens in `out`; only the secondary function of the non-integer
1279// reflection formulas (none for half-integer J and Y, where cos(νπ) = 0) and
1280// Y's K values need a workspace: the full length on
1281// the heap for `_seq`, or a fixed stack buffer filled in chunks for `_seq_into`.
1282
1283/// Set `out[j]` = f(|ν+j|) when the |orders| form a single lattice: all
1284/// orders negative (nc == n), or ν an integer whose orders cross zero.
1285///
1286/// For a crossing, the orders 0..=max(|ν|, ν+n−1) always fit in `out`
1287/// (n = nc + number of orders ≥ 0), so one call computes them in place and
1288/// they are mirrored into position.
1289#[allow(clippy::needless_range_loop)]
1290fn fill_abs_orders<T: BesselFloat>(
1291 out: &mut [Complex<T>],
1292 nu: T,
1293 nc: usize,
1294 f: &mut SeqFn<'_, T>,
1295) -> Result<Accuracy, Error> {
1296 let n = out.len();
1297 if nc == n {
1298 // |orders| = |ν|-(n-1), …, |ν| ascending, then reversed into place
1299 let start = nu.abs() - T::from_f64((n - 1) as f64);
1300 let (_, status) = f(start, out)?;
1301 out.reverse();
1302 return Ok(status);
1303 }
1304 // Integer crossing, e.g. ν=-2, n=5 → orders -2,-1,0,1,2; nc = |ν|
1305 let pos_n = n - nc; // orders 0, 1, …, pos_n-1
1306 if pos_n > nc {
1307 // Largest |order| is positive: compute 0..pos_n into the positive part,
1308 // then mirror f(nc-j) into out[j]
1309 let (_, status) = f(T::zero(), &mut out[nc..])?;
1310 for j in 0..nc {
1311 out[j] = out[2 * nc - j];
1312 }
1313 Ok(status)
1314 } else {
1315 // Largest |order| is |ν|: compute 0..=nc, reverse so out[j] = f(nc-j),
1316 // then mirror f(m) into the positive tail
1317 let (_, status) = f(T::zero(), &mut out[..=nc])?;
1318 out[..=nc].reverse();
1319 for m in 1..pos_n {
1320 out[nc + m] = out[nc - m];
1321 }
1322 Ok(status)
1323 }
1324}
1325
1326/// Set `out[..nc]` = f(|ν+j|) and `out[nc..]` = f(ν+j) for a non-integer ν
1327/// whose orders cross zero: two lattices, two calls.
1328fn fill_non_int_crossing<T: BesselFloat>(
1329 out: &mut [Complex<T>],
1330 nu: T,
1331 nc: usize,
1332 f: &mut SeqFn<'_, T>,
1333) -> Result<Accuracy, Error> {
1334 let abs_nu = nu.abs();
1335 // Negative part: |orders| frac, frac+1, …, |ν| ascending, then reversed
1336 let frac_neg = abs_nu - abs_nu.floor();
1337 let (_, status1) = f(frac_neg, &mut out[..nc])?;
1338 out[..nc].reverse();
1339 // Positive part: orders ν+nc, …, ν+n-1
1340 let pos_start = nu + T::from_f64(nc as f64);
1341 let (_, status2) = f(pos_start, &mut out[nc..])?;
1342 Ok(worse_status(status1, status2))
1343}
1344
1345/// Apply a two-function reflection formula to the negative non-integer orders
1346/// −(|ν| − j), j = 0, …, len−1, stored in `seg`.
1347///
1348/// Their |orders| form the lattice lo, lo+1, …, |ν| with lo = |ν| − (len−1),
1349/// computed in ascending order and then reversed. `whole` computes one
1350/// function for the whole lattice into `seg`; `chunked` computes the other
1351/// into `buf`, `buf.len()` orders at a time. With `chunked_first`, the first
1352/// chunk is computed before `whole` (to keep the call order of the formula).
1353/// `combine(|order|, whole, chunked)` returns the reflected value.
1354fn reflect_lattice<T: BesselFloat>(
1355 seg: &mut [Complex<T>],
1356 abs_nu: T,
1357 chunked_first: bool,
1358 whole: &mut SeqFn<'_, T>,
1359 chunked: &mut SeqFn<'_, T>,
1360 combine: &dyn Fn(T, Complex<T>, Complex<T>) -> Complex<T>,
1361 buf: &mut [Complex<T>],
1362) -> Result<Accuracy, Error> {
1363 let len = seg.len();
1364 let chunk = buf.len();
1365 assert!(chunk > 0, "empty workspace");
1366 // |ν| minus an integer below it stays on the ulp grid of |ν|: exact
1367 let lo = abs_nu - T::from_f64((len - 1) as f64);
1368 let mut status = Accuracy::Normal;
1369 let mut whole_done = false;
1370 if !chunked_first {
1371 let (_, s) = whole(lo, seg)?;
1372 status = s;
1373 whole_done = true;
1374 }
1375 let mut c0 = 0;
1376 while c0 < len {
1377 let c1 = (c0 + chunk).min(len);
1378 // Exact for the same reason as `lo`
1379 let start = lo + T::from_f64(c0 as f64);
1380 let (_, s) = chunked(start, &mut buf[..c1 - c0])?;
1381 status = worse_status(status, s);
1382 if !whole_done {
1383 let (_, s) = whole(lo, seg)?;
1384 status = worse_status(status, s);
1385 whole_done = true;
1386 }
1387 for k in c0..c1 {
1388 let order = abs_nu - T::from_f64((len - 1 - k) as f64);
1389 seg[k] = combine(order, seg[k], buf[k - c0]);
1390 }
1391 c0 = c1;
1392 }
1393 seg.reverse();
1394 Ok(status)
1395}
1396
1397/// Multiply the negative integer orders by (−1)^|order| (J and Y).
1398fn apply_integer_sign<T: BesselFloat>(out: &mut [Complex<T>], nu: T) {
1399 for (j, v) in out.iter_mut().enumerate() {
1400 let order = nu + T::from_f64(j as f64);
1401 if order < T::zero() {
1402 let int_order = order.abs().to_i64().unwrap_or(0);
1403 *v = *v * integer_sign::<T>(int_order);
1404 }
1405 }
1406}
1407
1408/// `n = 0` would underflow the `n - 1` order arithmetic of the helpers above.
1409#[inline]
1410fn check_nonempty<T>(out: &[T]) -> Result<(), Error> {
1411 if out.is_empty() {
1412 Err(Error::InvalidInput)
1413 } else {
1414 Ok(())
1415 }
1416}
1417
1418/// K with ν < 0: K_{−ν} = K_ν (even function).
1419fn besselk_seq_neg<T: BesselFloat>(
1420 nu: T,
1421 z: Complex<T>,
1422 scaling: Scaling,
1423 out: &mut [Complex<T>],
1424) -> Result<SeqInfo, Error> {
1425 check_nonempty(out)?;
1426 let nc = neg_count(nu, out.len());
1427 let f = &mut |start, buf: &mut [Complex<T>]| besk::zbesk(z, start, scaling, buf);
1428 let status = if nc == out.len() || as_integer(nu.abs()).is_some() {
1429 fill_abs_orders(out, nu, nc, f)?
1430 } else {
1431 fill_non_int_crossing(out, nu, nc, f)?
1432 };
1433 Ok(SeqInfo {
1434 underflow_count: recount_underflow(out),
1435 status,
1436 })
1437}
1438
1439/// H with ν < 0: H^(1)_{−ν} = exp(νπi)·H^(1)_ν, H^(2)_{−ν} = exp(−νπi)·H^(2)_ν.
1440fn hankel_seq_neg<T: BesselFloat>(
1441 kind: HankelKind,
1442 nu: T,
1443 z: Complex<T>,
1444 scaling: Scaling,
1445 out: &mut [Complex<T>],
1446) -> Result<SeqInfo, Error> {
1447 check_nonempty(out)?;
1448 let abs_nu = nu.abs();
1449 let nc = neg_count(nu, out.len());
1450 let f = &mut |start, buf: &mut [Complex<T>]| besh::zbesh(z, start, kind, scaling, buf);
1451 let status = if nc == out.len() || as_integer(abs_nu).is_some() {
1452 fill_abs_orders(out, nu, nc, f)?
1453 } else {
1454 fill_non_int_crossing(out, nu, nc, f)?
1455 };
1456 // Rotate the negative orders; |ν+j| = |ν| - j exactly
1457 for (j, v) in out.iter_mut().take(nc).enumerate() {
1458 let order = abs_nu - T::from_f64(j as f64);
1459 *v = reflect_h_element(order, kind, *v);
1460 }
1461 Ok(SeqInfo {
1462 underflow_count: recount_underflow(out),
1463 status,
1464 })
1465}
1466
1467/// J with ν < 0: J_{−ν} = cos(νπ)·J_ν − sin(νπ)·Y_ν, J_{−n} = (−1)^n·J_n.
1468fn besselj_seq_neg<T: BesselFloat>(
1469 nu: T,
1470 z: Complex<T>,
1471 scaling: Scaling,
1472 out: &mut [Complex<T>],
1473 ws: Workspace<'_, T>,
1474) -> Result<SeqInfo, Error> {
1475 check_nonempty(out)?;
1476 let n = out.len();
1477 let nc = neg_count(nu, n);
1478 let status = if as_integer(nu.abs()).is_some() {
1479 let f = &mut |start, buf: &mut [Complex<T>]| besj::zbesj(z, start, scaling, buf);
1480 let status = fill_abs_orders(out, nu, nc, f)?;
1481 apply_integer_sign(out, nu);
1482 status
1483 } else if utils::cospi(nu.abs()) == T::zero() {
1484 // Half-integer |ν|: cos(νπ) = 0, so the negative orders need only Y
1485 // (into `out`, its K values into the workspace)
1486 with_workspace(ws, n, |ws| {
1487 let abs_nu = nu.abs();
1488 let czero = Complex::new(T::zero(), T::zero());
1489 let (neg, pos) = out.split_at_mut(nc);
1490 let status1 = fill_abs_orders(neg, nu, nc, &mut |start, buf| {
1491 besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *ws))
1492 })?;
1493 for (j, v) in neg.iter_mut().enumerate() {
1494 *v = reflect_j_element(abs_nu - T::from_f64(j as f64), czero, *v);
1495 }
1496 if pos.is_empty() {
1497 return Ok(status1);
1498 }
1499 let (_, status2) = besj::zbesj(z, nu + T::from_f64(nc as f64), scaling, pos)?;
1500 Ok(worse_status(status1, status2))
1501 })?
1502 } else {
1503 // J into `out`, Y into one half of the workspace and Y's K values
1504 // into the other (J first, as before)
1505 with_workspace(ws, 2 * n, |ws| {
1506 let (neg, pos) = out.split_at_mut(nc);
1507 let (y_buf, k_buf) = ws.split_at_mut(ws.len() / 2);
1508 let status1 = reflect_lattice(
1509 neg,
1510 nu.abs(),
1511 false,
1512 &mut |start, buf| besj::zbesj(z, start, scaling, buf),
1513 &mut |start, buf| {
1514 besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *k_buf))
1515 },
1516 &|order, j, y| reflect_j_element(order, j, y),
1517 y_buf,
1518 )?;
1519 if pos.is_empty() {
1520 return Ok(status1);
1521 }
1522 let (_, status2) = besj::zbesj(z, nu + T::from_f64(nc as f64), scaling, pos)?;
1523 Ok(worse_status(status1, status2))
1524 })?
1525 };
1526 Ok(SeqInfo {
1527 underflow_count: recount_underflow(out),
1528 status,
1529 })
1530}
1531
1532/// Y with ν < 0: Y_{−ν} = sin(νπ)·J_ν + cos(νπ)·Y_ν, Y_{−n} = (−1)^n·Y_n.
1533fn bessely_seq_neg<T: BesselFloat>(
1534 nu: T,
1535 z: Complex<T>,
1536 scaling: Scaling,
1537 out: &mut [Complex<T>],
1538 ws: Workspace<'_, T>,
1539) -> Result<SeqInfo, Error> {
1540 check_nonempty(out)?;
1541 let n = out.len();
1542 let nc = neg_count(nu, n);
1543 let is_int = as_integer(nu.abs()).is_some();
1544 // Half-integer |ν|: cos(νπ) = 0, so the negative orders need only J
1545 let half_int = !is_int && utils::cospi(nu.abs()) == T::zero();
1546 // Heap: Y's K values only, or also the Y values of the reflection formula
1547 let ws_len = if is_int || half_int { n } else { 2 * n };
1548 let status = with_workspace(ws, ws_len, |ws| {
1549 if is_int {
1550 let f = &mut |start, buf: &mut [Complex<T>]| {
1551 besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *ws))
1552 };
1553 let status = fill_abs_orders(out, nu, nc, f)?;
1554 apply_integer_sign(out, nu);
1555 return Ok(status);
1556 }
1557 let (neg, pos) = out.split_at_mut(nc);
1558 let status1 = if half_int {
1559 let abs_nu = nu.abs();
1560 let czero = Complex::new(T::zero(), T::zero());
1561 let status = fill_abs_orders(neg, nu, nc, &mut |start, buf| {
1562 besj::zbesj(z, start, scaling, buf)
1563 })?;
1564 for (j, v) in neg.iter_mut().enumerate() {
1565 *v = reflect_y_element(abs_nu - T::from_f64(j as f64), *v, czero);
1566 }
1567 status
1568 } else {
1569 // J into `out`, Y into one half of the workspace and Y's K values
1570 // into the other (J first, as before)
1571 let (y_buf, k_buf) = ws.split_at_mut(ws.len() / 2);
1572 reflect_lattice(
1573 neg,
1574 nu.abs(),
1575 false,
1576 &mut |start, buf| besj::zbesj(z, start, scaling, buf),
1577 &mut |start, buf| {
1578 besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *k_buf))
1579 },
1580 &|order, j, y| reflect_y_element(order, j, y),
1581 y_buf,
1582 )?
1583 };
1584 if pos.is_empty() {
1585 return Ok(status1);
1586 }
1587 let pos_start = nu + T::from_f64(nc as f64);
1588 let (_, status2) = besy::zbesy_with(z, pos_start, scaling, pos, Workspace::Slice(ws))?;
1589 Ok(worse_status(status1, status2))
1590 })?;
1591 Ok(SeqInfo {
1592 underflow_count: recount_underflow(out),
1593 status,
1594 })
1595}
1596
1597/// I with ν < 0: I_{−ν} = I_ν + (2/π)·sin(νπ)·K_ν, I_{−n} = I_n.
1598fn besseli_seq_neg<T: BesselFloat>(
1599 nu: T,
1600 z: Complex<T>,
1601 scaling: Scaling,
1602 out: &mut [Complex<T>],
1603 ws: Workspace<'_, T>,
1604) -> Result<SeqInfo, Error> {
1605 check_nonempty(out)?;
1606 let n = out.len();
1607 let nc = neg_count(nu, n);
1608 let i_fn = &mut |start, buf: &mut [Complex<T>]| besi::zbesi(z, start, scaling, buf);
1609 let status = if as_integer(nu.abs()).is_some() {
1610 fill_abs_orders(out, nu, nc, i_fn)?
1611 } else {
1612 // K into `out` in one call, I into the workspace: Amos's K overflow
1613 // test uses the last order and length of the call, so splitting K
1614 // could fail where the whole call succeeds, while I's tests only get
1615 // more lenient in later chunks. The first I chunk goes first to keep
1616 // the I-then-K order.
1617 with_workspace(ws, n, |ws| {
1618 let (neg, pos) = out.split_at_mut(nc);
1619 let status1 = reflect_lattice(
1620 neg,
1621 nu.abs(),
1622 true,
1623 &mut |start, buf| besk::zbesk(z, start, scaling, buf),
1624 i_fn,
1625 &|order, k, i| {
1626 let k = if scaling == Scaling::Exponential {
1627 k_to_i_scaling_correction(z, k)
1628 } else {
1629 k
1630 };
1631 reflect_i_element(order, i, k)
1632 },
1633 ws,
1634 )?;
1635 if pos.is_empty() {
1636 return Ok(status1);
1637 }
1638 let (_, status2) = besi::zbesi(z, nu + T::from_f64(nc as f64), scaling, pos)?;
1639 Ok(worse_status(status1, status2))
1640 })?
1641 };
1642 Ok(SeqInfo {
1643 underflow_count: recount_underflow(out),
1644 status,
1645 })
1646}
1647
1648// ── Shared by `_seq_into` (stack workspace) and `_seq` (heap workspace) ──
1649
1650fn besselj_seq_impl<T: BesselFloat>(
1651 nu: T,
1652 z: Complex<T>,
1653 scaling: Scaling,
1654 out: &mut [Complex<T>],
1655 ws: Workspace<'_, T>,
1656) -> Result<SeqInfo, Error> {
1657 let info = if nu < T::zero() {
1658 besselj_seq_neg(nu, z, scaling, out, ws)?
1659 } else {
1660 let (underflow_count, status) = besj::zbesj(z, nu, scaling, out)?;
1661 SeqInfo {
1662 underflow_count,
1663 status,
1664 }
1665 };
1666 for v in out.iter_mut() {
1667 *v = axis::real_on_positive_axis(z, *v);
1668 }
1669 Ok(info)
1670}
1671
1672fn bessely_seq_impl<T: BesselFloat>(
1673 nu: T,
1674 z: Complex<T>,
1675 scaling: Scaling,
1676 out: &mut [Complex<T>],
1677 ws: Workspace<'_, T>,
1678) -> Result<SeqInfo, Error> {
1679 let info = if nu < T::zero() {
1680 bessely_seq_neg(nu, z, scaling, out, ws)?
1681 } else {
1682 let (underflow_count, status) = besy::zbesy_with(z, nu, scaling, out, ws)?;
1683 SeqInfo {
1684 underflow_count,
1685 status,
1686 }
1687 };
1688 for v in out.iter_mut() {
1689 *v = axis::real_on_positive_axis(z, *v);
1690 }
1691 axis::fix_seq(axis::AxisFn::Y, nu, z, scaling, out);
1692 Ok(info)
1693}
1694
1695fn besseli_seq_impl<T: BesselFloat>(
1696 nu: T,
1697 z: Complex<T>,
1698 scaling: Scaling,
1699 out: &mut [Complex<T>],
1700 ws: Workspace<'_, T>,
1701) -> Result<SeqInfo, Error> {
1702 if nu < T::zero() {
1703 return besseli_seq_neg(nu, z, scaling, out, ws);
1704 }
1705 let (underflow_count, status) = besi::zbesi(z, nu, scaling, out)?;
1706 Ok(SeqInfo {
1707 underflow_count,
1708 status,
1709 })
1710}
1711
1712fn hankel_seq_impl<T: BesselFloat>(
1713 kind: HankelKind,
1714 nu: T,
1715 z: Complex<T>,
1716 scaling: Scaling,
1717 out: &mut [Complex<T>],
1718) -> Result<SeqInfo, Error> {
1719 let info = if nu < T::zero() {
1720 hankel_seq_neg(kind, nu, z, scaling, out)?
1721 } else {
1722 let (underflow_count, status) = besh::zbesh(z, nu, kind, scaling, out)?;
1723 SeqInfo {
1724 underflow_count,
1725 status,
1726 }
1727 };
1728 axis::fix_seq(axis::AxisFn::Hankel(kind), nu, z, scaling, out);
1729 Ok(info)
1730}
1731
1732/// Compute J<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 into `out`, where n = `out.len()`.
1733///
1734/// Like [`besselj_seq`], but writes into a caller-provided slice instead of
1735/// allocating, so it also works without an allocator (`no_std`). Returns a
1736/// [`SeqInfo`] with the underflow count and [`Accuracy`]. If an error is
1737/// returned, the contents of `out` are unspecified.
1738///
1739/// For negative non-integer ν, the Y values of the reflection formula need
1740/// workspace. It is a 1 KB stack buffer (f64), so long sequences are computed
1741/// in chunks. That is somewhat slower than [`besselj_seq`], and the last digits
1742/// can differ (within the accuracy of the single-value functions); close to
1743/// the overflow limit, one of the two can even return an error where the
1744/// other succeeds.
1745///
1746/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1747/// see [crate-level docs](crate#exponential-scaling) for details.
1748///
1749/// Supports negative ν via the same reflection formula as [`besselj`].
1750///
1751/// # Example
1752///
1753/// ```
1754/// use complex_bessel::*;
1755/// use num_complex::Complex;
1756///
1757/// let z = Complex::new(1.0_f64, 0.0);
1758///
1759/// // J_0(z), J_1(z), J_2(z) in one call
1760/// let mut out = [Complex::new(0.0, 0.0); 3];
1761/// let info = besselj_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
1762/// assert!((out[0].re - 0.7652).abs() < 1e-3); // J_0(1) ≈ 0.7652
1763/// assert_eq!(info.status, Accuracy::Normal);
1764/// ```
1765///
1766/// # Errors
1767///
1768/// - [`Error::InvalidInput`] if `out` is empty, if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
1769/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
1770/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1771/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1772pub fn besselj_seq_into<T: BesselFloat>(
1773 nu: T,
1774 z: Complex<T>,
1775 scaling: Scaling,
1776 out: &mut [Complex<T>],
1777) -> Result<SeqInfo, Error> {
1778 besselj_seq_impl(nu, z, scaling, out, Workspace::Stack)
1779}
1780
1781/// Compute Y<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 into `out`, where n = `out.len()`.
1782///
1783/// Like [`bessely_seq`], but writes into a caller-provided slice instead of
1784/// allocating, so it also works without an allocator (`no_std`). Returns a
1785/// [`SeqInfo`] with the underflow count and [`Accuracy`]. If an error is
1786/// returned, the contents of `out` are unspecified.
1787///
1788/// The K values that Y is computed from (and, for negative non-integer ν, the
1789/// second function of the reflection formula) need workspace. It is a 1 KB
1790/// stack buffer (f64), so long sequences are computed in chunks. That is
1791/// somewhat slower than [`bessely_seq`], and the last digits can differ
1792/// (within the accuracy of the single-value functions); close to the overflow
1793/// limit, one of the two can even return an error where the other succeeds.
1794///
1795/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1796/// see [crate-level docs](crate#exponential-scaling) for details.
1797///
1798/// Supports negative ν via the same reflection formula as [`bessely`].
1799///
1800/// # Example
1801///
1802/// ```
1803/// use complex_bessel::*;
1804/// use num_complex::Complex;
1805///
1806/// let z = Complex::new(1.0_f64, 0.0);
1807///
1808/// // Y_0(z), Y_1(z), Y_2(z) in one call
1809/// let mut out = [Complex::new(0.0, 0.0); 3];
1810/// let info = bessely_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
1811/// assert!((out[0].re - 0.0883).abs() < 1e-3); // Y_0(1) ≈ 0.0883
1812/// assert_eq!(info.status, Accuracy::Normal);
1813/// ```
1814///
1815/// # Errors
1816///
1817/// - [`Error::InvalidInput`] if `out` is empty, if z or ν is not finite (NaN or infinite), or if z = 0 (unless every order is a negative half-integer, where Y_ν(0) = 0).
1818/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1819/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1820/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1821pub fn bessely_seq_into<T: BesselFloat>(
1822 nu: T,
1823 z: Complex<T>,
1824 scaling: Scaling,
1825 out: &mut [Complex<T>],
1826) -> Result<SeqInfo, Error> {
1827 bessely_seq_impl(nu, z, scaling, out, Workspace::Stack)
1828}
1829
1830/// Compute I<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 into `out`, where n = `out.len()`.
1831///
1832/// Like [`besseli_seq`], but writes into a caller-provided slice instead of
1833/// allocating, so it also works without an allocator (`no_std`). Returns a
1834/// [`SeqInfo`] with the underflow count and [`Accuracy`]. If an error is
1835/// returned, the contents of `out` are unspecified.
1836///
1837/// For negative non-integer ν, the I values of the reflection formula need
1838/// workspace. It is a 1 KB stack buffer (f64), so long sequences are computed
1839/// in chunks. That is somewhat slower than [`besseli_seq`], and the last digits
1840/// can differ (within the accuracy of the single-value functions); close to
1841/// the overflow limit, one of the two can even return an error where the
1842/// other succeeds.
1843///
1844/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1845/// see [crate-level docs](crate#exponential-scaling) for details.
1846///
1847/// Supports negative ν via the same reflection formula as [`besseli`].
1848///
1849/// # Example
1850///
1851/// ```
1852/// use complex_bessel::*;
1853/// use num_complex::Complex;
1854///
1855/// let z = Complex::new(1.0_f64, 0.0);
1856///
1857/// // I_0(z), I_1(z), I_2(z) in one call
1858/// let mut out = [Complex::new(0.0, 0.0); 3];
1859/// let info = besseli_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
1860/// assert!((out[0].re - 1.2661).abs() < 1e-3); // I_0(1) ≈ 1.2661
1861/// assert_eq!(info.status, Accuracy::Normal);
1862/// ```
1863///
1864/// # Errors
1865///
1866/// - [`Error::InvalidInput`] if `out` is empty, if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
1867/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
1868/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1869/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1870pub fn besseli_seq_into<T: BesselFloat>(
1871 nu: T,
1872 z: Complex<T>,
1873 scaling: Scaling,
1874 out: &mut [Complex<T>],
1875) -> Result<SeqInfo, Error> {
1876 besseli_seq_impl(nu, z, scaling, out, Workspace::Stack)
1877}
1878
1879/// Compute K<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 into `out`, where n = `out.len()`.
1880///
1881/// Like [`besselk_seq`], but writes into a caller-provided slice instead of
1882/// allocating, so it also works without an allocator (`no_std`). Returns a
1883/// [`SeqInfo`] with the underflow count and [`Accuracy`]. If an error is
1884/// returned, the contents of `out` are unspecified.
1885///
1886/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1887/// see [crate-level docs](crate#exponential-scaling) for details.
1888///
1889/// Supports negative ν via the same reflection formula as [`besselk`].
1890///
1891/// # Example
1892///
1893/// ```
1894/// use complex_bessel::*;
1895/// use num_complex::Complex;
1896///
1897/// let z = Complex::new(1.0_f64, 0.0);
1898///
1899/// // K_0(z), K_1(z), K_2(z) in one call
1900/// let mut out = [Complex::new(0.0, 0.0); 3];
1901/// let info = besselk_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
1902/// assert!((out[0].re - 0.4211).abs() < 1e-3); // K_0(1) ≈ 0.4211
1903/// assert_eq!(info.status, Accuracy::Normal);
1904/// ```
1905///
1906/// # Errors
1907///
1908/// - [`Error::InvalidInput`] if `out` is empty, if z or ν is not finite (NaN or infinite), or if z = 0.
1909/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1910/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1911/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1912pub fn besselk_seq_into<T: BesselFloat>(
1913 nu: T,
1914 z: Complex<T>,
1915 scaling: Scaling,
1916 out: &mut [Complex<T>],
1917) -> Result<SeqInfo, Error> {
1918 let info = if nu < T::zero() {
1919 besselk_seq_neg(nu, z, scaling, out)?
1920 } else {
1921 let (underflow_count, status) = besk::zbesk(z, nu, scaling, out)?;
1922 SeqInfo {
1923 underflow_count,
1924 status,
1925 }
1926 };
1927 axis::fix_seq(axis::AxisFn::K, nu, z, scaling, out);
1928 Ok(info)
1929}
1930
1931/// Compute H<sub>ν+j</sub><sup>(1)</sup>(z) for j = 0, 1, …, n−1 into `out`, where n = `out.len()`.
1932///
1933/// Like [`hankel1_seq`], but writes into a caller-provided slice instead of
1934/// allocating, so it also works without an allocator (`no_std`). Returns a
1935/// [`SeqInfo`] with the underflow count and [`Accuracy`]. If an error is
1936/// returned, the contents of `out` are unspecified.
1937///
1938/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1939/// see [crate-level docs](crate#exponential-scaling) for details.
1940///
1941/// Supports negative ν via the same reflection formula as [`hankel1`].
1942///
1943/// # Example
1944///
1945/// ```
1946/// use complex_bessel::*;
1947/// use num_complex::Complex;
1948///
1949/// let z = Complex::new(1.0_f64, 0.0);
1950///
1951/// // H^(1)_0(z), H^(1)_1(z) in one call
1952/// let mut out = [Complex::new(0.0, 0.0); 2];
1953/// let info = hankel1_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
1954/// assert!((out[0].im - 0.0883).abs() < 1e-3); // Im = Y_0(1) ≈ 0.0883
1955/// assert_eq!(info.status, Accuracy::Normal);
1956/// ```
1957///
1958/// # Errors
1959///
1960/// - [`Error::InvalidInput`] if `out` is empty, if z or ν is not finite (NaN or infinite), or if z = 0.
1961/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1962/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1963/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1964pub fn hankel1_seq_into<T: BesselFloat>(
1965 nu: T,
1966 z: Complex<T>,
1967 scaling: Scaling,
1968 out: &mut [Complex<T>],
1969) -> Result<SeqInfo, Error> {
1970 hankel_seq_impl(HankelKind::First, nu, z, scaling, out)
1971}
1972
1973/// Compute H<sub>ν+j</sub><sup>(2)</sup>(z) for j = 0, 1, …, n−1 into `out`, where n = `out.len()`.
1974///
1975/// Like [`hankel2_seq`], but writes into a caller-provided slice instead of
1976/// allocating, so it also works without an allocator (`no_std`). Returns a
1977/// [`SeqInfo`] with the underflow count and [`Accuracy`]. If an error is
1978/// returned, the contents of `out` are unspecified.
1979///
1980/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1981/// see [crate-level docs](crate#exponential-scaling) for details.
1982///
1983/// Supports negative ν via the same reflection formula as [`hankel2`].
1984///
1985/// # Example
1986///
1987/// ```
1988/// use complex_bessel::*;
1989/// use num_complex::Complex;
1990///
1991/// let z = Complex::new(1.0_f64, 0.0);
1992///
1993/// // H^(2)_0(z), H^(2)_1(z) in one call
1994/// let mut out = [Complex::new(0.0, 0.0); 2];
1995/// let info = hankel2_seq_into(0.0, z, Scaling::Unscaled, &mut out).unwrap();
1996/// assert!((out[0].im + 0.0883).abs() < 1e-3); // Im = -Y_0(1) ≈ -0.0883
1997/// assert_eq!(info.status, Accuracy::Normal);
1998/// ```
1999///
2000/// # Errors
2001///
2002/// - [`Error::InvalidInput`] if `out` is empty, if z or ν is not finite (NaN or infinite), or if z = 0.
2003/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
2004/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2005/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2006pub fn hankel2_seq_into<T: BesselFloat>(
2007 nu: T,
2008 z: Complex<T>,
2009 scaling: Scaling,
2010 out: &mut [Complex<T>],
2011) -> Result<SeqInfo, Error> {
2012 hankel_seq_impl(HankelKind::Second, nu, z, scaling, out)
2013}
2014
2015// ── Allocating sequence functions ──
2016
2017#[cfg(feature = "std")]
2018fn seq_helper<T: BesselFloat>(
2019 n: usize,
2020 f: impl FnOnce(&mut [Complex<T>]) -> Result<SeqInfo, Error>,
2021) -> Result<BesselResult<T>, Error> {
2022 let zero = T::zero();
2023 let mut values = alloc::vec![Complex::new(zero, zero); n];
2024 let info = f(&mut values)?;
2025 Ok(BesselResult {
2026 values,
2027 underflow_count: info.underflow_count,
2028 status: info.status,
2029 })
2030}
2031
2032#[cfg(feature = "std")]
2033/// Compute J<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
2034///
2035/// Allocating version of [`besselj_seq_into`].
2036///
2037/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
2038/// - [`Accuracy::Normal`] — no significant precision loss
2039/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
2040///
2041/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
2042/// see [crate-level docs](crate#exponential-scaling) for details.
2043///
2044/// Supports negative ν via the same reflection formula as [`besselj`].
2045///
2046/// # Example
2047///
2048/// ```
2049/// # #[cfg(feature = "std")] {
2050/// use complex_bessel::*;
2051/// use num_complex::Complex;
2052///
2053/// let z = Complex::new(1.0_f64, 0.0);
2054///
2055/// // J_0(z), J_1(z), J_2(z) in one call
2056/// let result = besselj_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
2057/// assert_eq!(result.values.len(), 3);
2058/// assert!((result.values[0].re - 0.7652).abs() < 1e-3); // J_0(1) ≈ 0.7652
2059/// # }
2060/// ```
2061///
2062/// # Errors
2063///
2064/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
2065/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
2066/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2067/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2068pub fn besselj_seq<T: BesselFloat>(
2069 nu: T,
2070 z: Complex<T>,
2071 n: usize,
2072 scaling: Scaling,
2073) -> Result<BesselResult<T>, Error> {
2074 seq_helper(n, |out| {
2075 besselj_seq_impl(nu, z, scaling, out, Workspace::Heap)
2076 })
2077}
2078
2079#[cfg(feature = "std")]
2080/// Compute Y<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
2081///
2082/// Allocating version of [`bessely_seq_into`].
2083///
2084/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
2085/// - [`Accuracy::Normal`] — no significant precision loss
2086/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
2087///
2088/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
2089/// see [crate-level docs](crate#exponential-scaling) for details.
2090///
2091/// Supports negative ν via the same reflection formula as [`bessely`].
2092///
2093/// # Example
2094///
2095/// ```
2096/// # #[cfg(feature = "std")] {
2097/// use complex_bessel::*;
2098/// use num_complex::Complex;
2099///
2100/// let z = Complex::new(1.0_f64, 0.0);
2101///
2102/// // Y_0(z), Y_1(z), Y_2(z) in one call
2103/// let result = bessely_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
2104/// assert_eq!(result.values.len(), 3);
2105/// assert!((result.values[0].re - 0.0883).abs() < 1e-3); // Y_0(1) ≈ 0.0883
2106/// # }
2107/// ```
2108///
2109/// # Errors
2110///
2111/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0 (unless every order is a negative half-integer, where Y_ν(0) = 0).
2112/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
2113/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2114/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2115pub fn bessely_seq<T: BesselFloat>(
2116 nu: T,
2117 z: Complex<T>,
2118 n: usize,
2119 scaling: Scaling,
2120) -> Result<BesselResult<T>, Error> {
2121 seq_helper(n, |out| {
2122 bessely_seq_impl(nu, z, scaling, out, Workspace::Heap)
2123 })
2124}
2125
2126#[cfg(feature = "std")]
2127/// Compute I<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
2128///
2129/// Allocating version of [`besseli_seq_into`].
2130///
2131/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
2132/// - [`Accuracy::Normal`] — no significant precision loss
2133/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
2134///
2135/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
2136/// see [crate-level docs](crate#exponential-scaling) for details.
2137///
2138/// Supports negative ν via the same reflection formula as [`besseli`].
2139///
2140/// # Example
2141///
2142/// ```
2143/// # #[cfg(feature = "std")] {
2144/// use complex_bessel::*;
2145/// use num_complex::Complex;
2146///
2147/// let z = Complex::new(1.0_f64, 0.0);
2148///
2149/// // I_0(z), I_1(z), I_2(z) in one call
2150/// let result = besseli_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
2151/// assert_eq!(result.values.len(), 3);
2152/// assert!((result.values[0].re - 1.2661).abs() < 1e-3); // I_0(1) ≈ 1.2661
2153/// # }
2154/// ```
2155///
2156/// # Errors
2157///
2158/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
2159/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
2160/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2161/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2162pub fn besseli_seq<T: BesselFloat>(
2163 nu: T,
2164 z: Complex<T>,
2165 n: usize,
2166 scaling: Scaling,
2167) -> Result<BesselResult<T>, Error> {
2168 seq_helper(n, |out| {
2169 besseli_seq_impl(nu, z, scaling, out, Workspace::Heap)
2170 })
2171}
2172
2173#[cfg(feature = "std")]
2174/// Compute K<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
2175///
2176/// Allocating version of [`besselk_seq_into`].
2177///
2178/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
2179/// - [`Accuracy::Normal`] — no significant precision loss
2180/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
2181///
2182/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
2183/// see [crate-level docs](crate#exponential-scaling) for details.
2184///
2185/// Supports negative ν via the same reflection formula as [`besselk`].
2186///
2187/// # Example
2188///
2189/// ```
2190/// # #[cfg(feature = "std")] {
2191/// use complex_bessel::*;
2192/// use num_complex::Complex;
2193///
2194/// let z = Complex::new(1.0_f64, 0.0);
2195///
2196/// // K_0(z), K_1(z), K_2(z) in one call
2197/// let result = besselk_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
2198/// assert_eq!(result.values.len(), 3);
2199/// assert!((result.values[0].re - 0.4211).abs() < 1e-3); // K_0(1) ≈ 0.4211
2200/// # }
2201/// ```
2202///
2203/// # Errors
2204///
2205/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
2206/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
2207/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2208/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2209pub fn besselk_seq<T: BesselFloat>(
2210 nu: T,
2211 z: Complex<T>,
2212 n: usize,
2213 scaling: Scaling,
2214) -> Result<BesselResult<T>, Error> {
2215 seq_helper(n, |out| besselk_seq_into(nu, z, scaling, out))
2216}
2217
2218#[cfg(feature = "std")]
2219/// Compute H<sub>ν+j</sub><sup>(1)</sup>(z) for j = 0, 1, …, n−1 in a single call.
2220///
2221/// Allocating version of [`hankel1_seq_into`].
2222///
2223/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
2224/// - [`Accuracy::Normal`] — no significant precision loss
2225/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
2226///
2227/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
2228/// see [crate-level docs](crate#exponential-scaling) for details.
2229///
2230/// Supports negative ν via the same reflection formula as [`hankel1`].
2231///
2232/// # Example
2233///
2234/// ```
2235/// # #[cfg(feature = "std")] {
2236/// use complex_bessel::*;
2237/// use num_complex::Complex;
2238///
2239/// let z = Complex::new(1.0_f64, 0.0);
2240///
2241/// // H^(1)_0(z), H^(1)_1(z) in one call
2242/// let result = hankel1_seq(0.0, z, 2, Scaling::Unscaled).unwrap();
2243/// assert_eq!(result.values.len(), 2);
2244/// assert!((result.values[0].im - 0.0883).abs() < 1e-3); // Im = Y_0(1) ≈ 0.0883
2245/// # }
2246/// ```
2247///
2248/// # Errors
2249///
2250/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
2251/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
2252/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2253/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2254pub fn hankel1_seq<T: BesselFloat>(
2255 nu: T,
2256 z: Complex<T>,
2257 n: usize,
2258 scaling: Scaling,
2259) -> Result<BesselResult<T>, Error> {
2260 seq_helper(n, |out| hankel1_seq_into(nu, z, scaling, out))
2261}
2262
2263#[cfg(feature = "std")]
2264/// Compute H<sub>ν+j</sub><sup>(2)</sup>(z) for j = 0, 1, …, n−1 in a single call.
2265///
2266/// Allocating version of [`hankel2_seq_into`].
2267///
2268/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
2269/// - [`Accuracy::Normal`] — no significant precision loss
2270/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
2271///
2272/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
2273/// see [crate-level docs](crate#exponential-scaling) for details.
2274///
2275/// Supports negative ν via the same reflection formula as [`hankel2`].
2276///
2277/// # Example
2278///
2279/// ```
2280/// # #[cfg(feature = "std")] {
2281/// use complex_bessel::*;
2282/// use num_complex::Complex;
2283///
2284/// let z = Complex::new(1.0_f64, 0.0);
2285///
2286/// // H^(2)_0(z), H^(2)_1(z) in one call
2287/// let result = hankel2_seq(0.0, z, 2, Scaling::Unscaled).unwrap();
2288/// assert_eq!(result.values.len(), 2);
2289/// assert!((result.values[0].im + 0.0883).abs() < 1e-3); // Im = -Y_0(1) ≈ -0.0883
2290/// # }
2291/// ```
2292///
2293/// # Errors
2294///
2295/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
2296/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
2297/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
2298/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
2299pub fn hankel2_seq<T: BesselFloat>(
2300 nu: T,
2301 z: Complex<T>,
2302 n: usize,
2303 scaling: Scaling,
2304) -> Result<BesselResult<T>, Error> {
2305 seq_helper(n, |out| hankel2_seq_into(nu, z, scaling, out))
2306}
2307
2308// ── Tests for non-finite input rejection at the public API level ──
2309// Deliberately gated on test only (not std), so they run in every
2310// feature configuration.
2311
2312#[cfg(test)]
2313mod non_finite_tests {
2314 use super::*;
2315 use num_complex::{Complex32, Complex64};
2316
2317 #[test]
2318 fn bessel_non_finite_z_returns_invalid_input() {
2319 let nan = f64::NAN;
2320 for z in [
2321 Complex64::new(nan, 0.0),
2322 Complex64::new(0.0, nan),
2323 Complex64::new(nan, nan),
2324 Complex64::new(f64::INFINITY, 0.0),
2325 ] {
2326 assert!(matches!(besselj(0.0, z), Err(Error::InvalidInput)));
2327 assert!(matches!(besselj_scaled(1.0, z), Err(Error::InvalidInput)));
2328 assert!(matches!(bessely(0.0, z), Err(Error::InvalidInput)));
2329 assert!(matches!(besseli(0.0, z), Err(Error::InvalidInput)));
2330 assert!(matches!(besselk(0.0, z), Err(Error::InvalidInput)));
2331 assert!(matches!(hankel1(0.0, z), Err(Error::InvalidInput)));
2332 assert!(matches!(hankel2(0.0, z), Err(Error::InvalidInput)));
2333 // Negative orders exercise the reflection paths in the wrappers
2334 // (one function for half-integer |ν|, two otherwise).
2335 assert!(matches!(besselj(-2.5, z), Err(Error::InvalidInput)));
2336 assert!(matches!(besselj(-2.3, z), Err(Error::InvalidInput)));
2337 assert!(matches!(besselk(-2.5, z), Err(Error::InvalidInput)));
2338 }
2339 }
2340
2341 #[test]
2342 fn bessel_non_finite_nu_returns_invalid_input() {
2343 let z = Complex64::new(1.0, 1.0);
2344 for nu in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2345 assert!(matches!(besselj(nu, z), Err(Error::InvalidInput)));
2346 assert!(matches!(bessely(nu, z), Err(Error::InvalidInput)));
2347 assert!(matches!(besseli(nu, z), Err(Error::InvalidInput)));
2348 assert!(matches!(besselk(nu, z), Err(Error::InvalidInput)));
2349 assert!(matches!(hankel1(nu, z), Err(Error::InvalidInput)));
2350 assert!(matches!(hankel2(nu, z), Err(Error::InvalidInput)));
2351 }
2352 }
2353
2354 #[test]
2355 fn bessel_non_finite_f32_returns_invalid_input() {
2356 let z = Complex32::new(1.0, 1.0);
2357 assert!(matches!(
2358 besselj(0.0f32, Complex32::new(f32::NAN, 0.0)),
2359 Err(Error::InvalidInput)
2360 ));
2361 assert!(matches!(besselj(f32::NAN, z), Err(Error::InvalidInput)));
2362 }
2363
2364 #[test]
2365 fn airy_non_finite_z_returns_invalid_input() {
2366 let nan = f64::NAN;
2367 for z in [
2368 Complex64::new(nan, 0.0),
2369 Complex64::new(0.0, nan),
2370 Complex64::new(f64::INFINITY, 0.0),
2371 ] {
2372 assert!(matches!(airy(z), Err(Error::InvalidInput)));
2373 assert!(matches!(airyprime(z), Err(Error::InvalidInput)));
2374 assert!(matches!(biry(z), Err(Error::InvalidInput)));
2375 assert!(matches!(biryprime(z), Err(Error::InvalidInput)));
2376 assert!(matches!(
2377 airy_raw(z, Scaling::Unscaled),
2378 Err(Error::InvalidInput)
2379 ));
2380 assert!(matches!(
2381 biry_raw(z, Scaling::Unscaled),
2382 Err(Error::InvalidInput)
2383 ));
2384 }
2385 }
2386
2387 #[test]
2388 fn finite_overflowing_modulus_keeps_total_precision_loss() {
2389 // |z| overflows to infinity while both components are finite: this
2390 // must stay TotalPrecisionLoss (range check), not InvalidInput.
2391 let z = Complex64::new(1.7e308, 1.7e308);
2392 assert!(matches!(besselj(0.0, z), Err(Error::TotalPrecisionLoss)));
2393 }
2394
2395 #[test]
2396 fn seq_non_finite_inputs_return_invalid_input() {
2397 let z_nan = Complex64::new(f64::NAN, 0.0);
2398 let z = Complex64::new(1.0, 1.0);
2399 let mut out = [Complex64::new(0.0, 0.0); 3];
2400 assert!(matches!(
2401 besselj_seq_into(f64::NAN, z, Scaling::Unscaled, &mut out[..2]),
2402 Err(Error::InvalidInput)
2403 ));
2404 assert!(matches!(
2405 besselj_seq_into(0.0, z_nan, Scaling::Unscaled, &mut out[..2]),
2406 Err(Error::InvalidInput)
2407 ));
2408 assert!(matches!(
2409 besselk_seq_into(-2.5, z_nan, Scaling::Unscaled, &mut out),
2410 Err(Error::InvalidInput)
2411 ));
2412 }
2413}
2414
2415// ── Scaled vs unscaled consistency ──
2416//
2417// Every `_scaled` function is the unscaled one times an elementary factor, so
2418// the two must agree wherever both are representable. The grid reaches the
2419// regions where the scaled paths switch to underflow/overflow handling
2420// (2|Im z| above ELIM for Y, large orders), which a value comparison at
2421// moderate arguments does not.
2422
2423#[cfg(test)]
2424mod scaling_tests {
2425 use super::*;
2426 use num_complex::Complex64;
2427
2428 pub(super) type F<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
2429
2430 pub(super) fn fns<T: BesselFloat>() -> [(&'static str, F<T>, F<T>); 6] {
2431 [
2432 ("J", besselj, besselj_scaled),
2433 ("Y", bessely, bessely_scaled),
2434 ("I", besseli, besseli_scaled),
2435 ("K", besselk, besselk_scaled),
2436 ("H1", hankel1, hankel1_scaled),
2437 ("H2", hankel2, hankel2_scaled),
2438 ]
2439 }
2440
2441 /// Log of the factor in scaled = exp(c)·unscaled.
2442 fn log_factor(name: &str, z: Complex64) -> Complex64 {
2443 let i = Complex64::new(0.0, 1.0);
2444 match name {
2445 "J" | "Y" => Complex64::new(-z.im.abs(), 0.0),
2446 "I" => Complex64::new(-z.re.abs(), 0.0),
2447 "K" => z,
2448 "H1" => -i * z,
2449 _ => i * z,
2450 }
2451 }
2452
2453 /// Compare scaled with exp(c)·unscaled, formed in log space so neither
2454 /// the factor nor the product overflows. Pairs where either value fails,
2455 /// is non-finite, or lies below `floor` (underflow handling) are skipped.
2456 fn check<T: BesselFloat>(nus: &[f64], radii: &[f64], floor: f64, tol: f64) {
2457 let to64 = |v: Complex<T>| Complex64::new(v.re.to_f64().unwrap(), v.im.to_f64().unwrap());
2458 let mut compared = 0;
2459 for (name, unscaled, scaled) in fns::<T>() {
2460 for &nu in nus {
2461 for &r in radii {
2462 for k in 0..12 {
2463 // 12 angles over the full circle, including both axes
2464 let th =
2465 -core::f64::consts::PI + core::f64::consts::FRAC_PI_6 * f64::from(k);
2466 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
2467 let nu_t = T::from_f64(nu);
2468 let (Ok(u), Ok(s)) = (unscaled(nu_t, z), scaled(nu_t, z)) else {
2469 continue;
2470 };
2471 let (u, s) = (to64(u), to64(s));
2472 let ok = |v: Complex64| v.norm().is_finite() && v.norm() > floor;
2473 if !ok(u) || !ok(s) {
2474 continue;
2475 }
2476 let expected = (u.ln() + log_factor(name, to64(z))).exp();
2477 let err = (s - expected).norm() / expected.norm();
2478 assert!(
2479 err < tol,
2480 "{name}: nu={nu}, z={z:?}: scaled={s:?}, exp(c)*unscaled={expected:?}, \
2481 rel_err={err:e}"
2482 );
2483 compared += 1;
2484 }
2485 }
2486 }
2487 }
2488 assert!(compared > 0);
2489 }
2490
2491 #[test]
2492 fn scaled_matches_unscaled_f64() {
2493 // Radii past ELIM/2 ≈ 350 with orders up to beyond |z|. Observed up to
2494 // ~6e-13 (large ν and |z|); a lost or mis-scaled term gives O(1).
2495 check::<f64>(
2496 &[0.0, 0.3, 2.5, 45.7, 250.0, 575.0, 800.0, 1200.0],
2497 &[0.5, 5.0, 40.0, 200.0, 360.0, 450.0, 600.0],
2498 1e-290,
2499 1e-10,
2500 );
2501 }
2502
2503 #[test]
2504 fn scaled_matches_unscaled_f32() {
2505 // ELIM/2 ≈ 40 in f32. Observed up to ~4e-5.
2506 check::<f32>(
2507 &[0.0, 0.3, 2.5, 25.7, 65.0, 90.0, 130.0],
2508 &[0.5, 5.0, 20.0, 41.0, 50.0, 60.0, 75.0],
2509 1e-30,
2510 1e-3,
2511 );
2512 }
2513}
2514
2515#[cfg(test)]
2516mod special_value_tests {
2517 use super::scaling_tests::fns;
2518 use super::*;
2519
2520 type AiryFn<T> = fn(Complex<T>) -> Result<Complex<T>, Error>;
2521 type AiryRawFn<T> = fn(Complex<T>, Scaling) -> Result<AiryResult<T>, Error>;
2522
2523 /// As in Amos, a real or imaginary part of −0 must give the same result
2524 /// as +0: on the negative real axis the functions with a branch cut take
2525 /// their values from above it, and the Airy functions are entire.
2526 fn check_signed_zero<T: BesselFloat>() {
2527 let airy_fns: [(&str, AiryFn<T>); 8] = [
2528 ("Ai", airy),
2529 ("Ai'", airyprime),
2530 ("Bi", biry),
2531 ("Bi'", biryprime),
2532 ("Ai scaled", airy_scaled),
2533 ("Ai' scaled", airyprime_scaled),
2534 ("Bi scaled", biry_scaled),
2535 ("Bi' scaled", biryprime_scaled),
2536 ];
2537 let raw_fns: [(&str, AiryRawFn<T>); 4] = [
2538 ("Ai raw", airy_raw),
2539 ("Ai' raw", airyprime_raw),
2540 ("Bi raw", biry_raw),
2541 ("Bi' raw", biryprime_raw),
2542 ];
2543 let zero = T::zero();
2544 // Power series (|z| <= 1) and the K/I paths, up to the overflow limits
2545 for x in [0.5, 2.0, 5.0, 50.0, 105.0] {
2546 let x = T::from_f64(x);
2547 for (plus, minus) in [
2548 (Complex::new(-x, zero), Complex::new(-x, -zero)),
2549 (Complex::new(x, zero), Complex::new(x, -zero)),
2550 (Complex::new(zero, x), Complex::new(-zero, x)),
2551 (Complex::new(zero, -x), Complex::new(-zero, -x)),
2552 ] {
2553 for (name, f) in airy_fns {
2554 assert_eq!(f(plus), f(minus), "{name}: {plus:?} vs {minus:?}");
2555 }
2556 for (name, f) in raw_fns {
2557 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
2558 assert_eq!(
2559 f(plus, scaling),
2560 f(minus, scaling),
2561 "{name} {scaling:?}: {plus:?} vs {minus:?}"
2562 );
2563 }
2564 }
2565 for (name, unscaled, scaled) in fns::<T>() {
2566 for nu in [0.0, 0.3, 1.0, 2.5, -0.3, -2.5, 90.5] {
2567 let nu = T::from_f64(nu);
2568 for f in [unscaled, scaled] {
2569 assert_eq!(
2570 f(nu, plus),
2571 f(nu, minus),
2572 "{name}: nu={nu:?}, {plus:?} vs {minus:?}"
2573 );
2574 }
2575 }
2576 }
2577 }
2578 }
2579 }
2580
2581 #[test]
2582 fn signed_zero_matches_positive_zero_f64() {
2583 check_signed_zero::<f64>();
2584 }
2585
2586 #[test]
2587 fn signed_zero_matches_positive_zero_f32() {
2588 check_signed_zero::<f32>();
2589 }
2590
2591 /// For half-integer |ν|, cos(νπ) = 0, so J_{−ν} = −sin(νπ)·Y_ν and
2592 /// Y_{−ν} = sin(νπ)·J_ν: the result (or error) is that of the one
2593 /// remaining function, even where the vanishing one overflows (Y_ν for
2594 /// large ν and small |z|) or is undefined (Y_ν at z = 0).
2595 fn check_negative_half_integer<T: BesselFloat>(orders: &[f64]) {
2596 let zero = T::zero();
2597 let zs = [
2598 Complex::new(T::one(), zero),
2599 Complex::new(T::from_f64(100.0), zero),
2600 Complex::from_polar(T::from_f64(0.3), T::one()),
2601 Complex::from_polar(T::from_f64(5.0), T::from_f64(2.0)),
2602 Complex::new(zero, zero),
2603 ];
2604 let pairs: [(&str, scaling_tests::F<T>, scaling_tests::F<T>); 2] = [
2605 ("unscaled", besselj, bessely),
2606 ("scaled", besselj_scaled, bessely_scaled),
2607 ];
2608 for &a in orders {
2609 let a = T::from_f64(a);
2610 let s = utils::sinpi(a);
2611 for (name, j, y) in pairs {
2612 for z in zs {
2613 assert_eq!(
2614 y(-a, z),
2615 j(a, z).map(|v| v * s),
2616 "Y {name}: nu={:?}, z={z:?}",
2617 -a
2618 );
2619 assert_eq!(
2620 j(-a, z),
2621 y(a, z).map(|v| -(v * s)),
2622 "J {name}: nu={:?}, z={z:?}",
2623 -a
2624 );
2625 }
2626 }
2627 }
2628 }
2629
2630 #[test]
2631 fn negative_half_integer_needs_one_function_f64() {
2632 check_negative_half_integer::<f64>(&[0.5, 2.5, 10.5, 150.5, 200.5, 519.5]);
2633 }
2634
2635 #[test]
2636 fn negative_half_integer_needs_one_function_f32() {
2637 check_negative_half_integer::<f32>(&[0.5, 2.5, 10.5, 30.5, 40.5, 150.5]);
2638 }
2639
2640 /// Above FNUL, the uniform expansions (ZUNI2, ZUNK2) evaluate the Airy
2641 /// function at about (1.5|z|)^(2/3), beyond the Airy functions' own range
2642 /// limit once |z| exceeds about 7.2e8. Amos leaves the result undefined
2643 /// there; these values were 0 (or, for Y, missing the K part). The
2644 /// Wronskians (DLMF 10.5.2, 10.5.5, 10.28.2) rule out a zero or a missing term.
2645 /// They cannot see a phase error common to ν and ν + 1; against mpmath
2646 /// the error here is up to ~3e-5, as at smaller |z| (status `Reduced`).
2647 #[test]
2648 fn uniform_expansion_beyond_airy_range_f64() {
2649 use core::f64::consts::{FRAC_PI_2, PI};
2650 let czero = Complex::new(0.0, 0.0);
2651 let check =
2652 |what: &str, nu: f64, z: Complex<f64>, w: Complex<f64>, expected: Complex<f64>| {
2653 let err = (w - expected).norm() / expected.norm();
2654 assert!(err < 1e-5, "{what}: nu={nu}, z={z:?}, rel_err={err:e}");
2655 };
2656 // Within ~30° of the real axis (H, J, Y) or the imaginary axis (I, K),
2657 // where ZUNI2 and ZUNK2 are used
2658 let args = [
2659 0.0,
2660 0.3,
2661 FRAC_PI_2 - 0.3,
2662 FRAC_PI_2,
2663 FRAC_PI_2 + 0.3,
2664 PI - 0.3,
2665 PI,
2666 -0.3,
2667 -FRAC_PI_2,
2668 -FRAC_PI_2 + 0.3,
2669 ];
2670 // ν = 1e5 also takes J and I through ZUNI2 (2|z| < ν²)
2671 for nu in [86.0, 90.5, 1000.0, 1e5] {
2672 for r in [7.5e8, 9e8, 1.05e9] {
2673 for th in args {
2674 let z = Complex::from_polar(r, th);
2675 // H1·H2 (the scaling factors cancel), from single values and from one call
2676 let h1 = |n: f64| hankel1_scaled(n, z).unwrap();
2677 let h2 = |n: f64| hankel2_scaled(n, z).unwrap();
2678 let expected = Complex::new(0.0, 4.0 / PI) / z;
2679 let w = h1(nu) * h2(nu + 1.0) - h1(nu + 1.0) * h2(nu);
2680 check("H single", nu, z, w, expected);
2681 let (mut s1, mut s2) = ([czero; 2], [czero; 2]);
2682 hankel1_seq_into(nu, z, Scaling::Exponential, &mut s1).unwrap();
2683 hankel2_seq_into(nu, z, Scaling::Exponential, &mut s2).unwrap();
2684 check("H seq", nu, z, s1[0] * s2[1] - s1[1] * s2[0], expected);
2685 // I·K for Re z ≥ 0, where the scaling factors leave exp(i Im z)
2686 if z.re >= 0.0 {
2687 let i = |n: f64| besseli_scaled(n, z).unwrap();
2688 let k = |n: f64| besselk_scaled(n, z).unwrap();
2689 let w = i(nu) * k(nu + 1.0) + i(nu + 1.0) * k(nu);
2690 let expected = Complex::from_polar(1.0, z.im) / z;
2691 check("I K", nu, z, w, expected);
2692 }
2693 }
2694 for x in [r, -r] {
2695 let z = Complex::new(x, 0.0);
2696 let j = |n: f64| besselj(n, z).unwrap();
2697 let y = |n: f64| bessely(n, z).unwrap();
2698 let w = j(nu) * y(nu + 1.0) - j(nu + 1.0) * y(nu);
2699 check("J Y", nu, z, w, Complex::new(-2.0 / PI, 0.0) / z);
2700 }
2701 }
2702 }
2703 }
2704
2705 /// Scaled K and H continued across the imaginary axis (ZACON) combine
2706 /// K(−z), rescaled by exp(−2z), with I(−z). Fortran rounds the phase of
2707 /// that rescaling to an ulp of 2·Im z, a relative error of up to eps·|z|.
2708 /// Half-integer orders have closed forms (DLMF 10.49.12, 10.16.2, 10.39.1);
2709 /// the points are where the K term is not negligible (near the imaginary
2710 /// axis for K and I, near the real axis for H). The K sequence (n = 5)
2711 /// reaches ZACON's second and loop calls and its IUF = 3 recovery.
2712 fn check_scaled_continuation<T: BesselFloat>(radii: &[f64], tol: f64) {
2713 use core::f64::consts::PI;
2714 let to64 = |z: Complex<T>| Complex::new(z.re.to_f64().unwrap(), z.im.to_f64().unwrap());
2715 // √(2/(πz)) on the principal branch (the reciprocal of z would flip
2716 // the sign of a zero imaginary part)
2717 let rt = |z: Complex<f64>| Complex::new((2.0 / PI).sqrt(), 0.0) / z.sqrt();
2718 // K_{n+1/2}(z)·e^z = √(π/(2z)) Σ_{k=0}^{n} (n+k)!/(k!(n−k)!) (2z)^−k
2719 let k_half = |n: usize, z: Complex<f64>| {
2720 let (mut sum, mut term, mut c) = (Complex::new(0.0, 0.0), Complex::new(1.0, 0.0), 1.0);
2721 for k in 0..=n {
2722 sum += term * c;
2723 c *= ((n + k + 1) * (n - k)) as f64 / (k + 1) as f64;
2724 term /= z * 2.0;
2725 }
2726 rt(z) * (PI / 2.0) * sum
2727 };
2728 let check =
2729 |what: &str, z: Complex<T>, v: Complex<T>, expected: Complex<f64>, scale: f64| {
2730 let err = (to64(v) - expected).norm() / scale;
2731 assert!(
2732 err < tol,
2733 "{what}: z={z:?}, value={v:?}, expected={expected:?}, err={err:e}"
2734 );
2735 };
2736 let i = Complex::new(0.0, 1.0);
2737 let half = T::from_f64(0.5);
2738 let three_halves = T::from_f64(1.5);
2739 for &r in radii {
2740 for d in [1e-3, 1.0, 5.0] {
2741 // K and I(−1/2) just left of the imaginary axis
2742 for z in [Complex::new(-d, r), Complex::new(-d, -r)] {
2743 let z = Complex::new(T::from_f64(z.re), T::from_f64(z.im));
2744 let z64 = to64(z);
2745 let k0 = k_half(0, z64);
2746 check("K 1/2", z, besselk_scaled(half, z).unwrap(), k0, k0.norm());
2747 let k1 = k_half(1, z64);
2748 check(
2749 "K 3/2",
2750 z,
2751 besselk_scaled(three_halves, z).unwrap(),
2752 k1,
2753 k1.norm(),
2754 );
2755 let mut seq = [Complex::new(T::zero(), T::zero()); 5];
2756 besselk_seq_into(half, z, Scaling::Exponential, &mut seq).unwrap();
2757 for (n, &v) in seq.iter().enumerate() {
2758 let k = k_half(n, z64);
2759 check("K seq", z, v, k, k.norm());
2760 }
2761 // I_{−1/2}(z) = √(2/(πz))·cosh z, scaled by exp(−|Re z|);
2762 // relative to √(2/(π|z|)), as cosh z can be small
2763 let x = z64.re.abs();
2764 let cosh = (Complex::from_polar((z64.re - x).exp(), z64.im)
2765 + Complex::from_polar((-z64.re - x).exp(), -z64.im))
2766 / 2.0;
2767 let ri = rt(z64);
2768 check(
2769 "I -1/2",
2770 z,
2771 besseli_scaled(-half, z).unwrap(),
2772 ri * cosh,
2773 ri.norm(),
2774 );
2775 }
2776 // H(1) below and H(2) above the real axis, and H(2) on its negative part
2777 for (z, h2) in [
2778 (Complex::new(r, -d), false),
2779 (Complex::new(-r, -d), false),
2780 (Complex::new(r, d), true),
2781 (Complex::new(-r, d), true),
2782 (Complex::new(-r, 0.0), true),
2783 ] {
2784 let z = Complex::new(T::from_f64(z.re), T::from_f64(z.im));
2785 let z64 = to64(z);
2786 let s = rt(z64);
2787 let (f, e0, e1): (scaling_tests::F<T>, _, _) = if h2 {
2788 (hankel2_scaled, i * s, -s * (1.0 - i / z64))
2789 } else {
2790 (hankel1_scaled, -i * s, -s * (1.0 + i / z64))
2791 };
2792 let name = if h2 { "H2" } else { "H1" };
2793 check(name, z, f(half, z).unwrap(), e0, e0.norm());
2794 check(name, z, f(three_halves, z).unwrap(), e1, e1.norm());
2795 }
2796 }
2797 }
2798 }
2799
2800 #[test]
2801 fn scaled_continuation_phase_f64() {
2802 // The old error is up to eps·|z|, ~2e-8 at |z| = 1e8
2803 check_scaled_continuation::<f64>(&[1e2, 1e4, 1e6, 1e8, 1e9], 1e-13);
2804 }
2805
2806 #[test]
2807 fn scaled_continuation_phase_f32() {
2808 // The old error reaches ~0.2 at |z| = 4e6
2809 check_scaled_continuation::<f32>(&[1e2, 1e3, 1e4, 1e6, 4e6], 2e-6);
2810 }
2811
2812 /// Y_{−ν} = sin(νπ)·J_ν + cos(νπ)·Y_ν next to a half-integer ν: the small
2813 /// cos(νπ) multiplies the large Y_ν, so it needs full relative accuracy.
2814 /// cospi used to lose it there (relative error ~ε/|cos(νπ)|).
2815 #[test]
2816 fn bessely_negative_order_near_half_integer() {
2817 // References from mpmath (40 digits); the old errors were 4.2e-8 and 4.8e-5
2818 let cases = [
2819 (
2820 -1.500000001,
2821 Complex::new(1e-5, 0.0),
2822 Complex::new(-0.07926656189549441, 0.0),
2823 ),
2824 (
2825 -20.499999999999,
2826 Complex::new(-3.0, 0.5),
2827 Complex::new(22.825467418360233, -109.6379263279235),
2828 ),
2829 ];
2830 for (nu, z, expected) in cases {
2831 let got = bessely(nu, z).unwrap();
2832 let err = (got - expected).norm() / expected.norm();
2833 assert!(err < 5e-14, "Y_{nu}({z}) = {got}, rel_err = {err:e}");
2834 }
2835 }
2836
2837 /// f32 version of `uniform_expansion_beyond_airy_range_f64`. Amos's
2838 /// single-precision algorithm keeps few correct digits at this |z| (as
2839 /// just below this range), so only the magnitudes are checked:
2840 /// |H(1,2)| ≈ √(2/(π|z|)) and |K| ≈ √(π/(2|z|)) for |z| ≫ ν², which rules
2841 /// out the former zeros. Half-integer orders avoid the 2·cos(νπ) term that
2842 /// H(2) picks up across the negative real axis; points on the axes
2843 /// are exact, as `from_polar` would put them off by ~0.3 in f32. Observed
2844 /// ratios are within 1%.
2845 #[test]
2846 fn uniform_expansion_beyond_airy_range_f32() {
2847 let polar = Complex::from_polar;
2848 for r in [3e6_f32, 4e6] {
2849 // The axes, and 0.3 rad on either side of them
2850 let points = [
2851 Complex::new(r, 0.0),
2852 Complex::new(-r, 0.0),
2853 Complex::new(0.0, r),
2854 Complex::new(0.0, -r),
2855 polar(r, 0.3),
2856 polar(r, 1.27),
2857 polar(r, 1.87),
2858 polar(r, 2.84),
2859 polar(r, -0.3),
2860 polar(r, -1.27),
2861 polar(r, -1.87),
2862 polar(r, -2.84),
2863 ];
2864 for (nu, z) in [35.5_f32, 40.5, 100.5]
2865 .into_iter()
2866 .flat_map(|nu| points.map(|z| (nu, z)))
2867 {
2868 let s = (z.norm() * core::f32::consts::FRAC_PI_2).sqrt();
2869 for (name, v) in [
2870 ("H1", hankel1_scaled(nu, z).unwrap() * s),
2871 ("H2", hankel2_scaled(nu, z).unwrap() * s),
2872 (
2873 "K",
2874 besselk_scaled(nu, z).unwrap() * (s / core::f32::consts::FRAC_PI_2),
2875 ),
2876 ] {
2877 // On the negative real axis H(2) is continued from two
2878 // terms of equal weight, whose f32 phase noise leaves
2879 // even the magnitude off by up to ~2.7
2880 if name == "H2" && z.im == 0.0 && z.re < 0.0 {
2881 continue;
2882 }
2883 let m = v.norm();
2884 assert!(
2885 (0.5..2.0).contains(&m),
2886 "{name}: nu={nu}, z={z:?}, ratio={m}"
2887 );
2888 }
2889 }
2890 }
2891 }
2892}
2893
2894// ── Tests for _seq functions (negative orders, recurrence paths) ──
2895
2896#[cfg(test)]
2897mod axis_tests {
2898 //! Components of values on the real and imaginary axes, against
2899 //! independent oracles for the real functions of order m = |ν| at x > 0:
2900 //! power series (J, I), half-integer and integral formulas (Y, K).
2901 //! Expected values use the |ν|-basis formulas (DLMF, with the reflection
2902 //! formulas merged in); the signed-ν formulas would cancel for some rows.
2903 use super::*;
2904 use crate::algo::gamln::gamln;
2905 use crate::utils::{cospi, sinpi};
2906
2907 const PI: f64 = core::f64::consts::PI;
2908
2909 /// (sign, ln|Γ(t)|) for t not a non-positive integer.
2910 fn lgamma_sign(t: f64) -> (f64, f64) {
2911 if t > 0.0 {
2912 (1.0, gamln(t).unwrap())
2913 } else {
2914 let s = sinpi(t);
2915 (s.signum(), PI.ln() - s.abs().ln() - gamln(1.0 - t).unwrap())
2916 }
2917 }
2918
2919 /// exp(−shift)·Σ_k (∓1)^k (x/2)^{2k+ν} / (k!·Γ(k+ν+1)): J_ν (alternating)
2920 /// or I_ν, for any real ν that is not a negative integer.
2921 fn series(nu: f64, x: f64, alternating: bool, shift: f64) -> f64 {
2922 let lx = (x / 2.0).ln();
2923 let mut sum = 0.0;
2924 for k in 0..5000usize {
2925 let kf = k as f64;
2926 let (sg, lg) = lgamma_sign(kf + nu + 1.0);
2927 let lkf = gamln(kf + 1.0).unwrap();
2928 let mag = ((2.0 * kf + nu) * lx - lkf - lg - shift).exp();
2929 let term = if alternating && k % 2 == 1 {
2930 -sg * mag
2931 } else {
2932 sg * mag
2933 };
2934 sum += term;
2935 if kf > x && term.abs() <= 1e-18 * sum.abs() {
2936 return sum;
2937 }
2938 }
2939 panic!("series did not converge: nu={nu}, x={x}");
2940 }
2941
2942 fn near_integer(m: f64) -> bool {
2943 (m - m.round()).abs() < 1e-3
2944 }
2945
2946 fn half_integer(m: f64) -> bool {
2947 (m - 0.5).fract() == 0.0
2948 }
2949
2950 /// Y_m(x), if an accurate oracle exists (not for integer or near-integer m).
2951 fn y_oracle(m: f64, x: f64) -> Option<f64> {
2952 if half_integer(m) {
2953 // Y_{n+½} = (−1)^{n+1} J_{−n−½}
2954 let n = (m - 0.5) as i64;
2955 let sign = if n % 2 == 0 { -1.0 } else { 1.0 };
2956 Some(sign * series(-m, x, true, 0.0))
2957 } else if near_integer(m) || x >= m {
2958 None
2959 } else {
2960 // J_{−m} dominates J_m·cos for x < m: no cancellation
2961 Some((series(m, x, true, 0.0) * cospi(m) - series(-m, x, true, 0.0)) / sinpi(m))
2962 }
2963 }
2964
2965 /// exp(x)·K_m(x), if an accurate oracle exists.
2966 fn k_scaled_oracle(m: f64, x: f64) -> Option<f64> {
2967 if half_integer(m) {
2968 // K_{n+½}(x)·e^x = √(π/2x)·Σ_k (n+k)!/(k!(n−k)!(2x)^k)
2969 let n = (m - 0.5) as i64;
2970 let mut term = 1.0;
2971 let mut sum = 1.0;
2972 for k in 1..=n {
2973 let kf = k as f64;
2974 term *= ((n + k) as f64) * ((n - k + 1) as f64) / (kf * 2.0 * x);
2975 sum += term;
2976 }
2977 Some((PI / (2.0 * x)).sqrt() * sum)
2978 } else if x >= 1.0 && m <= 31.0 {
2979 // e^x·K_ν(x) = ∫₀^∞ exp(−2x·sinh²(t/2))·cosh(νt) dt (trapezoid)
2980 let h = (0.6 / x.sqrt()).min(0.1);
2981 let f = |t: f64| (-2.0 * x * (t / 2.0).sinh().powi(2)).exp() * (m * t).cosh();
2982 let mut sum = f(0.0) / 2.0;
2983 for k in 1..100_000 {
2984 let v = f(k as f64 * h);
2985 sum += v;
2986 if v <= 1e-18 * sum {
2987 break;
2988 }
2989 }
2990 Some(sum * h)
2991 } else if !near_integer(m) && x < m.max(1.0) / 4.0 {
2992 // K_m = π/2·(I_{−m} − I_m)/sin(mπ); I_{−m} dominates for small x
2993 let d = series(-m, x, false, -x) - series(m, x, false, -x);
2994 Some(PI / 2.0 * d / sinpi(m))
2995 } else {
2996 None
2997 }
2998 }
2999
3000 /// Coefficients of (A, B) in the (real, imaginary) parts of `row` at
3001 /// order ν (written out from the DLMF formulas, independently of
3002 /// `axis::coefficients`).
3003 #[allow(clippy::type_complexity)]
3004 fn coeffs(row: &str, nu: f64) -> ((f64, f64), (f64, f64)) {
3005 let m = nu.abs();
3006 let (c, s) = (cospi(m), sinpi(m));
3007 let (c2, s2) = (cospi(2.0 * m), sinpi(2.0 * m));
3008 let (ch, sh) = (cospi(m / 2.0), sinpi(m / 2.0));
3009 let (c3, s3) = (cospi(1.5 * m), sinpi(1.5 * m));
3010 let neg = nu < 0.0;
3011 let h = PI / 2.0;
3012 let t = 2.0 / PI;
3013 // ((A re, B re), (A im, B im))
3014 match (row, neg) {
3015 ("H1+x", false) => ((1.0, 0.0), (0.0, 1.0)),
3016 ("H1+x", true) => ((c, -s), (s, c)),
3017 ("H2+x", false) => ((1.0, 0.0), (0.0, -1.0)),
3018 ("H2+x", true) => ((c, -s), (-s, -c)),
3019 ("H1-x", false) => ((-c, s), (s, c)),
3020 ("H1-x", true) => ((-1.0, 0.0), (0.0, 1.0)),
3021 ("H2-x", false) => ((3.0 * c, -s), (s, -c)),
3022 ("H2-x", true) => ((c2 + 2.0, -s2), (-s2, -c2)),
3023 ("Y-x", false) => ((0.0, c), (2.0 * c, -s)),
3024 ("Y-x", true) => ((s2 / 2.0, c * c), (c * c + 1.0, -s2 / 2.0)),
3025 ("K+ix", _) => ((-h * sh, -h * ch), (-h * ch, h * sh)),
3026 ("K-ix", _) => ((-h * sh, -h * ch), (h * ch, -h * sh)),
3027 ("H2+ix", false) => ((2.0 * ch, t * sh), (2.0 * sh, t * ch)),
3028 ("H2+ix", true) => ((2.0 * ch, t * s3), (-2.0 * sh, t * c3)),
3029 ("H1-ix", false) => ((2.0 * ch, t * sh), (-2.0 * sh, -t * ch)),
3030 ("H1-ix", true) => ((2.0 * ch, t * s3), (2.0 * sh, -t * c3)),
3031 _ => unreachable!(),
3032 }
3033 }
3034
3035 type F<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
3036
3037 /// (row, function, z from x, scaled with factor exp(−x)?, I·K basis?)
3038 type RowCase<T> = (&'static str, F<T>, fn(T) -> Complex<T>, bool, bool);
3039
3040 fn rows<T: BesselFloat>() -> [RowCase<T>; 13] {
3041 [
3042 (
3043 "H1+x",
3044 hankel1,
3045 |x| Complex::new(x, T::zero()),
3046 false,
3047 false,
3048 ),
3049 (
3050 "H2+x",
3051 hankel2,
3052 |x| Complex::new(x, T::zero()),
3053 false,
3054 false,
3055 ),
3056 (
3057 "H1-x",
3058 hankel1,
3059 |x| Complex::new(-x, T::zero()),
3060 false,
3061 false,
3062 ),
3063 (
3064 "H2-x",
3065 hankel2,
3066 |x| Complex::new(-x, T::zero()),
3067 false,
3068 false,
3069 ),
3070 (
3071 "Y-x",
3072 bessely,
3073 |x| Complex::new(-x, T::zero()),
3074 false,
3075 false,
3076 ),
3077 (
3078 "Y-x",
3079 bessely_scaled,
3080 |x| Complex::new(-x, T::zero()),
3081 false,
3082 false,
3083 ),
3084 (
3085 "K+ix",
3086 besselk,
3087 |x| Complex::new(T::zero(), x),
3088 false,
3089 false,
3090 ),
3091 (
3092 "K-ix",
3093 besselk,
3094 |x| Complex::new(T::zero(), -x),
3095 false,
3096 false,
3097 ),
3098 (
3099 "H2+ix",
3100 hankel2,
3101 |x| Complex::new(T::zero(), x),
3102 false,
3103 true,
3104 ),
3105 (
3106 "H2+ix",
3107 hankel2_scaled,
3108 |x| Complex::new(T::zero(), x),
3109 true,
3110 true,
3111 ),
3112 (
3113 "H1-ix",
3114 hankel1,
3115 |x| Complex::new(T::zero(), -x),
3116 false,
3117 true,
3118 ),
3119 (
3120 "H1-ix",
3121 hankel1_scaled,
3122 |x| Complex::new(T::zero(), -x),
3123 true,
3124 true,
3125 ),
3126 // Signed zeros as the real part of the imaginary axis
3127 (
3128 "K+ix",
3129 besselk,
3130 |x| Complex::new(-T::zero(), x),
3131 false,
3132 false,
3133 ),
3134 ]
3135 }
3136
3137 /// Check every component that has an oracle; returns the number checked.
3138 fn check_axis<T: BesselFloat>(
3139 nus: &[f64],
3140 jy_x: &[f64],
3141 ik_x: &[f64],
3142 tol: f64,
3143 tiny: f64,
3144 ) -> usize {
3145 let mut checked = 0;
3146 for (row, f, zf, scaled, ik) in rows::<T>() {
3147 for &nu_d in nus {
3148 let nu = T::from_f64(nu_d);
3149 let nu_d = nu.to_f64().unwrap();
3150 let m = nu_d.abs();
3151 let xs: &[f64] = if ik { ik_x } else { jy_x };
3152 for &xr in xs {
3153 // Values below 1 are fractions of m for the J·Y rows
3154 let x_d = if !ik && xr < 1.0 { xr * m } else { xr };
3155 let x = T::from_f64(x_d);
3156 let x_d = x.to_f64().unwrap();
3157 // The J series cancels beyond x ≈ √(m+1)
3158 if x_d == 0.0 || (!ik && x_d > (m / 2.0).min((m + 1.0).sqrt())) {
3159 continue;
3160 }
3161 let Ok(v) = f(nu, zf(x)) else { continue };
3162 let (a, b) = if ik {
3163 let shift = if scaled { x_d } else { 0.0 };
3164 let Some(k) = k_scaled_oracle(m, x_d) else {
3165 continue;
3166 };
3167 (series(m, x_d, false, shift), k * (-x_d - shift).exp())
3168 } else {
3169 (
3170 series(m, x_d, true, 0.0),
3171 y_oracle(m, x_d).unwrap_or(f64::NAN),
3172 )
3173 };
3174 let ((ar, br), (ai, bi)) = coeffs(row, nu_d);
3175 for (got, ca, cb) in [(v.re, ar, br), (v.im, ai, bi)] {
3176 let (ta, tb) = (ca * a, if cb == 0.0 { 0.0 } else { cb * b });
3177 let expected = ta + tb;
3178 // Skip where the two terms cancel (the oracle itself is then
3179 // inaccurate), e.g. H2_{1.5}(i) = 0 as K_{3/2}(1) = π·I_{3/2}(1)
3180 if !expected.is_finite()
3181 || expected.abs() < tiny
3182 || expected.abs() > 1e300
3183 || expected.abs() < 1e-3 * (ta.abs() + tb.abs())
3184 {
3185 continue;
3186 }
3187 let got = got.to_f64().unwrap();
3188 let err = ((got - expected) / expected).abs();
3189 assert!(
3190 err < tol,
3191 "{row}: nu={nu_d}, x={x_d}: got {got:e}, expected {expected:e}, rel {err:e}"
3192 );
3193 checked += 1;
3194 }
3195 }
3196 }
3197 }
3198 checked
3199 }
3200
3201 // Includes orders a little away from integers (2.02, 3.97, ...), where
3202 // the small sin(νπ) coefficients of Y take part
3203 const NUS: [f64; 41] = [
3204 0.5, -0.5, 1.5, -1.5, 2.5, -2.5, 5.5, -5.5, 10.5, -10.5, 30.5, -30.5, 90.5, 1.0, -1.0, 2.0,
3205 -2.0, 10.0, -10.0, 0.3, -0.3, 3.3, -3.3, 10.7, -10.7, 2.000001, -1.999999, 2.500001,
3206 -2.499999, 1.000001, -0.999999, 20.0, 85.5, 2.02, -2.02, 3.97, -3.97, 10.01, -10.01, 1.49,
3207 -2.51,
3208 ];
3209
3210 #[test]
3211 fn axis_components_f64() {
3212 // J·Y rows: x as fractions of m (≤ m/2) and absolute small values
3213 let jy_x = [1e-10, 1e-5, 1e-2, 0.1, 0.3, 0.5];
3214 let ik_x = [1e-5, 1e-2, 0.3, 1.0, 5.0, 20.0, 60.0, 200.0, 690.0];
3215 let n = check_axis::<f64>(&NUS, &jy_x, &ik_x, 1e-11, 1e-280);
3216 assert!(n > 2000, "only {n} components checked");
3217 }
3218
3219 #[test]
3220 fn axis_components_f32() {
3221 let nus = [0.5, -0.5, 2.5, -2.5, 5.5, 10.5, 1.0, -2.0, 3.3, -3.3];
3222 let jy_x = [1e-3, 0.1, 0.4];
3223 let ik_x = [1e-2, 1.0, 10.0, 40.0];
3224 let n = check_axis::<f32>(&nus, &jy_x, &ik_x, 1e-4, 1e-28);
3225 assert!(n > 200, "only {n} components checked");
3226 }
3227
3228 /// An (I, K) component at |small|/|large| = 5e-3 is recomputed: Amos's
3229 /// Im H^(2)_ν(ix) here is off by 5.2e-14, the recomputed one by < 1e-15
3230 /// (mpmath reference).
3231 #[test]
3232 fn axis_ik_ratio_between_1e3_and_1e2() {
3233 let h = hankel2(9.999_999_99f64, Complex::new(0.0, 8.199_999_991_8)).unwrap();
3234 let err = ((h.im + 0.015700603715088221) / 0.015700603715088221).abs();
3235 assert!(err < 1e-14, "Im H2_9.99999999(8.2i): error {err:e}");
3236 }
3237
3238 /// Re Y_ν(−x) for x ≥ |ν| next to half-integer orders is cos(νπ)·Y_ν(x),
3239 /// with a nearly vanishing cos(νπ). Compared against `bessely` on the
3240 /// positive real axis (a different Amos path, reflected with cospi for
3241 /// ν < 0), skipping x next to zeros of Y_ν. Amos's own Re Y_ν(−x) fails
3242 /// 248 of these f64 checks, with relative errors up to 0.8 (f32: 0.9).
3243 fn check_y_neg_axis<T: BesselFloat + core::fmt::Display>(
3244 nus: &[f64],
3245 deltas: &[f64],
3246 fracs: &[f64],
3247 tol: f64,
3248 ) -> usize {
3249 let mut checked = 0;
3250 for &nu0 in nus {
3251 for &d in deltas {
3252 for sign in [1.0, -1.0] {
3253 let nu = T::from_f64(sign * (nu0 + d));
3254 let m = nu.abs().to_f64().unwrap();
3255 for &frac in fracs {
3256 let x = T::from_f64(m * frac + 0.3);
3257 let (Ok(v), Ok(y)) = (
3258 bessely(nu, Complex::new(-x, T::zero())),
3259 bessely(nu, Complex::new(x, T::zero())),
3260 ) else {
3261 continue;
3262 };
3263 let j = besselj(nu, Complex::new(x, T::zero())).unwrap().re;
3264 let (y, j) = (y.re.to_f64().unwrap(), j.to_f64().unwrap());
3265 if y.abs() < 0.1 * j.abs() {
3266 continue;
3267 }
3268 let expected = cospi(nu).to_f64().unwrap() * y;
3269 let err = ((v.re.to_f64().unwrap() - expected) / expected).abs();
3270 assert!(
3271 err < tol,
3272 "nu={nu}, x={x}: Re Y(-x) = {}, expected {expected:e}",
3273 v.re
3274 );
3275 checked += 1;
3276 }
3277 }
3278 }
3279 }
3280 checked
3281 }
3282
3283 #[test]
3284 fn axis_y_neg_beyond_order_f64() {
3285 let nus = [0.5, 1.5, 4.5, 10.5, 30.5, 70.5];
3286 let deltas = [1e-13, 1e-10, -1e-7, 1e-5, -3e-4, 1e-3];
3287 let fracs = [1.0, 1.1, 1.5, 3.0, 8.0];
3288 let n = check_y_neg_axis::<f64>(&nus, &deltas, &fracs, 1e-11);
3289 assert!(n > 250, "only {n} values checked");
3290 }
3291
3292 #[test]
3293 fn axis_y_neg_beyond_order_f32() {
3294 let nus = [0.5, 1.5, 4.5, 10.5];
3295 let deltas = [3e-6, -3e-5, 3e-4];
3296 let fracs = [1.0, 1.5, 3.0];
3297 let n = check_y_neg_axis::<f32>(&nus, &deltas, &fracs, 1e-4);
3298 assert!(n > 40, "only {n} values checked");
3299 }
3300
3301 /// In a sequence the order of element j is exactly ν + j (from ν + nc
3302 /// for ν < 0, as the sequence computes it), in general not a float. Next
3303 /// to a half-integer, Re Y_μ(−x) = cos(μπ)·Y_μ(x) with
3304 /// cos(μπ) = (−1)^p·cos(base·π) needs that exact order: an ulp of μ is a
3305 /// relative error of ulp/|cos(μπ)| (e.g. 2.8e-9 at order 4.4999999 from
3306 /// ν = 0.4999999 at −6).
3307 /// Y_μ(x) comes from the sequence on the positive real axis (the same
3308 /// orders); x next to zeros of Y_μ is skipped. Returns the number of
3309 /// checked elements whose order is not a float.
3310 fn check_y_neg_seq_orders<T: BesselFloat + core::fmt::Display>(
3311 nus: &[f64],
3312 xs: &[f64],
3313 tol: f64,
3314 ) -> usize {
3315 const N: usize = 30;
3316 let czero = Complex::new(T::zero(), T::zero());
3317 let (mut neg, mut pos, mut jpos) = ([czero; N], [czero; N], [czero; N]);
3318 let mut inexact = 0;
3319 for &nu in nus {
3320 let nu = T::from_f64(nu);
3321 let nc = if nu < T::zero() {
3322 nu.abs().ceil().to_usize().unwrap()
3323 } else {
3324 0
3325 };
3326 let base = nu + T::from_f64(nc as f64);
3327 for &x in xs {
3328 let x = T::from_f64(x);
3329 let (zn, zp) = (Complex::new(-x, T::zero()), Complex::new(x, T::zero()));
3330 bessely_seq_into(nu, zn, Scaling::Unscaled, &mut neg).unwrap();
3331 bessely_seq_into(nu, zp, Scaling::Unscaled, &mut pos).unwrap();
3332 besselj_seq_into(nu, zp, Scaling::Unscaled, &mut jpos).unwrap();
3333 for j in nc..N {
3334 let p = j - nc;
3335 let (y, jv) = (pos[j].re, jpos[j].re);
3336 if y.abs() < T::from_f64(0.1) * jv.abs() {
3337 continue;
3338 }
3339 let sign = if p % 2 == 0 { T::one() } else { -T::one() };
3340 let expected = sign * cospi(base) * y;
3341 let err = ((neg[j].re - expected) / expected).abs().to_f64().unwrap();
3342 assert!(
3343 err < tol,
3344 "nu={nu}, x={x}, j={j}: Re Y(-x) = {}, expected {expected}, rel {err:e}",
3345 neg[j].re
3346 );
3347 // Whether base + p is a float
3348 let pt = T::from_f64(p as f64);
3349 let s = base + pt;
3350 let bb = s - base;
3351 if (base - (s - bb)) + (pt - bb) != T::zero() {
3352 inexact += 1;
3353 }
3354 }
3355 }
3356 }
3357 inexact
3358 }
3359
3360 #[test]
3361 fn axis_y_neg_seq_orders_off_the_float_grid() {
3362 let nus = [0.4999999, 0.50000031, 2.4999997, -2.4999999, -0.50000013];
3363 let n = check_y_neg_seq_orders::<f64>(&nus, &[0.3, 6.0, 15.0, 40.0], 1e-12);
3364 assert!(n > 200, "only {n} inexact orders checked");
3365 let nus = [0.4999, 0.50031, 2.4997, -2.4999, -0.50013];
3366 let n = check_y_neg_seq_orders::<f32>(&nus, &[2.0, 6.0, 15.0], 1e-4);
3367 assert!(n > 150, "only {n} inexact orders checked");
3368 }
3369
3370 /// As `check_y_neg_seq_orders` for K_μ(ix) = −(π/2)·((s·J + c·Y) +
3371 /// i(c·J − s·Y)) with (s, c) = (sin, cos)(μπ/2), x < μ: next to an
3372 /// integer μ one of s, c nearly vanishes. They come from those of base/2
3373 /// by p quarter turns; J and Y from sequences on the positive real axis.
3374 fn check_k_imag_seq_orders<T: BesselFloat + core::fmt::Display>(
3375 nus: &[f64],
3376 xs: &[f64],
3377 tol: f64,
3378 ) -> usize {
3379 const N: usize = 30;
3380 let czero = Complex::new(T::zero(), T::zero());
3381 let (mut k, mut ypos, mut jpos) = ([czero; N], [czero; N], [czero; N]);
3382 let hpi = T::from_f64(PI / 2.0);
3383 let mut checked = 0;
3384 for &nu in nus {
3385 let nu = T::from_f64(nu);
3386 let nc = if nu < T::zero() {
3387 nu.abs().ceil().to_usize().unwrap()
3388 } else {
3389 0
3390 };
3391 let base = nu + T::from_f64(nc as f64);
3392 let half = base / T::from_f64(2.0);
3393 let (s0, c0) = (sinpi(half), cospi(half));
3394 for &x in xs {
3395 let x = T::from_f64(x);
3396 let zp = Complex::new(x, T::zero());
3397 besselk_seq_into(nu, Complex::new(T::zero(), x), Scaling::Unscaled, &mut k)
3398 .unwrap();
3399 bessely_seq_into(nu, zp, Scaling::Unscaled, &mut ypos).unwrap();
3400 besselj_seq_into(nu, zp, Scaling::Unscaled, &mut jpos).unwrap();
3401 for j in nc..N {
3402 let p = j - nc;
3403 let m = base + T::from_f64(p as f64);
3404 if x >= m {
3405 continue;
3406 }
3407 let (s, c) = match p % 4 {
3408 0 => (s0, c0),
3409 1 => (c0, -s0),
3410 2 => (-s0, -c0),
3411 _ => (-c0, s0),
3412 };
3413 let (jv, y) = (jpos[j].re, ypos[j].re);
3414 let terms = [(s * jv, c * y), (c * jv, -(s * y))];
3415 let got = [k[j].re, k[j].im];
3416 // The small component, where its two terms do not cancel
3417 let (i, (ta, tb)) = if got[0].abs() < got[1].abs() {
3418 (0, terms[0])
3419 } else {
3420 (1, terms[1])
3421 };
3422 let expected = -hpi * (ta + tb);
3423 if got[i].abs() >= T::from_f64(0.1) * got[1 - i].abs()
3424 || (ta + tb).abs() < T::from_f64(0.1) * (ta.abs() + tb.abs())
3425 {
3426 continue;
3427 }
3428 let err = ((got[i] - expected) / expected).abs().to_f64().unwrap();
3429 assert!(
3430 err < tol,
3431 "nu={nu}, x={x}, j={j}: K(ix) = {}, expected {expected} in part {i}, rel {err:e}",
3432 k[j]
3433 );
3434 checked += 1;
3435 }
3436 }
3437 }
3438 checked
3439 }
3440
3441 #[test]
3442 fn axis_k_imag_seq_orders_off_the_float_grid() {
3443 let nus = [1.9999997, 3.00000031, 0.9999997, -2.0000001, -1.00000013];
3444 let n = check_k_imag_seq_orders::<f64>(&nus, &[0.3, 2.0, 6.0], 1e-12);
3445 assert!(n > 150, "only {n} components checked");
3446 let nus = [1.9997, 3.00031, 0.9997, -2.0001, -1.00013];
3447 let n = check_k_imag_seq_orders::<f32>(&nus, &[2.0, 6.0], 1e-4);
3448 assert!(n > 100, "only {n} components checked");
3449 }
3450
3451 /// Whether the J·Y rule recomputes component k (0 = real, 1 = imaginary)
3452 /// of `kind` ('Y', or '2' for unscaled H^(2)) at −x for x ≥ |ν|: Y_m
3453 /// alone (J's coefficient below 1e-3 of Y's) for m < 18, or Re Y(−x)
3454 /// with coefficients summing below 0.1. The coefficients are those of
3455 /// J_m and Y_m (DLMF 10.4.6, 10.11.2/5).
3456 fn neg_axis_selected(kind: char, nu: f64, k: usize) -> bool {
3457 let m = nu.abs();
3458 let (c, s) = (cospi(m), sinpi(m));
3459 let (c2, s2) = (cospi(2.0 * m), sinpi(2.0 * m));
3460 let (a, b) = match (kind, nu < 0.0, k) {
3461 ('Y', false, 0) => (0.0, c),
3462 ('Y', false, _) => (2.0 * c, -s),
3463 ('Y', true, 0) => (s2 / 2.0, c * c),
3464 ('Y', true, _) => (c * c + 1.0, -s2 / 2.0),
3465 ('2', false, 0) => (3.0 * c, -s),
3466 ('2', false, _) => (s, -c),
3467 ('2', true, 0) => (c2 + 2.0, -s2),
3468 ('2', true, _) => (-s2, -c2),
3469 _ => return false,
3470 };
3471 (m < 18.0 && a.abs() < 1e-3 * b.abs()) || (kind == 'Y' && k == 0 && a.abs() + b.abs() < 0.1)
3472 }
3473
3474 /// Components of Y(−x) and H^(2)(−x) beyond the order that are Y_m alone
3475 /// (at or next to integer and half-integer orders, and Re Y(−x) for
3476 /// ν ≥ 0), next to their zeros: single values and the same element of a
3477 /// sequence of 45, crossing ν = 0 where the orders allow it exactly. Errors in units of ε·(|α| + |β|)·|H_m(x)|,
3478 /// the error to expect of a normwise accurate value: Amos's are 31 to
3479 /// 1746 here (f32 up to 13809), except at the two points of
3480 /// ν = −1 − 5e-8 and −9 (11.6 and 3.5), the recomputed ones, single and
3481 /// in sequences, at most 7.1 (mpmath references).
3482 #[test]
3483 fn axis_neg_next_to_zeros() {
3484 type Case = (char, f64, f64, usize, f64, f64);
3485 fn check<T: BesselFloat + core::fmt::Display>(cases: &[Case]) {
3486 let czero = Complex::new(T::zero(), T::zero());
3487 let mut buf = [czero; 45];
3488 for &(kind, nu, x, k, want, scale) in cases {
3489 let z = Complex::new(T::from_f64(-x), T::zero());
3490 let tnu = T::from_f64(nu);
3491 // Element j of a sequence from ν − j crossing ν = 0, with the
3492 // largest j ≤ 20 whose order is exactly ν (ν < 0 crosses with
3493 // j = 0; the order of a negative element is |ν0| − j)
3494 let exact = |j: usize| {
3495 let (jt, nu0) = (T::from_f64(j as f64), tnu - T::from_f64(j as f64));
3496 if nu < 0.0 {
3497 nu0.abs() - jt == tnu.abs()
3498 } else {
3499 nu0 + jt == tnu
3500 }
3501 };
3502 let j = (0..=20).rev().find(|&j| exact(j)).unwrap();
3503 let nu0 = tnu - T::from_f64(j as f64);
3504 let (singles, seq) = if kind == 'Y' {
3505 let seq = bessely_seq_into(nu0, z, Scaling::Unscaled, &mut buf);
3506 let s = [bessely(tnu, z).unwrap(), bessely_scaled(tnu, z).unwrap()];
3507 (s, seq)
3508 } else {
3509 let seq = hankel2_seq_into(nu0, z, Scaling::Unscaled, &mut buf);
3510 let v = hankel2(tnu, z).unwrap();
3511 ([v, v], seq)
3512 };
3513 seq.unwrap();
3514 for v in singles.into_iter().chain([buf[j]]) {
3515 let got = if k == 0 { v.re } else { v.im };
3516 let units = (got.to_f64().unwrap() - want).abs()
3517 / (T::epsilon().to_f64().unwrap() * scale);
3518 assert!(
3519 units < 16.0,
3520 "{kind} nu={nu}, x={x}, k={k}: {v}, {units} units"
3521 );
3522 }
3523 }
3524 }
3525 // (function, ν, x, component, value, (|α| + |β|)·|H_m(x)|)
3526 check::<f64>(&[
3527 (
3528 'Y',
3529 0.5404395744314369,
3530 14.199204647196062,
3531 0,
3532 1.7790766124893162e-07,
3533 0.02683,
3534 ),
3535 (
3536 'Y',
3537 5.4579621742598405,
3538 17.955980580074737,
3539 0,
3540 6.434329040379513e-07,
3541 0.02539,
3542 ),
3543 (
3544 'Y',
3545 8.553187617888364,
3546 14.839639619602085,
3547 0,
3548 6.376630874085129e-06,
3549 0.03803,
3550 ),
3551 (
3552 'Y',
3553 10.466204201871951,
3554 20.809512383347556,
3555 0,
3556 -0.0003560905884186947,
3557 0.01992,
3558 ),
3559 (
3560 'Y',
3561 12.541375369126225,
3562 19.383109940166836,
3563 0,
3564 -0.0010992113067365456,
3565 0.02685,
3566 ),
3567 (
3568 'Y',
3569 15.457226611852212,
3570 17.934577879174395,
3571 0,
3572 -0.0009456667810636375,
3573 0.03461,
3574 ),
3575 (
3576 'Y',
3577 -1.0000000505613758,
3578 18.058798586324336,
3579 0,
3580 -0.002889004423008248,
3581 0.1879,
3582 ),
3583 (
3584 'Y',
3585 -9.0,
3586 15.361301343580108,
3587 0,
3588 -7.680400842858634e-13,
3589 0.2257,
3590 ),
3591 (
3592 'Y',
3593 -1e-08,
3594 13.361097489864841,
3595 0,
3596 6.9195873309076905e-09,
3597 0.2182,
3598 ),
3599 (
3600 'Y',
3601 -14.0,
3602 84.44679697015918,
3603 0,
3604 -1.9655202108533837e-08,
3605 0.08743,
3606 ),
3607 (
3608 'Y',
3609 -16.0002,
3610 100.03440128478886,
3611 0,
3612 -5.04102359239503e-05,
3613 0.08034,
3614 ),
3615 (
3616 'Y',
3617 16.5001,
3618 100.74840631508634,
3619 1,
3620 -1.572667517633897e-11,
3621 0.08008,
3622 ),
3623 (
3624 'Y',
3625 16.50001,
3626 100.74885123627298,
3627 1,
3628 3.4050198315155154e-12,
3629 0.08004,
3630 ),
3631 (
3632 '2',
3633 2.0,
3634 13.209986713986947,
3635 1,
3636 8.25474504044067e-10,
3637 0.2207,
3638 ),
3639 (
3640 '2',
3641 14.0,
3642 84.44679697015918,
3643 1,
3644 1.9655202108533837e-08,
3645 0.08743,
3646 ),
3647 (
3648 '2',
3649 17.0,
3650 56.32193676896589,
3651 1,
3652 1.067480680320756e-12,
3653 0.1089,
3654 ),
3655 (
3656 '2',
3657 5.5,
3658 7.293692200815263,
3659 0,
3660 2.0120730332658653e-11,
3661 0.3557,
3662 ),
3663 (
3664 '2',
3665 14.5,
3666 85.15744139214142,
3667 0,
3668 3.7729005022425324e-05,
3669 0.0871,
3670 ),
3671 (
3672 '2',
3673 -5.5,
3674 21.283248938558053,
3675 1,
3676 1.5177917276336462e-11,
3677 0.1759,
3678 ),
3679 (
3680 '2',
3681 -17.5,
3682 60.27562453458215,
3683 1,
3684 -1.0842178117582853e-11,
3685 0.1051,
3686 ),
3687 (
3688 '2',
3689 0.0001,
3690 13.360940502860311,
3691 1,
3692 0.0001371041719352024,
3693 0.2183,
3694 ),
3695 (
3696 '2',
3697 -16.5001,
3698 100.74840631508634,
3699 1,
3700 2.0689729735150853e-11,
3701 0.08008,
3702 ),
3703 (
3704 '2',
3705 16.5001,
3706 100.74840631508634,
3707 0,
3708 -2.5143155227226787e-05,
3709 0.08011,
3710 ),
3711 ]);
3712 check::<f32>(&[
3713 (
3714 'Y',
3715 0.42830756306648254,
3716 7.750723838806152,
3717 0,
3718 0.0003261815181377399,
3719 0.06399,
3720 ),
3721 (
3722 'Y',
3723 3.5858168601989746,
3724 8.841499328613281,
3725 0,
3726 3.636876710628325e-05,
3727 0.0746,
3728 ),
3729 (
3730 'Y',
3731 11.286245346069336,
3732 38.86476516723633,
3733 0,
3734 0.0005308631669817854,
3735 0.0814,
3736 ),
3737 (
3738 'Y',
3739 12.348158836364746,
3740 40.268272399902344,
3741 0,
3742 8.929377383150825e-08,
3743 0.05917,
3744 ),
3745 (
3746 'Y',
3747 17.424158096313477,
3748 53.573184967041016,
3749 0,
3750 6.935261359731625e-07,
3751 0.02646,
3752 ),
3753 (
3754 'Y',
3755 -13.999799728393555,
3756 39.09739303588867,
3757 0,
3758 0.00014275489424510885,
3759 0.1321,
3760 ),
3761 (
3762 'Y',
3763 -17.0,
3764 49.711029052734375,
3765 0,
3766 0.0025988763574300126,
3767 0.1167,
3768 ),
3769 (
3770 'Y',
3771 14.500100135803223,
3772 39.73927688598633,
3773 1,
3774 -1.494391009619099e-06,
3775 0.1312,
3776 ),
3777 (
3778 '2',
3779 17.0,
3780 49.711029052734375,
3781 1,
3782 0.0025988763574300126,
3783 0.1167,
3784 ),
3785 (
3786 '2',
3787 12.0,
3788 39.807090759277344,
3789 1,
3790 -5.3511832603744025e-06,
3791 0.1295,
3792 ),
3793 (
3794 '2',
3795 16.5,
3796 45.684940338134766,
3797 0,
3798 -3.289102085362203e-06,
3799 0.1222,
3800 ),
3801 (
3802 '2',
3803 -16.500099182128906,
3804 38.856502532958984,
3805 1,
3806 4.2589171368920826e-05,
3807 0.1346,
3808 ),
3809 ]);
3810 }
3811
3812 /// Values on the negative real axis beyond the order with a small
3813 /// component the rule leaves alone are Amos's, bit for bit (mpmath
3814 /// errors in units as in `axis_neg_next_to_zeros`, Amos → recomputed):
3815 /// - Re Y_41(−x) next to a zero of Y_41 (relative errors 5.5e-15 →
3816 /// 2.2e-13).
3817 /// - Components whose J coefficient is not small: Im Y(−x) and
3818 /// Re Y(−x) for ν < 0 next to their zeros (e.g. Re Y_{−7.9}, 0.1 →
3819 /// 73 units), with J/Y ratios 3e-3 and 1.3e-3 just above the limit,
3820 /// and J alone (Im H^(2)_{16.5}(−x) = J_{16.5}(x)).
3821 /// - Y alone above order 18 (Im H^(2)_20(−x)), and in H^(1)(−x), where
3822 /// Amos's value is as accurate.
3823 #[test]
3824 fn axis_neg_keeps_amos() {
3825 for (kind, nu, x) in [
3826 ('Y', 41.0f64, 50.623088092303156),
3827 ('Y', 0.7876708575032857, 29.094981102136426),
3828 ('Y', 2.000000000007951, 24.27010247905739),
3829 ('Y', -7.9, 18.043947370535463),
3830 ('Y', -16.999, 59.607268325147366),
3831 ('2', -16.0002, 100.03440128478886),
3832 ('2', 16.5, 99.21561951113041),
3833 ('2', 20.0, 56.92777653004243),
3834 ('1', -17.4999, 29.133873186247307),
3835 ] {
3836 let z = Complex::new(-x, 0.0);
3837 let pairs = if kind == 'Y' {
3838 [
3839 (bessely(nu, z), bessely_value(nu, z, Scaling::Unscaled)),
3840 (
3841 bessely_scaled(nu, z),
3842 bessely_value(nu, z, Scaling::Exponential),
3843 ),
3844 ]
3845 } else {
3846 let (f, h): (F<f64>, _) = if kind == '1' {
3847 (hankel1, HankelKind::First)
3848 } else {
3849 (hankel2, HankelKind::Second)
3850 };
3851 let p = (f(nu, z), hankel_value(h, nu, z, Scaling::Unscaled));
3852 [p, p]
3853 };
3854 for (v, r) in pairs {
3855 let (v, r) = (v.unwrap(), r.unwrap());
3856 assert_eq!(
3857 (v.re.to_bits(), v.im.to_bits()),
3858 (r.re.to_bits(), r.im.to_bits()),
3859 "{kind} nu={nu}, x={x}"
3860 );
3861 }
3862 }
3863 }
3864
3865 /// Re H(ν, x) against `besselj`: a different Amos path (J from zbinu,
3866 /// H from K(−iz)).
3867 #[test]
3868 fn hankel_real_part_matches_besselj() {
3869 for nu in [1.0, 5.0, 10.0, 30.5, -10.0, -3.3, 90.0, 150.5] {
3870 for frac in [1e-6, 0.01, 0.3, 0.9] {
3871 let z = Complex::new(frac * f64::abs(nu), 0.0);
3872 let j = besselj(nu, z).unwrap().re;
3873 if j == 0.0 {
3874 continue;
3875 }
3876 for h in [hankel1(nu, z).unwrap(), hankel2(nu, z).unwrap()] {
3877 let err = ((h.re - j) / j).abs();
3878 assert!(
3879 err < 1e-11,
3880 "nu={nu}, x={}: Re H = {:e}, J = {j:e}",
3881 z.re,
3882 h.re
3883 );
3884 }
3885 }
3886 }
3887 }
3888
3889 /// J and Y of real order are exactly real on the positive real axis.
3890 #[test]
3891 fn bessel_jy_real_on_positive_axis() {
3892 let mut out = [Complex::new(0.0f64, 0.0); 9];
3893 for nu in [0.0f64, 0.3, 1.0, 5.5, 120.0, -0.3, -2.0, -7.5, -40.25] {
3894 for x in [1e-8f64, 0.5, 5.0, 37.0, 1e3, 1e5] {
3895 let z = Complex::new(x, 0.0);
3896 for f in [besselj, besselj_scaled, bessely, bessely_scaled] {
3897 if let Ok(v) = f(nu, z) {
3898 assert_eq!(v.im.to_bits(), 0, "nu={nu}, x={x}: {v}");
3899 }
3900 }
3901 for s in [Scaling::Unscaled, Scaling::Exponential] {
3902 if besselj_seq_into(nu, z, s, &mut out).is_ok() {
3903 assert!(
3904 out.iter().all(|v| v.im.to_bits() == 0),
3905 "J seq nu={nu}, x={x}"
3906 );
3907 }
3908 if bessely_seq_into(nu, z, s, &mut out).is_ok() {
3909 assert!(
3910 out.iter().all(|v| v.im.to_bits() == 0),
3911 "Y seq nu={nu}, x={x}"
3912 );
3913 }
3914 }
3915 }
3916 }
3917 }
3918
3919 /// Values the rule does not select are Amos's own, bit for bit: off the
3920 /// axes, for the J·Y rows at x ≥ m (except components of Y(−x) and
3921 /// H^(2)(−x) that `neg_axis_selected`), and where the components are not
3922 /// far apart in size (ratio ≥ 0.1 for J·Y, ≥ 1e-2 for I·K).
3923 #[test]
3924 fn axis_fix_leaves_other_values_alone() {
3925 type Raw = fn(f64, Complex<f64>, Scaling) -> Result<Complex<f64>, Error>;
3926 let raw_h1: Raw = |nu, z, s| hankel_value(HankelKind::First, nu, z, s);
3927 let raw_h2: Raw = |nu, z, s| hankel_value(HankelKind::Second, nu, z, s);
3928 // (function, Amos's value, scaling, 'H' / '2' (unscaled H^(2)) / 'K' / 'Y')
3929 let fns: [(F<f64>, Raw, Scaling, char); 7] = [
3930 (hankel1, raw_h1, Scaling::Unscaled, 'H'),
3931 (hankel2, raw_h2, Scaling::Unscaled, '2'),
3932 (hankel1_scaled, raw_h1, Scaling::Exponential, 'H'),
3933 (hankel2_scaled, raw_h2, Scaling::Exponential, 'H'),
3934 (besselk, besselk_value, Scaling::Unscaled, 'K'),
3935 (bessely, bessely_value, Scaling::Unscaled, 'Y'),
3936 (bessely_scaled, bessely_value, Scaling::Exponential, 'Y'),
3937 ];
3938 let tiny = f64::MIN_POSITIVE;
3939 for (f, raw, scaling, kind) in fns {
3940 for nu in [0.0, 0.3, 2.0, 5.5, 20.0, -0.3, -2.0, -5.5, -20.0, 90.5] {
3941 for x in [0.01, 0.7, 1.9, 6.0, 25.0, 100.0] {
3942 let zs = [
3943 Complex::new(x, 0.0),
3944 Complex::new(-x, 0.0),
3945 Complex::new(0.0, x),
3946 Complex::new(0.0, -x),
3947 // Just off the axes
3948 Complex::new(x, tiny),
3949 Complex::new(-x, -tiny),
3950 Complex::new(tiny, x),
3951 Complex::new(-tiny, -x),
3952 Complex::new(x, x),
3953 ];
3954 for (iz, z) in zs.into_iter().enumerate() {
3955 let (Ok(v), Ok(r)) = (f(nu, z), raw(nu, z, scaling)) else {
3956 continue;
3957 };
3958 let (sr, si) = (r.re.abs(), r.im.abs());
3959 let ratio = sr.min(si) / sr.max(si);
3960 let imag_axis = z.re == 0.0;
3961 // H on the imaginary axis is an I·K row; the rest are J·Y
3962 let ik = imag_axis && matches!(kind, 'H' | '2');
3963 let selectable = iz < 4
3964 && if ik {
3965 ratio < 1e-2
3966 } else if x < f64::abs(nu) {
3967 ratio < 0.1
3968 } else {
3969 iz == 1
3970 && ratio < 0.1
3971 && neg_axis_selected(kind, nu, usize::from(sr >= si))
3972 };
3973 // Y is made exactly real on the positive real axis
3974 let r = if kind == 'Y' && iz == 0 {
3975 Complex::new(r.re, 0.0)
3976 } else {
3977 r
3978 };
3979 if !selectable {
3980 assert!(
3981 v.re.to_bits() == r.re.to_bits()
3982 && v.im.to_bits() == r.im.to_bits(),
3983 "nu={nu}, z={z}: {v} vs Amos {r}"
3984 );
3985 }
3986 }
3987 }
3988 }
3989 }
3990 }
3991
3992 type SeqF = fn(f64, Complex<f64>, Scaling, &mut [Complex<f64>]) -> Result<SeqInfo, Error>;
3993
3994 /// `_seq_into` against single values on the axes, across chunk boundaries
3995 /// and orders crossing zero. Where the small component is clearly
3996 /// recomputed by both, compare it componentwise; otherwise normwise
3997 /// (Amos's own seq and single values differ by up to ~2e-13 normwise).
3998 #[test]
3999 fn axis_seq_matches_single() {
4000 // (.., ratio, function for `neg_axis_selected` beyond x = m, or ' ')
4001 type SeqCase = (SeqF, F<f64>, Scaling, fn(f64) -> Complex<f64>, f64, char);
4002 let cases: [SeqCase; 9] = [
4003 (
4004 hankel1_seq_into,
4005 hankel1,
4006 Scaling::Unscaled,
4007 |x| Complex::new(x, 0.0),
4008 0.1,
4009 ' ',
4010 ),
4011 (
4012 hankel2_seq_into,
4013 hankel2,
4014 Scaling::Unscaled,
4015 |x| Complex::new(-x, 0.0),
4016 0.1,
4017 '2',
4018 ),
4019 (
4020 bessely_seq_into,
4021 bessely,
4022 Scaling::Unscaled,
4023 |x| Complex::new(-x, 0.0),
4024 0.1,
4025 'Y',
4026 ),
4027 (
4028 bessely_seq_into,
4029 bessely_scaled,
4030 Scaling::Exponential,
4031 |x| Complex::new(-x, 0.0),
4032 0.1,
4033 'Y',
4034 ),
4035 (
4036 besselk_seq_into,
4037 besselk,
4038 Scaling::Unscaled,
4039 |x| Complex::new(0.0, x),
4040 0.1,
4041 ' ',
4042 ),
4043 (
4044 besselk_seq_into,
4045 besselk,
4046 Scaling::Unscaled,
4047 |x| Complex::new(0.0, -x),
4048 0.1,
4049 ' ',
4050 ),
4051 (
4052 hankel2_seq_into,
4053 hankel2,
4054 Scaling::Unscaled,
4055 |x| Complex::new(0.0, x),
4056 1e-2,
4057 ' ',
4058 ),
4059 (
4060 hankel2_seq_into,
4061 hankel2_scaled,
4062 Scaling::Exponential,
4063 |x| Complex::new(0.0, x),
4064 1e-2,
4065 ' ',
4066 ),
4067 (
4068 hankel1_seq_into,
4069 hankel1_scaled,
4070 Scaling::Exponential,
4071 |x| Complex::new(0.0, -x),
4072 1e-2,
4073 ' ',
4074 ),
4075 ];
4076 let mut buf = [Complex::new(0.0, 0.0); 70];
4077 for (seq, single, scaling, zf, ratio, neg) in cases {
4078 for nu0 in [-7.3, -6.0, -40.5, 0.5, 2.0, 30.5] {
4079 for n in [1usize, 5, 40, 64, 70] {
4080 // Not x = 1: H2_{±1.5}(i) = 0 exactly (K_{3/2}(1) = π·I_{3/2}(1))
4081 // 1e-10 also reaches the single-value fallback of `fix_seq`
4082 // (a chunk of Y overflows for ν0 = −40.5, n = 64 on −x)
4083 for x in [1e-10, 0.01, 1.3, 7.5, 30.0] {
4084 let z = zf(x);
4085 let out = &mut buf[..n];
4086 if seq(nu0, z, scaling, out).is_err() {
4087 continue;
4088 }
4089 for (j, v) in out.iter().enumerate() {
4090 let nu = nu0 + j as f64;
4091 let Ok(s) = single(nu, z) else { continue };
4092 let (sr, si) = (s.re.abs(), s.im.abs());
4093 let (small, large) = (sr.min(si), sr.max(si));
4094 let jy_ok = ratio == 1e-2
4095 || x < nu.abs()
4096 || neg_axis_selected(neg, nu, usize::from(sr >= si));
4097 if jy_ok && small < 1e-3 * ratio * large && small > 0.0 {
4098 let (gs, ss) = if sr < si { (v.re, s.re) } else { (v.im, s.im) };
4099 let err = ((gs - ss) / ss).abs();
4100 assert!(
4101 err < 1e-12,
4102 "nu={nu}, z={z}, n={n}: seq {v} vs single {s}"
4103 );
4104 } else if s.norm() > 0.0 {
4105 let err = (v - s).norm() / s.norm();
4106 assert!(
4107 err < 1e-10,
4108 "nu={nu}, z={z}, n={n}: seq {v} vs single {s}"
4109 );
4110 // A part recomputed as exactly zero (cos(νπ) = 0 at
4111 // half-integers) must be zero in the sequence too
4112 if jy_ok && small == 0.0 {
4113 let gs = if sr < si { v.re } else { v.im };
4114 assert!(
4115 gs == 0.0,
4116 "nu={nu}, z={z}, n={n}: seq {v} vs single {s}"
4117 );
4118 }
4119 }
4120 }
4121 }
4122 }
4123 }
4124 }
4125 }
4126}
4127
4128#[cfg(test)]
4129mod seq_tests {
4130 use super::*;
4131 use crate::utils::SEQ_WORKSPACE;
4132 use num_complex::Complex64;
4133
4134 /// Longest sequence used below; buffers are fixed arrays so that these
4135 /// tests also run without `std`.
4136 const MAX_N: usize = 128;
4137
4138 fn zeros<T: BesselFloat>() -> [Complex<T>; MAX_N] {
4139 [Complex::new(T::zero(), T::zero()); MAX_N]
4140 }
4141
4142 /// Power series path (ZSERI) where the top orders underflow after the
4143 /// near-underflow scaling was switched on: the lower orders retried after
4144 /// that must keep the scaling consistent (they came out multiplied by TOL,
4145 /// and so did every order below them through the recurrence).
4146 /// Whether a top order underflows is a rounding-level ZUCHK decision on
4147 /// the real axis, so scan several top orders and require at least one hit.
4148 #[test]
4149 fn seq_power_series_underflow_retry_keeps_scaling() {
4150 fn check<T: BesselFloat>(
4151 tops: core::ops::Range<i32>,
4152 ns: &[usize],
4153 z: Complex<T>,
4154 tol: f64,
4155 ) {
4156 for &n in ns {
4157 let mut hits = 0;
4158 for top in tops.clone() {
4159 let nu = T::from_f64(f64::from(top) - (n - 1) as f64);
4160 let mut buf = zeros::<T>();
4161 let values = &mut buf[..n];
4162 let info = besselj_seq_into(nu, z, Scaling::Unscaled, values).unwrap();
4163 if info.underflow_count == 0 || values[0].norm() == T::zero() {
4164 continue;
4165 }
4166 hits += 1;
4167 for (j, &got) in values.iter().enumerate() {
4168 if got.norm() == T::zero() {
4169 continue;
4170 }
4171 let order = nu + T::from_f64(j as f64);
4172 let single = besselj(order, z).unwrap();
4173 let err = ((got - single).norm() / single.norm()).to_f64().unwrap();
4174 // Loose bound: the scaled path near underflow differs from the
4175 // single value by ~1e-13 (as in Fortran); the bug was a factor of TOL.
4176 assert!(
4177 err < tol,
4178 "n={n} order={:?}: {got:?} vs {single:?}",
4179 order.to_f64()
4180 );
4181 }
4182 }
4183 assert!(hits > 0, "n={n}: no case with a partial underflow");
4184 }
4185 }
4186 check(202..219, &[3, 40], Complex64::new(6.5, 0.0), 1e-10);
4187 check(36..48, &[3, 20], Complex::new(6.5f32, 0.0), 1e-4);
4188 }
4189
4190 /// With ν < 0, underflow can be at either end of the sequence: at large
4191 /// |orders| below zero or at the high orders above it. `underflow_count`
4192 /// must count every zero, as Amos does for ν ≥ 0 (including z = 0).
4193 #[test]
4194 fn seq_neg_underflow_count_counts_all_zeros() {
4195 fn check<T: BesselFloat>(radii: &[f64]) {
4196 let mut hits = 0;
4197 for nu in [-0.5, -3.0, -40.3, -100.0, -127.0] {
4198 for &r in radii {
4199 for deg in [0.0, 60.0, 135.0, -100.0] {
4200 let a = f64::to_radians(deg);
4201 let z = Complex::new(T::from_f64(r * a.cos()), T::from_f64(r * a.sin()));
4202 for s in [Scaling::Unscaled, Scaling::Exponential] {
4203 for (name, seq, _, _) in seq_fns::<T>() {
4204 let mut out = zeros::<T>();
4205 let Ok(info) = seq(T::from_f64(nu), z, s, &mut out) else {
4206 continue;
4207 };
4208 let count = out.iter().filter(|v| v.norm() == T::zero()).count();
4209 assert_eq!(
4210 info.underflow_count, count,
4211 "{name} ν={nu} r={r} {deg}°"
4212 );
4213 hits += usize::from(count > 0);
4214 }
4215 }
4216 }
4217 }
4218 }
4219 assert!(hits > 0, "no case with underflow");
4220 }
4221 check::<f64>(&[0.1, 6.5, 40.0, 700.0]);
4222 check::<f32>(&[0.1, 6.5, 40.0, 90.0]);
4223
4224 // Trailing underflow after the orders cross zero (was reported as 0)
4225 let z = Complex64::new(0.1, 0.0);
4226 let mut out = zeros::<f64>();
4227 let info = besselj_seq_into(-0.5, z, Scaling::Unscaled, &mut out).unwrap();
4228 assert!(out[0].norm() > 0.0 && out[MAX_N - 1].norm() == 0.0);
4229 assert!(info.underflow_count > 0);
4230
4231 // z = 0: every order but 0 is zero, counted as for ν ≥ 0
4232 let z = Complex64::new(0.0, 0.0);
4233 for nu in [-2.0, 2.0] {
4234 for seq in [besselj_seq_into::<f64>, besseli_seq_into::<f64>] {
4235 let mut out = [Complex64::new(0.0, 0.0); 5];
4236 let info = seq(nu, z, Scaling::Unscaled, &mut out).unwrap();
4237 let expected = if nu < 0.0 { 4 } else { 5 };
4238 assert_eq!(info.underflow_count, expected, "ν={nu}");
4239 }
4240 }
4241 }
4242
4243 /// Above the half-precision threshold of |z| (~32767 for f64, ~2048 for
4244 /// f32), every path must report `Reduced`: positive ν, the negative
4245 /// lattices, crossings, and Y's chunked K values (n > SEQ_WORKSPACE).
4246 #[test]
4247 fn seq_reports_reduced_accuracy() {
4248 fn check<T: BesselFloat>(r: f64) {
4249 let z = Complex::new(T::from_f64(r * 0.6), T::from_f64(r * 0.8));
4250 for nu in [-80.5, -80.3, -3.0, -2.5, 0.5] {
4251 for n in [5, 70] {
4252 for (name, seq, _, _) in seq_fns::<T>() {
4253 let mut out = zeros::<T>();
4254 let info =
4255 seq(T::from_f64(nu), z, Scaling::Exponential, &mut out[..n]).unwrap();
4256 assert_eq!(info.status, Accuracy::Reduced, "{name} ν={nu} n={n}");
4257 }
4258 }
4259 }
4260 }
4261 check::<f64>(4.0e4);
4262 check::<f32>(3.0e3);
4263 }
4264
4265 // ── Edge: empty output ──
4266
4267 #[test]
4268 fn seq_into_empty_output_is_invalid_input() {
4269 let z = Complex64::new(1.5, 0.5);
4270 let s = Scaling::Unscaled;
4271 // Non-integer and integer ν take different code paths
4272 for nu in [-1.5, -2.0, 1.5] {
4273 for (name, seq, _, _) in seq_fns::<f64>() {
4274 assert_eq!(seq(nu, z, s, &mut []), Err(Error::InvalidInput), "{name}");
4275 }
4276 }
4277 }
4278
4279 // ── Seq/single consistency over a grid of regions ──
4280 //
4281 // Every element of a `_seq` result must match the single-value function at
4282 // the same order. The grid spans negative and positive orders, small to
4283 // near-overflow |z|, all four quadrants, and long sequences, so it reaches
4284 // the recurrence, rescaling, and analytic-continuation paths that only run
4285 // for n > 1.
4286
4287 type SeqIntoFn<T> = fn(T, Complex<T>, Scaling, &mut [Complex<T>]) -> Result<SeqInfo, Error>;
4288 type SingleFn<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
4289 /// (name, seq, single unscaled, single scaled)
4290 type SeqCase<T> = (&'static str, SeqIntoFn<T>, SingleFn<T>, SingleFn<T>);
4291
4292 fn seq_fns<T: BesselFloat>() -> [SeqCase<T>; 6] {
4293 [
4294 ("J", besselj_seq_into, besselj, besselj_scaled),
4295 ("Y", bessely_seq_into, bessely, bessely_scaled),
4296 ("I", besseli_seq_into, besseli, besseli_scaled),
4297 ("K", besselk_seq_into, besselk, besselk_scaled),
4298 ("H1", hankel1_seq_into, hankel1, hankel1_scaled),
4299 ("H2", hankel2_seq_into, hankel2, hankel2_scaled),
4300 ]
4301 }
4302
4303 /// Compare seq[j] with single(ν+j) for every (ν, |z|, arg z) on the grid.
4304 /// A sequence may fail as a whole only if some of its orders fail on their
4305 /// own (e.g. orders below the requested range must not cause an error),
4306 /// with one exception kept from the Fortran: K tests order max(last
4307 /// order, n), up to one order above the last for orders starting below 1,
4308 /// so such K, H, and Y sequences can report `Overflow` just below the
4309 /// limit (e.g. K from ν = 0.5 at z = 0.05 with n = 97). The converse does
4310 /// not hold either: Amos's overflow tests depend on the orders and length
4311 /// of the call, so a single value can report `Overflow` where a sequence
4312 /// that contains it succeeds (as in the Fortran, e.g. f32 K at ν = 10.5
4313 /// alone vs. from ν = 7.5 with n = 9, z = 90·e^{-2.6i}). Elements whose
4314 /// single value is an error, non-finite, or below 1e3 times the smallest
4315 /// normal number are skipped.
4316 fn check_seq_grid<T: BesselFloat>(
4317 nus: &[f64],
4318 radii: &[f64],
4319 args: &[f64],
4320 n: usize,
4321 tol: f64,
4322 ) {
4323 let floor = T::MACH_TINY * T::from_f64(1e3);
4324 for (name, seq, single, single_scaled) in seq_fns::<T>() {
4325 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
4326 let single = if scaling == Scaling::Unscaled {
4327 single
4328 } else {
4329 single_scaled
4330 };
4331 for &nu in nus {
4332 for &r in radii {
4333 for &th in args {
4334 let nu_t = T::from_f64(nu);
4335 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
4336 let mut buf = zeros::<T>();
4337 let values = &mut buf[..n];
4338 match seq(nu_t, z, scaling, values) {
4339 Ok(_) => {}
4340 Err(e) => {
4341 let some_order_fails = (0..n)
4342 .any(|j| single(nu_t + T::from_f64(j as f64), z).is_err());
4343 assert!(
4344 some_order_fails,
4345 "{name} {scaling:?}: nu={nu}, z={z:?}, n={n}: seq \
4346 failed with {e:?} but every single value succeeds"
4347 );
4348 continue;
4349 }
4350 }
4351 for (j, &w) in values.iter().enumerate() {
4352 let order = nu_t + T::from_f64(j as f64);
4353 let Ok(v) = single(order, z) else {
4354 continue;
4355 };
4356 if !(v.norm() > floor && v.norm().is_finite()) {
4357 continue;
4358 }
4359 let err = ((w - v).norm() / v.norm()).to_f64().unwrap();
4360 assert!(
4361 err < tol,
4362 "{name} {scaling:?}: nu={nu}, z={z:?}, n={n}, j={j}: \
4363 seq={w:?}, single={v:?}, rel_err={err:e}"
4364 );
4365 }
4366 }
4367 }
4368 }
4369 }
4370 }
4371 }
4372
4373 /// Angles covering all four quadrants, the real and imaginary axes, and
4374 /// both sides of the |Im z| = √3·|Re z| region boundary.
4375 const ARGS: [f64; 8] = [
4376 0.0,
4377 0.5,
4378 1.2,
4379 core::f64::consts::FRAC_PI_2,
4380 2.2,
4381 3.0,
4382 -1.0,
4383 -2.6,
4384 ];
4385
4386 /// Recurrence paths in f64. The seq values come from recurrence while the
4387 /// single values come from direct expansions, so their ~1e-14 errors do not
4388 /// cancel (observed up to ~6e-12 for n = 100); a misplaced or mis-scaled
4389 /// element gives O(1e-3) error or worse.
4390 const TOL_GRID_F64: f64 = 1e-10;
4391
4392 #[test]
4393 fn seq_matches_single_grid_f64() {
4394 // Orders: all-negative / crossing (integer and not), small, and beyond
4395 // FNUL (≈ 86) so the uniform expansions recur. Radii include values near
4396 // the ELIM/ALIM scaling thresholds (≈ 665–700) and beyond, where low
4397 // orders overflow but ν = -200 does not. ν = 575 at |z| = 360 reaches
4398 // scaled Y with 2|Im z| > ELIM, where the K term still dominates. -0.0
4399 // is an edge case of the sign dispatch; n = 1 is the one-element
4400 // lattice edge case. For n = 9, ν = -3 / -4 / -7 cover an integer
4401 // crossing whose largest |order| is positive / both / negative.
4402 for n in [1, 9] {
4403 check_seq_grid::<f64>(
4404 &[
4405 -200.5, -200.0, -20.0, -12.3, -7.0, -5.3, -4.0, -3.0, -2.5, -0.0, 0.0, 0.4,
4406 13.5, 46.2, 81.7, 121.3, 575.0,
4407 ],
4408 &[
4409 0.3, 3.0, 17.0, 45.0, 120.0, 330.0, 360.0, 666.0, 698.0, 705.0, 709.0,
4410 ],
4411 &ARGS,
4412 n,
4413 TOL_GRID_F64,
4414 );
4415 }
4416 }
4417
4418 #[test]
4419 fn seq_matches_single_grid_f64_long() {
4420 // n = 100 with small ν: ν + n − 1 exceeds FNUL while ν itself does not.
4421 // Negative non-integer ν spans several stack chunks of the secondary
4422 // function (all negative, and crossing zero with more negative orders
4423 // than a chunk holds).
4424 check_seq_grid::<f64>(
4425 &[-80.3, -40.3, 0.5, 3.3],
4426 &[2.2, 25.0, 90.0],
4427 &ARGS,
4428 100,
4429 TOL_GRID_F64,
4430 );
4431 }
4432
4433 /// f32 hits the scaling thresholds at |values| ≈ 1e28, so moderate |z|
4434 /// exercises the rescaling branches. Observed up to ~1.4e-4 on this grid;
4435 /// misplaced or mis-scaled elements give O(1) error or worse. (Off this grid,
4436 /// near the turning point ν ≈ |z| ≈ FNUL, Amos's single-precision algorithm
4437 /// itself loses up to ~2e-2, so random points would need a looser bound.)
4438 const TOL_GRID_F32: f64 = 1e-3;
4439
4440 #[test]
4441 fn seq_matches_single_grid_f32() {
4442 // At |z| = 90, low orders overflow but ν = -60 does not. ν = 70 at
4443 // |z| = 45 reaches scaled Y with 2|Im z| > ELIM (K term dominant).
4444 check_seq_grid::<f32>(
4445 &[
4446 -60.0, -25.5, -9.0, -4.6, -0.0, 0.0, 0.4, 7.5, 21.2, 29.9, 46.3, 70.0,
4447 ],
4448 &[0.3, 3.0, 17.0, 45.0, 70.0, 90.0],
4449 &ARGS,
4450 9,
4451 TOL_GRID_F32,
4452 );
4453 }
4454
4455 // ── Three-term recurrence across ν = 0 ──
4456 //
4457 // Negative orders come from reflection formulas shared by the single-value
4458 // and `_seq` functions, so comparing the two cannot catch an error in a
4459 // formula. The recurrence can: across ν = 0 it links reflected values to
4460 // directly computed ones. Every scaling factor is independent of the order,
4461 // so scaled values satisfy the same recurrence.
4462
4463 /// Check C(ν-1) + s·C(ν+1) = t·(2ν/z)·C(ν) on consecutive `_seq` elements,
4464 /// with (s, t) = (1, 1) for J, Y, H1, H2, (-1, 1) for I, and (-1, -1) for K
4465 /// (DLMF 10.6.1, 10.29.1). The residual is relative to the largest term.
4466 fn check_recurrence_grid<T: BesselFloat>(
4467 nus: &[f64],
4468 radii: &[f64],
4469 args: &[f64],
4470 n: usize,
4471 tol: f64,
4472 ) {
4473 for (name, seq, _, _) in seq_fns::<T>() {
4474 let (s, t) = match name {
4475 "I" => (-1.0, 1.0),
4476 "K" => (-1.0, -1.0),
4477 _ => (1.0, 1.0),
4478 };
4479 let (s, t) = (T::from_f64(s), T::from_f64(t));
4480 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
4481 for &nu in nus {
4482 for &r in radii {
4483 for &th in args {
4484 let nu_t = T::from_f64(nu);
4485 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
4486 let mut buf = zeros::<T>();
4487 let values = &mut buf[..n];
4488 if seq(nu_t, z, scaling, values).is_err() {
4489 continue;
4490 }
4491 for (j, w) in values.windows(3).enumerate() {
4492 // Underflowed (zeroed) or overflowed elements break the recurrence
4493 if w.iter()
4494 .any(|v| v.norm() == T::zero() || !v.norm().is_finite())
4495 {
4496 continue;
4497 }
4498 let order = nu_t + T::from_f64((j + 1) as f64);
4499 let lhs = w[0] + w[2] * s;
4500 let rhs = w[1] * (z.inv() * (T::from_f64(2.0) * order * t));
4501 let mag = w[0].norm().max(w[2].norm()).max(rhs.norm());
4502 let err = ((lhs - rhs).norm() / mag).to_f64().unwrap();
4503 assert!(
4504 err < tol,
4505 "{name} {scaling:?}: nu={nu}, z={z:?}, order={:?}: \
4506 recurrence residual {err:e}",
4507 order.to_f64()
4508 );
4509 }
4510 }
4511 }
4512 }
4513 }
4514 }
4515 }
4516
4517 #[test]
4518 fn seq_recurrence_across_zero_f64() {
4519 // Non-integer (generic and half-integer) and integer orders crossing
4520 // zero, plus all-negative sequences.
4521 check_recurrence_grid::<f64>(
4522 &[-12.3, -5.3, -4.0, -3.7, -2.5, -0.3],
4523 &[0.3, 3.0, 17.0, 45.0],
4524 &ARGS,
4525 9,
4526 1e-11, // observed up to ~1.3e-13
4527 );
4528 }
4529
4530 #[test]
4531 fn seq_recurrence_across_zero_f32() {
4532 check_recurrence_grid::<f32>(
4533 &[-12.3, -5.3, -4.0, -3.7, -2.5, -0.3],
4534 &[0.3, 3.0, 17.0, 45.0],
4535 &ARGS,
4536 9,
4537 1e-4, // observed up to ~1e-5
4538 );
4539 }
4540
4541 // ── Chunked workspace ──
4542 //
4543 // Y's K values and the secondary function of the negative non-integer
4544 // reflection formulas go to a workspace, in chunks of its length (Y, and
4545 // I with non-integer ν < 0) or of half of it (J and Y with non-integer
4546 // ν < 0, except half-integer ν, which need one function). A workspace
4547 // that covers the whole
4548 // sequence (as `_seq` allocates) gives one Amos call per function, so
4549 // compare short workspaces against it. Amos's tests depend on the orders
4550 // and length of each call, so a chunked sequence may fail only where some
4551 // single value fails, as above.
4552
4553 type ImplFn<T> = for<'a> fn(
4554 T,
4555 Complex<T>,
4556 Scaling,
4557 &mut [Complex<T>],
4558 Workspace<'a, T>,
4559 ) -> Result<SeqInfo, Error>;
4560
4561 /// (name, implementation, single unscaled, single scaled)
4562 type ImplCase<T> = (&'static str, ImplFn<T>, SingleFn<T>, SingleFn<T>);
4563
4564 fn impl_fns<T: BesselFloat>() -> [ImplCase<T>; 3] {
4565 [
4566 ("J", besselj_seq_impl, besselj, besselj_scaled),
4567 ("Y", bessely_seq_impl, bessely, bessely_scaled),
4568 ("I", besseli_seq_impl, besseli, besseli_scaled),
4569 ]
4570 }
4571
4572 /// Bitwise equality (exact through the lossless conversion to f64).
4573 fn same_bits<T: BesselFloat>(x: Complex<T>, y: Complex<T>) -> bool {
4574 let bits = |v: T| v.to_f64().unwrap().to_bits();
4575 bits(x.re) == bits(y.re) && bits(x.im) == bits(y.im)
4576 }
4577
4578 fn check_chunk_invariance<T: BesselFloat>(
4579 nus: &[f64],
4580 radii: &[f64],
4581 ns: &[usize],
4582 ws_len: usize,
4583 tol: f64,
4584 ) {
4585 let floor = T::MACH_TINY * T::from_f64(1e3);
4586 let czero = Complex::new(T::zero(), T::zero());
4587 for (name, f, single, single_scaled) in impl_fns::<T>() {
4588 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
4589 let single = if scaling == Scaling::Unscaled {
4590 single
4591 } else {
4592 single_scaled
4593 };
4594 for &nu in nus {
4595 for &r in radii {
4596 for &th in &ARGS {
4597 for &n in ns {
4598 let nu_t = T::from_f64(nu);
4599 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
4600 let (mut a, mut b) = (zeros::<T>(), zeros::<T>());
4601 let (a, b) = (&mut a[..n], &mut b[..n]);
4602 let mut ws_whole = [czero; 2 * MAX_N];
4603 let mut ws_short = [czero; 2 * MAX_N];
4604 let whole = f(nu_t, z, scaling, a, Workspace::Slice(&mut ws_whole));
4605 let chunked = f(
4606 nu_t,
4607 z,
4608 scaling,
4609 b,
4610 Workspace::Slice(&mut ws_short[..ws_len]),
4611 );
4612 let some_single_fails = || {
4613 (0..n).any(|j| single(nu_t + T::from_f64(j as f64), z).is_err())
4614 };
4615 let (info_a, info_b) = match (whole, chunked) {
4616 (Ok(ia), Ok(ib)) => (ia, ib),
4617 (Ok(_), Err(e)) => {
4618 assert!(
4619 some_single_fails(),
4620 "{name} {scaling:?} ws={ws_len}: nu={nu}, z={z:?}, \
4621 n={n}: chunked failed with {e:?}, whole call and \
4622 every single value succeed"
4623 );
4624 continue;
4625 }
4626 // Chunks can pass Amos's tests where the whole call
4627 // fails (e.g. K's test of max(last order, n)); the
4628 // values must then match the single values.
4629 (Err(_), Ok(_)) => {
4630 for (j, &y) in b.iter().enumerate() {
4631 let Ok(v) = single(nu_t + T::from_f64(j as f64), z)
4632 else {
4633 continue;
4634 };
4635 if !(v.norm() > floor && v.norm().is_finite()) {
4636 continue;
4637 }
4638 let err = ((y - v).norm() / v.norm()).to_f64().unwrap();
4639 assert!(
4640 err < tol,
4641 "{name} {scaling:?} ws={ws_len}: nu={nu}, \
4642 z={z:?}, n={n}, j={j}: chunked={y:?}, \
4643 single={v:?}"
4644 );
4645 }
4646 continue;
4647 }
4648 (Err(_), Err(_)) => continue,
4649 };
4650 if 2 * n <= ws_len {
4651 // One chunk: the same calls as the whole sequence
4652 assert_eq!(
4653 info_a, info_b,
4654 "{name} {scaling:?} ws={ws_len}: nu={nu}, n={n}"
4655 );
4656 for (&x, &y) in a.iter().zip(b.iter()) {
4657 assert!(
4658 same_bits(x, y),
4659 "{name} {scaling:?} ws={ws_len}: nu={nu}, z={z:?}, \
4660 n={n}: {x:?} vs {y:?}"
4661 );
4662 }
4663 continue;
4664 }
4665 // The underflow count is decided per call, so only the
4666 // status must match
4667 assert_eq!(
4668 info_a.status, info_b.status,
4669 "{name} {scaling:?} ws={ws_len}: nu={nu}, n={n}"
4670 );
4671 for (j, (&x, &y)) in a.iter().zip(b.iter()).enumerate() {
4672 if !(x.norm() > floor && x.norm().is_finite()) {
4673 continue;
4674 }
4675 let err = ((x - y).norm() / x.norm()).to_f64().unwrap();
4676 assert!(
4677 err < tol,
4678 "{name} {scaling:?} ws={ws_len}: nu={nu}, z={z:?}, \
4679 n={n}, j={j}: whole={x:?}, chunked={y:?}, \
4680 rel_err={err:e}"
4681 );
4682 }
4683 }
4684 }
4685 }
4686 }
4687 }
4688 }
4689 }
4690
4691 /// Workspace lengths: chunks of 1, 2, 7 orders for J and Y with non-integer,
4692 /// non-half-integer ν < 0 (half the workspace) and of 2, 4, 14 otherwise,
4693 /// and the stack workspace.
4694 const WS_LENS: [usize; 4] = [2, 4, 14, SEQ_WORKSPACE];
4695
4696 #[test]
4697 fn seq_chunk_size_invariance_f64() {
4698 // Negative non-integer ν (all negative, and crossing zero) chunk the
4699 // secondary function; positive ν and negative integer ν (both mirror
4700 // branches of a crossing: n = 100 from −40 and from −70) chunk the K
4701 // values inside Y. Radii reach the ELIM/ALIM thresholds. With the
4702 // stack workspace, n = 32 and 33 end at a chunk boundary or one past it
4703 // for J and Y with non-integer ν < 0, and n = 9 is a single chunk.
4704 let nus = [
4705 -80.3, -70.0, -40.5, -40.3, -40.0, -12.3, -0.7, 0.4, 3.3, 81.7,
4706 ];
4707 let radii = [0.3, 3.0, 17.0, 45.0, 120.0, 666.0, 698.0, 705.0];
4708 let ns = [9, 32, 33, 65, 100];
4709 for ws_len in WS_LENS {
4710 check_chunk_invariance::<f64>(&nus, &radii, &ns, ws_len, TOL_CHUNK_F64);
4711 }
4712 }
4713
4714 #[test]
4715 fn seq_chunk_size_invariance_f32() {
4716 // Thresholds sit at |z| ≈ 34–100 in f32; z = 90·e^{-2.6i} is where K's
4717 // overflow test depends on the call (see `check_seq_grid`).
4718 let nus = [-70.0, -40.3, -40.0, -15.5, -4.6, 0.4, 7.5, 29.9];
4719 let radii = [3.0, 17.0, 34.0, 45.0, 70.0, 90.0, 100.0];
4720 let ns = [9, 32, 33, 65, 100];
4721 for ws_len in WS_LENS {
4722 check_chunk_invariance::<f32>(&nus, &radii, &ns, ws_len, TOL_CHUNK_F32);
4723 }
4724 }
4725
4726 /// Chunks restart Amos's algorithms, so their values differ from the whole
4727 /// call as single values do, plus where Amos's own error is largest: on
4728 /// these grids up to ~1.4e-9 in f64 (Y near |z| = 705 on the real axis,
4729 /// where both differ from mpmath by ~3e-9) and ~3.9e-3 in f32 (near the
4730 /// turning point). Off the grids, cancellation in the reflection formulas
4731 /// can reach ~1.3e-8 (J, ν = −250.7, z = 698, where |J| ≈ 5e-7 comes from
4732 /// J and Y terms ≈ 3e-2); the whole call itself shifts by ~3e-9 there with
4733 /// n. A misplaced or mis-scaled chunk gives O(1) error.
4734 const TOL_CHUNK_F64: f64 = 1e-8;
4735 const TOL_CHUNK_F32: f64 = 2e-2;
4736
4737 // ── Output buffer contents ──
4738
4739 #[test]
4740 fn seq_into_ignores_initial_out_contents() {
4741 // Every path must overwrite `out`; NaN left behind would show up
4742 let z_list = [Complex64::new(1.5, 0.5), Complex64::new(-30.0, 20.0)];
4743 for (name, seq, _, _) in seq_fns::<f64>() {
4744 for nu in [-40.3, -7.0, -2.5, 0.4, 3.0] {
4745 for z in z_list {
4746 for n in [1, 9, 40] {
4747 let mut a = [Complex64::new(0.0, 0.0); 40];
4748 let mut b = [Complex64::new(f64::NAN, f64::NAN); 40];
4749 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
4750 let ra = seq(nu, z, scaling, &mut a[..n]);
4751 let rb = seq(nu, z, scaling, &mut b[..n]);
4752 assert_eq!(ra, rb, "{name}: nu={nu}, z={z:?}, n={n}");
4753 if ra.is_err() {
4754 continue;
4755 }
4756 for (&x, &y) in a[..n].iter().zip(b[..n].iter()) {
4757 assert!(
4758 same_bits(x, y),
4759 "{name}: nu={nu}, z={z:?}, n={n}: {x:?} vs {y:?}"
4760 );
4761 }
4762 }
4763 }
4764 }
4765 }
4766 }
4767 }
4768}
4769
4770// ── Allocating `_seq` wrappers ──
4771
4772#[cfg(all(test, feature = "std"))]
4773mod seq_vec_tests {
4774 use super::*;
4775 use num_complex::Complex64;
4776
4777 /// `_seq` gives every function a heap workspace of the full length, so each
4778 /// is one Amos call (as in 0.4), unlike the stack chunks of `_seq_into`.
4779 /// These long sequences span several stack chunks.
4780 #[test]
4781 fn seq_computes_each_function_in_one_call() {
4782 type Seq = fn(f64, Complex64, usize, Scaling) -> Result<BesselResult<f64>, Error>;
4783 type Impl = for<'a> fn(
4784 f64,
4785 Complex64,
4786 Scaling,
4787 &mut [Complex64],
4788 Workspace<'a, f64>,
4789 ) -> Result<SeqInfo, Error>;
4790 let cases: [(Seq, Impl, f64, Complex64); 6] = [
4791 (
4792 bessely_seq,
4793 bessely_seq_impl,
4794 0.4,
4795 Complex64::from_polar(25.0, 0.5),
4796 ),
4797 (
4798 bessely_seq,
4799 bessely_seq_impl,
4800 -40.3,
4801 Complex64::from_polar(17.0, -1.0),
4802 ),
4803 (
4804 besselj_seq,
4805 besselj_seq_impl,
4806 -80.3,
4807 Complex64::from_polar(45.0, 3.0),
4808 ),
4809 (
4810 besseli_seq,
4811 besseli_seq_impl,
4812 -40.3,
4813 Complex64::from_polar(17.0, 2.2),
4814 ),
4815 // Half-integer |ν|: one function for the negative orders
4816 (
4817 besselj_seq,
4818 besselj_seq_impl,
4819 -80.5,
4820 Complex64::from_polar(45.0, 3.0),
4821 ),
4822 (
4823 bessely_seq,
4824 bessely_seq_impl,
4825 -80.5,
4826 Complex64::from_polar(17.0, -1.0),
4827 ),
4828 ];
4829 const N: usize = 100;
4830 for (seq, imp, nu, z) in cases {
4831 let r = seq(nu, z, N, Scaling::Unscaled).unwrap();
4832 let mut out = [Complex64::new(0.0, 0.0); N];
4833 let mut ws = [Complex64::new(0.0, 0.0); 2 * N];
4834 let info = imp(
4835 nu,
4836 z,
4837 Scaling::Unscaled,
4838 &mut out,
4839 Workspace::Slice(&mut ws),
4840 )
4841 .unwrap();
4842 assert_eq!(
4843 (r.underflow_count, r.status),
4844 (info.underflow_count, info.status)
4845 );
4846 for (j, (x, y)) in r.values.iter().zip(out.iter()).enumerate() {
4847 assert!(
4848 x.re.to_bits() == y.re.to_bits() && x.im.to_bits() == y.im.to_bits(),
4849 "nu={nu}, z={z:?}, j={j}: {x:?} vs {y:?}"
4850 );
4851 }
4852 }
4853 }
4854
4855 #[test]
4856 fn seq_matches_seq_into() {
4857 type Pair = (
4858 fn(f64, Complex64, usize, Scaling) -> Result<BesselResult<f64>, Error>,
4859 fn(f64, Complex64, Scaling, &mut [Complex64]) -> Result<SeqInfo, Error>,
4860 );
4861 let pairs: [Pair; 6] = [
4862 (besselj_seq, besselj_seq_into),
4863 (bessely_seq, bessely_seq_into),
4864 (besseli_seq, besseli_seq_into),
4865 (besselk_seq, besselk_seq_into),
4866 (hankel1_seq, hankel1_seq_into),
4867 (hankel2_seq, hankel2_seq_into),
4868 ];
4869 let z = Complex64::new(1.5, -0.5);
4870 for (seq, seq_into) in pairs {
4871 for nu in [-5.3, -2.0, 0.0, 1.7] {
4872 for n in [0, 1, 6] {
4873 let mut out = [Complex64::new(0.0, 0.0); 6];
4874 let expected = seq_into(nu, z, Scaling::Unscaled, &mut out[..n]);
4875 match seq(nu, z, n, Scaling::Unscaled) {
4876 Ok(r) => {
4877 let info = expected.unwrap();
4878 assert_eq!(r.values, &out[..n]);
4879 assert_eq!(r.underflow_count, info.underflow_count);
4880 assert_eq!(r.status, info.status);
4881 }
4882 Err(e) => assert_eq!(Err(e), expected),
4883 }
4884 }
4885 }
4886 }
4887 }
4888}