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, Bi
7//! - **Consecutive orders** — `_seq` variants return ν, ν+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` support** — 3-tier: bare `no_std`, `alloc`, `std` (default)
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)` | 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)` | 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)` | 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)` | 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)` | 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)` | 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//! ## Exponential scaling
50//!
51//! Each `_scaled` variant multiplies the result by an exponential factor that
52//! cancels the asymptotic growth (or decay), keeping values in a representable
53//! floating-point range. The exact factor is listed in the "Scaled variant
54//! returns" column above. Use scaled functions whenever |z| or |Im(z)| is
55//! large enough that the unscaled result would overflow or underflow.
56//!
57//! # Negative orders
58//!
59//! All functions accept any real order, including negative values.
60//! DLMF reflection formulas are applied automatically:
61//!
62//! - **J**: J<sub>−ν</sub>(z) = cos(νπ) J<sub>ν</sub>(z) − sin(νπ) Y<sub>ν</sub>(z) (DLMF 10.2.3)
63//! - **Y**: Y<sub>−ν</sub>(z) = sin(νπ) J<sub>ν</sub>(z) + cos(νπ) Y<sub>ν</sub>(z) (DLMF 10.2.3)
64//! - **I**: I<sub>−ν</sub>(z) = I<sub>ν</sub>(z) + (2/π) sin(νπ) K<sub>ν</sub>(z) (DLMF 10.27.2)
65//! - **K**: K<sub>−ν</sub>(z) = K<sub>ν</sub>(z) (DLMF 10.27.3)
66//! - **H<sup>(1)</sup>**: H<sup>(1)</sup><sub>−ν</sub>(z) = exp(νπi) H<sup>(1)</sup><sub>ν</sub>(z) (DLMF 10.4.6)
67//! - **H<sup>(2)</sup>**: H<sup>(2)</sup><sub>−ν</sub>(z) = exp(−νπi) H<sup>(2)</sup><sub>ν</sub>(z) (DLMF 10.4.6)
68//!
69//! For integer orders, simplified identities are used (e.g., J<sub>−n</sub>(z) = (−1)<sup>n</sup> J<sub>n</sub>(z)).
70//!
71//! # Function variants
72//!
73//! ## Consecutive orders
74//!
75//! The `_seq` variants ([`besselj_seq`], [`besselk_seq`], …) compute values at
76//! consecutive orders ν, ν+1, …, ν+n−1 in a single call and return
77//! a [`BesselResult`] that includes an [`Accuracy`] field.
78//! The `_raw` Airy variants ([`airy_raw`], [`biry_raw`], …) similarly return
79//! an [`AiryResult`] with [`Accuracy`].
80//! All other functions return only the computed value without [`Accuracy`].
81//!
82//! **[`Accuracy`]:**
83//!
84//! | Status | Meaning |
85//! |--------|---------|
86//! | [`Normal`](Accuracy::Normal) | Full machine precision |
87//! | [`Reduced`](Accuracy::Reduced) | More than half of significant digits may be lost |
88//!
89//! [`Reduced`](Accuracy::Reduced) is extremely rare in practice. SciPy's Bessel wrappers also silently
90//! discard the equivalent Amos IERR=3 flag by default.
91//!
92//! To check accuracy status, use a `_seq` function (Bessel) or a `_raw` function (Airy):
93//!
94//! ```
95//! # #[cfg(feature = "alloc")] {
96//! use complex_bessel::*;
97//! use num_complex::Complex;
98//!
99//! let z = Complex::new(1.0, 2.0);
100//! let result = besselk_seq(0.0, z, 1, Scaling::Unscaled).unwrap();
101//! assert!(matches!(result.status, Accuracy::Normal));
102//!
103//! let result = airy_raw(z, Scaling::Unscaled).unwrap();
104//! assert!(matches!(result.status, Accuracy::Normal));
105//! # }
106//! ```
107//!
108//! # Error handling
109//!
110//! All functions return `Result<_, Error>`. The four error variants are:
111//!
112//! | Variant | Cause |
113//! |---------|-------|
114//! | [`InvalidInput`](Error::InvalidInput) | non-finite z or ν, z = 0 for K/Y/H, n < 1 |
115//! | [`Overflow`](Error::Overflow) | \|z\| or \|ν\| too large (or \|z\| too small) for finite result |
116//! | [`TotalPrecisionLoss`](Error::TotalPrecisionLoss) | \|z\| or \|ν\| too large for meaningful computation |
117//! | [`ConvergenceFailure`](Error::ConvergenceFailure) | Internal algorithm did not converge |
118//!
119//! # `no_std` support
120//!
121//! | Cargo features | Available API |
122//! |---------------|---------------|
123//! | `default-features = false` | 24 single-value functions |
124//! | `features = ["alloc"]` | full API (+ 6 `_seq` variants) |
125//! | `features = ["std"]` (default) | full API, with platform-native math |
126//!
127//! The 24 single-value functions include 12 Bessel (J/Y/I/K/H<sup>(1)</sup>/H<sup>(2)</sup> × unscaled/scaled)
128//! and 12 Airy (Ai/Ai'/Bi/Bi' × unscaled/scaled/raw).
129//!
130//! ```toml
131//! # Bare no_std — no allocator needed:
132//! complex-bessel = { version = "0.4", default-features = false }
133//!
134//! # no_std + alloc — full API:
135//! complex-bessel = { version = "0.4", default-features = false, features = ["alloc"] }
136//! ```
137
138#![warn(missing_docs)]
139#![no_std]
140
141#[cfg(feature = "alloc")]
142extern crate alloc;
143#[cfg(feature = "std")]
144extern crate std;
145
146pub(crate) mod airy;
147pub(crate) mod algo;
148pub(crate) mod besh;
149pub(crate) mod besi;
150pub(crate) mod besj;
151pub(crate) mod besk;
152pub(crate) mod besy;
153pub mod machine;
154pub mod types;
155pub(crate) mod utils;
156
157pub use machine::BesselFloat;
158#[cfg(feature = "alloc")]
159pub use types::BesselResult;
160pub use types::{Accuracy, AiryResult, Error, Scaling};
161
162use num_complex::Complex;
163use types::{AiryDerivative, HankelKind};
164
165// ── Helper: integer order detection ──
166
167/// Check if `nu` is a non-negative integer. Returns `Some(n)` if so.
168#[inline]
169fn as_integer<T: BesselFloat>(nu: T) -> Option<i64> {
170 if nu == nu.floor() {
171 // Safe conversion: orders beyond i64 range are not practical
172 nu.to_i64()
173 } else {
174 None
175 }
176}
177
178// ── Element-wise reflection helpers (shared by single-value and _seq) ──
179
180/// J_{-ν}(z) = cos(νπ)·J_ν(z) − sin(νπ)·Y_ν(z) (DLMF 10.2.3)
181#[inline]
182fn reflect_j_element<T: BesselFloat>(order: T, j: Complex<T>, y: Complex<T>) -> Complex<T> {
183 j * utils::cospi(order) - y * utils::sinpi(order)
184}
185
186/// Y_{-ν}(z) = sin(νπ)·J_ν(z) + cos(νπ)·Y_ν(z) (DLMF 10.2.3)
187#[inline]
188fn reflect_y_element<T: BesselFloat>(order: T, j: Complex<T>, y: Complex<T>) -> Complex<T> {
189 j * utils::sinpi(order) + y * utils::cospi(order)
190}
191
192/// I_{-ν}(z) = I_ν(z) + (2/π)·sin(νπ)·K_ν(z) (DLMF 10.27.2)
193/// `k_val` must already have scaling correction applied by the caller.
194#[inline]
195fn reflect_i_element<T: BesselFloat>(order: T, i_val: Complex<T>, k_val: Complex<T>) -> Complex<T> {
196 let pi = T::from_f64(core::f64::consts::PI);
197 let two = T::from_f64(2.0);
198 utils::mul_add_scalar(k_val, two / pi * utils::sinpi(order), i_val)
199}
200
201/// H^(m)_{-ν}(z) = exp(±νπi)·H^(m)_ν(z) (DLMF 10.4.6/7)
202#[inline]
203fn reflect_h_element<T: BesselFloat>(order: T, kind: HankelKind, h: Complex<T>) -> Complex<T> {
204 let cos_nu_pi = utils::cospi(order);
205 let sin_nu_pi = utils::sinpi(order);
206 let rotation = match kind {
207 HankelKind::First => Complex::new(cos_nu_pi, sin_nu_pi),
208 HankelKind::Second => Complex::new(cos_nu_pi, -sin_nu_pi),
209 };
210 h * rotation
211}
212
213/// Convert K scaling to I scaling for the reflection formula.
214/// I_scaled = exp(-|Re(z)|)·I, K_scaled = exp(z)·K.
215/// factor = exp(-|Re(z)|) / exp(z) = exp(-i·Im(z)) · [exp(-2·Re(z)) if Re(z)>0]
216#[inline]
217fn k_to_i_scaling_correction<T: BesselFloat>(z: Complex<T>, k_val: Complex<T>) -> Complex<T> {
218 let two = T::from_f64(2.0);
219 let (sin_a, cos_a) = (-z.im).sin_cos();
220 let mut result = k_val * Complex::new(cos_a, sin_a);
221 if z.re > T::zero() {
222 result = result * (-two * z.re).exp();
223 }
224 result
225}
226
227/// (-1)^n sign factor for integer order reflection.
228#[inline]
229fn integer_sign<T: BesselFloat>(n: i64) -> T {
230 if n % 2 == 0 { T::one() } else { -T::one() }
231}
232
233// ── Internal: compute with given scaling for negative order support ──
234
235#[inline]
236fn besselj_internal<T: BesselFloat>(
237 nu: T,
238 z: Complex<T>,
239 scaling: Scaling,
240) -> Result<Complex<T>, Error> {
241 let zero = T::zero();
242 let czero = Complex::new(zero, zero);
243 if nu >= zero {
244 let mut y = [czero];
245 besj::zbesj(z, nu, scaling, &mut y)?;
246 return Ok(y[0]);
247 }
248
249 let abs_nu = nu.abs();
250
251 // Integer shortcut: J_{-n}(z) = (-1)^n * J_n(z)
252 if let Some(n) = as_integer(abs_nu) {
253 let mut y = [czero];
254 besj::zbesj(z, abs_nu, scaling, &mut y)?;
255 return Ok(y[0] * integer_sign::<T>(n));
256 }
257
258 // General case: need both J and Y at positive |ν|
259 let mut j_buf = [czero];
260 let mut y_buf = [czero];
261 besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
262 besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
263
264 Ok(reflect_j_element(abs_nu, j_buf[0], y_buf[0]))
265}
266
267#[inline]
268fn bessely_internal<T: BesselFloat>(
269 nu: T,
270 z: Complex<T>,
271 scaling: Scaling,
272) -> Result<Complex<T>, Error> {
273 let zero = T::zero();
274 let czero = Complex::new(zero, zero);
275 if nu >= zero {
276 let mut y = [czero];
277 besy::zbesy(z, nu, scaling, &mut y)?;
278 return Ok(y[0]);
279 }
280
281 let abs_nu = nu.abs();
282
283 // Integer shortcut: Y_{-n}(z) = (-1)^n * Y_n(z)
284 if let Some(n) = as_integer(abs_nu) {
285 let mut y = [czero];
286 besy::zbesy(z, abs_nu, scaling, &mut y)?;
287 return Ok(y[0] * integer_sign::<T>(n));
288 }
289
290 // General case: need both J and Y at positive |ν|
291 let mut j_buf = [czero];
292 let mut y_buf = [czero];
293 besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
294 besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
295
296 Ok(reflect_y_element(abs_nu, j_buf[0], y_buf[0]))
297}
298
299#[inline]
300fn besseli_internal<T: BesselFloat>(
301 nu: T,
302 z: Complex<T>,
303 scaling: Scaling,
304) -> Result<Complex<T>, Error> {
305 let zero = T::zero();
306 let czero = Complex::new(zero, zero);
307 if nu >= zero {
308 let mut y = [czero];
309 besi::zbesi(z, nu, scaling, &mut y)?;
310 return Ok(y[0]);
311 }
312
313 let abs_nu = nu.abs();
314
315 // Integer shortcut: I_{-n}(z) = I_n(z)
316 if as_integer(abs_nu).is_some() {
317 let mut y = [czero];
318 besi::zbesi(z, abs_nu, scaling, &mut y)?;
319 return Ok(y[0]);
320 }
321
322 // General case: need both I and K at positive |ν|
323 let mut i_buf = [czero];
324 let mut k_buf = [czero];
325 besi::zbesi(z, abs_nu, scaling, &mut i_buf)?;
326 besk::zbesk(z, abs_nu, scaling, &mut k_buf)?;
327
328 let mut k_val = k_buf[0];
329 if scaling == Scaling::Exponential {
330 k_val = k_to_i_scaling_correction(z, k_val);
331 }
332
333 Ok(reflect_i_element(abs_nu, i_buf[0], k_val))
334}
335
336#[inline]
337fn besselk_internal<T: BesselFloat>(
338 nu: T,
339 z: Complex<T>,
340 scaling: Scaling,
341) -> Result<Complex<T>, Error> {
342 // K_{-ν}(z) = K_ν(z) (DLMF 10.27.3)
343 let abs_nu = nu.abs();
344 let zero = T::zero();
345 let mut y = [Complex::new(zero, zero)];
346 besk::zbesk(z, abs_nu, scaling, &mut y)?;
347 Ok(y[0])
348}
349
350#[inline]
351fn hankel_internal<T: BesselFloat>(
352 kind: HankelKind,
353 nu: T,
354 z: Complex<T>,
355 scaling: Scaling,
356) -> Result<Complex<T>, Error> {
357 let zero = T::zero();
358 if nu >= zero {
359 let mut y = [Complex::new(zero, zero)];
360 besh::zbesh(z, nu, kind, scaling, &mut y)?;
361 return Ok(y[0]);
362 }
363
364 let abs_nu = nu.abs();
365 let mut y = [Complex::new(zero, zero)];
366 besh::zbesh(z, abs_nu, kind, scaling, &mut y)?;
367
368 Ok(reflect_h_element(abs_nu, kind, y[0]))
369}
370
371// ── Single-value convenience functions ──
372
373/// Bessel function of the first kind, J<sub>ν</sub>(z).
374///
375/// Computes a single value of J_ν(z) for complex z and real order ν
376/// (any real value, including negative).
377///
378/// For negative ν, the DLMF 10.2.3 reflection formula is applied:
379/// `J_{-ν}(z) = cos(νπ) J_ν(z) - sin(νπ) Y_ν(z)`.
380/// For negative integer n, `J_{−n}(z) = (−1)^n J_n(z)`.
381///
382/// # Example
383///
384/// ```
385/// use complex_bessel::besselj;
386/// use num_complex::Complex;
387///
388/// let z = Complex::new(1.0_f64, 0.0);
389/// let j0 = besselj(0.0, z).unwrap();
390/// assert!((j0.re - 0.7652).abs() < 1e-3); // J_0(1) ≈ 0.7652
391/// ```
392///
393/// # Errors
394///
395/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
396/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
397/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
398/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
399#[inline]
400pub fn besselj<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
401 besselj_internal(nu, z, Scaling::Unscaled)
402}
403
404/// Bessel function of the second kind, Y<sub>ν</sub>(z).
405///
406/// Computes a single value of Y_ν(z) for complex z and real order ν
407/// (any real value, including negative).
408///
409/// For negative ν, the DLMF 10.2.3 reflection formula is applied:
410/// `Y_{-ν}(z) = sin(νπ) J_ν(z) + cos(νπ) Y_ν(z)`.
411/// For negative integer n, `Y_{−n}(z) = (−1)^n Y_n(z)`.
412///
413/// # Example
414///
415/// ```
416/// use complex_bessel::bessely;
417/// use num_complex::Complex;
418///
419/// let z = Complex::new(1.0_f64, 0.0);
420/// let y0 = bessely(0.0, z).unwrap();
421/// assert!((y0.re - 0.0883).abs() < 1e-3); // Y_0(1) ≈ 0.0883
422/// ```
423///
424/// # Errors
425///
426/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
427/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
428/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
429/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
430#[inline]
431pub fn bessely<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
432 bessely_internal(nu, z, Scaling::Unscaled)
433}
434
435/// Modified Bessel function of the first kind, I<sub>ν</sub>(z).
436///
437/// Computes a single value of I_ν(z) for complex z and real order ν
438/// (any real value, including negative).
439///
440/// For negative ν, the DLMF 10.27.2 reflection formula is applied:
441/// `I_{-ν}(z) = I_ν(z) + (2/π) sin(νπ) K_ν(z)`.
442/// For negative integer n, `I_{−n}(z) = I_n(z)`.
443///
444/// # Example
445///
446/// ```
447/// use complex_bessel::besseli;
448/// use num_complex::Complex;
449///
450/// let z = Complex::new(1.0_f64, 0.0);
451/// let i0 = besseli(0.0, z).unwrap();
452/// assert!((i0.re - 1.2661).abs() < 1e-3); // I_0(1) ≈ 1.2661
453/// ```
454///
455/// # Errors
456///
457/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
458/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
459/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
460/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
461#[inline]
462pub fn besseli<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
463 besseli_internal(nu, z, Scaling::Unscaled)
464}
465
466/// Modified Bessel function of the second kind, K<sub>ν</sub>(z).
467///
468/// Computes a single value of K_ν(z) for complex z and real order ν
469/// (any real value, including negative).
470///
471/// For negative ν, K_{-ν}(z) = K_ν(z) (DLMF 10.27.3).
472///
473/// # Example
474///
475/// ```
476/// use complex_bessel::besselk;
477/// use num_complex::Complex;
478///
479/// let z = Complex::new(1.0_f64, 0.0);
480/// let k0 = besselk(0.0, z).unwrap();
481/// assert!((k0.re - 0.4211).abs() < 1e-3); // K_0(1) ≈ 0.4211
482/// ```
483///
484/// # Errors
485///
486/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
487/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
488/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
489/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
490#[inline]
491pub fn besselk<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
492 besselk_internal(nu, z, Scaling::Unscaled)
493}
494
495/// Hankel function of the first kind, H<sub>ν</sub><sup>(1)</sup>(z).
496///
497/// Computes a single value of H_ν^(1)(z) for complex z and real order ν
498/// (any real value, including negative).
499///
500/// For negative ν, the DLMF 10.4.6 reflection formula is applied:
501/// `H^(1)_{-ν}(z) = exp(νπi) H^(1)_ν(z)`.
502/// For negative integer n, `H^(1)_{−n}(z) = (−1)^n H^(1)_n(z)`.
503///
504/// # Example
505///
506/// ```
507/// use complex_bessel::hankel1;
508/// use num_complex::Complex;
509///
510/// let z = Complex::new(1.0_f64, 0.0);
511/// let h = hankel1(0.0, z).unwrap();
512/// assert!((h.re - 0.7652).abs() < 1e-3); // Re = J_0(1) ≈ 0.7652
513/// assert!((h.im - 0.0883).abs() < 1e-3); // Im = Y_0(1) ≈ 0.0883
514/// ```
515///
516/// # Errors
517///
518/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
519/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
520/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
521/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
522#[inline]
523pub fn hankel1<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
524 hankel_internal(HankelKind::First, nu, z, Scaling::Unscaled)
525}
526
527/// Hankel function of the second kind, H<sub>ν</sub><sup>(2)</sup>(z).
528///
529/// Computes a single value of H_ν^(2)(z) for complex z and real order ν
530/// (any real value, including negative).
531///
532/// For negative ν, the DLMF 10.4.6 reflection formula is applied:
533/// `H^(2)_{-ν}(z) = exp(-νπi) H^(2)_ν(z)`.
534/// For negative integer n, `H^(2)_{−n}(z) = (−1)^n H^(2)_n(z)`.
535///
536/// # Example
537///
538/// ```
539/// use complex_bessel::hankel2;
540/// use num_complex::Complex;
541///
542/// let z = Complex::new(1.0_f64, 0.0);
543/// let h = hankel2(0.0, z).unwrap();
544/// assert!((h.re - 0.7652).abs() < 1e-3); // Re = J_0(1) ≈ 0.7652
545/// assert!((h.im + 0.0883).abs() < 1e-3); // Im = -Y_0(1) ≈ -0.0883
546/// ```
547///
548/// # Errors
549///
550/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
551/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
552/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
553/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
554#[inline]
555pub fn hankel2<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
556 hankel_internal(HankelKind::Second, nu, z, Scaling::Unscaled)
557}
558
559/// Airy function Ai(z).
560///
561/// Computes the Airy function Ai(z) for complex z.
562/// Ai(z) is a solution to the differential equation w′′ − z·w = 0
563/// that decays super-exponentially for large positive real z.
564///
565/// # Example
566///
567/// ```
568/// use complex_bessel::airy;
569/// use num_complex::Complex;
570///
571/// let z = Complex::new(0.0_f64, 0.0);
572/// let ai = airy(z).unwrap();
573/// assert!((ai.re - 0.3550).abs() < 1e-3); // Ai(0) ≈ 0.3550
574/// ```
575///
576/// # Errors
577///
578/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
579/// - [`Error::Overflow`] if |z| is too large for a finite result.
580/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
581/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
582#[inline]
583pub fn airy<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
584 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Value, Scaling::Unscaled)?;
585 Ok(result)
586}
587
588/// Derivative of the Airy function, Ai'(z).
589///
590/// Computes the derivative of Ai(z) for complex z.
591/// Ai'(z) decays super-exponentially for large positive real z, just as Ai(z) does.
592/// Satisfies the differential equation Ai′′(z) = z · Ai(z).
593///
594/// # Example
595///
596/// ```
597/// use complex_bessel::airyprime;
598/// use num_complex::Complex;
599///
600/// let z = Complex::new(0.0_f64, 0.0);
601/// let ai_prime = airyprime(z).unwrap();
602/// assert!((ai_prime.re + 0.2588).abs() < 1e-3); // Ai'(0) ≈ -0.2588
603/// ```
604///
605/// # Errors
606///
607/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
608/// - [`Error::Overflow`] if |z| is too large for a finite result.
609/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
610/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
611#[inline]
612pub fn airyprime<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
613 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Derivative, Scaling::Unscaled)?;
614 Ok(result)
615}
616
617/// Airy function of the second kind, Bi(z).
618///
619/// Computes the Airy function Bi(z) for complex z.
620/// Bi(z) is the solution to w′′ − z·w = 0 that grows super-exponentially
621/// for large positive real z.
622///
623/// # Example
624///
625/// ```
626/// use complex_bessel::biry;
627/// use num_complex::Complex;
628///
629/// let z = Complex::new(0.0_f64, 0.0);
630/// let bi = biry(z).unwrap();
631/// assert!((bi.re - 0.6149).abs() < 1e-3); // Bi(0) ≈ 0.6149
632/// ```
633///
634/// # Errors
635///
636/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
637/// - [`Error::Overflow`] if |z| is too large for a finite result.
638/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
639/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
640#[inline]
641pub fn biry<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
642 let (result, _status) = airy::zbiry(z, AiryDerivative::Value, Scaling::Unscaled)?;
643 Ok(result)
644}
645
646/// Derivative of the Airy function of the second kind, Bi'(z).
647///
648/// Computes the derivative of Bi(z) for complex z.
649/// Bi'(z) grows super-exponentially for large positive real z, just as Bi(z) does.
650/// Satisfies the differential equation Bi′′(z) = z · Bi(z).
651///
652/// # Example
653///
654/// ```
655/// use complex_bessel::biryprime;
656/// use num_complex::Complex;
657///
658/// let z = Complex::new(0.0_f64, 0.0);
659/// let bi_prime = biryprime(z).unwrap();
660/// assert!((bi_prime.re - 0.4483).abs() < 1e-3); // Bi'(0) ≈ 0.4483
661/// ```
662///
663/// # Errors
664///
665/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
666/// - [`Error::Overflow`] if |z| is too large for a finite result.
667/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
668/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
669#[inline]
670pub fn biryprime<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
671 let (result, _status) = airy::zbiry(z, AiryDerivative::Derivative, Scaling::Unscaled)?;
672 Ok(result)
673}
674
675// ── Scaled single-value functions ──
676
677/// Scaled Bessel function of the first kind, exp(−|Im(z)|) · J<sub>ν</sub>(z).
678///
679/// The exponential factor cancels the asymptotic growth of J for large imaginary
680/// arguments, keeping results in a representable floating-point range.
681///
682/// Supports negative ν via the same reflection formula as [`besselj`].
683///
684/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
685///
686/// # Example
687///
688/// ```
689/// use complex_bessel::besselj_scaled;
690/// use num_complex::Complex;
691///
692/// let z = Complex::new(0.0_f64, 10.0);
693/// let j_s = besselj_scaled(0.0, z).unwrap();
694/// assert!((j_s.re - 0.1278).abs() < 1e-3); // exp(-|Im(z)|) * J_0(10i) ≈ 0.1278
695/// ```
696///
697/// # Errors
698///
699/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
700/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
701/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
702/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
703#[inline]
704pub fn besselj_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
705 besselj_internal(nu, z, Scaling::Exponential)
706}
707
708/// Scaled Bessel function of the second kind, exp(−|Im(z)|) · Y<sub>ν</sub>(z).
709///
710/// The exponential factor cancels the asymptotic growth of Y for large imaginary
711/// arguments, keeping results in a representable floating-point range.
712///
713/// Supports negative ν via the same reflection formula as [`bessely`].
714///
715/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
716///
717/// # Example
718///
719/// ```
720/// use complex_bessel::bessely_scaled;
721/// use num_complex::Complex;
722///
723/// let z = Complex::new(0.0_f64, 10.0);
724/// let y_s = bessely_scaled(0.0, z).unwrap();
725/// assert!((y_s.im - 0.1278).abs() < 1e-3); // exp(-|Im(z)|) * Y_0(10i), Im ≈ 0.1278
726/// ```
727///
728/// # Errors
729///
730/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
731/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
732/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
733/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
734#[inline]
735pub fn bessely_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
736 bessely_internal(nu, z, Scaling::Exponential)
737}
738
739/// Scaled modified Bessel function of the first kind, exp(−|Re(z)|) · I<sub>ν</sub>(z).
740///
741/// I_ν(z) grows exponentially for large |Re(z)|, so the unscaled value can
742/// easily overflow. The scaling factor exp(−|Re(z)|) keeps the result finite.
743///
744/// Supports negative ν via the same reflection formula as [`besseli`].
745///
746/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
747///
748/// # Example
749///
750/// ```
751/// use complex_bessel::besseli_scaled;
752/// use num_complex::Complex;
753///
754/// let z = Complex::new(10.0_f64, 0.0);
755/// let i_s = besseli_scaled(0.0, z).unwrap();
756/// assert!((i_s.re - 0.1278).abs() < 1e-3); // exp(-10) * I_0(10) ≈ 0.1278
757/// ```
758///
759/// # Errors
760///
761/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
762/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
763/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
764/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
765#[inline]
766pub fn besseli_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
767 besseli_internal(nu, z, Scaling::Exponential)
768}
769
770/// Scaled modified Bessel function of the second kind, exp(z) · K<sub>ν</sub>(z).
771///
772/// K_ν(z) decays exponentially for large Re(z), so unscaled values can underflow
773/// to zero. The scaling factor exp(z) keeps the result in a normal range.
774///
775/// Supports negative ν via the same reflection formula as [`besselk`].
776///
777/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
778///
779/// # Example
780///
781/// ```
782/// use complex_bessel::besselk_scaled;
783/// use num_complex::Complex;
784///
785/// let z = Complex::new(500.0_f64, 0.0);
786///
787/// // K_0(500) would underflow to 0, but the scaled version stays finite:
788/// let k_s = besselk_scaled(0.0, z).unwrap();
789/// assert!((k_s.re - 0.0560).abs() < 1e-3); // exp(500) * K_0(500) ≈ 0.0560
790/// ```
791///
792/// # Errors
793///
794/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
795/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
796/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
797/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
798#[inline]
799pub fn besselk_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
800 besselk_internal(nu, z, Scaling::Exponential)
801}
802
803/// Scaled Hankel function of the first kind, exp(−iz) · H<sub>ν</sub><sup>(1)</sup>(z).
804///
805/// H^(1) grows exponentially in the lower half-plane;
806/// the scaling factor removes this growth, preventing overflow.
807///
808/// Supports negative ν via the same reflection formula as [`hankel1`].
809///
810/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
811///
812/// # Example
813///
814/// ```
815/// use complex_bessel::hankel1_scaled;
816/// use num_complex::Complex;
817///
818/// let z = Complex::new(1.0_f64, 0.0);
819/// let h_s = hankel1_scaled(0.0, z).unwrap();
820/// assert!((h_s.re - 0.4877).abs() < 1e-3); // exp(-i) * H^(1)_0(1), Re ≈ 0.4877
821/// ```
822///
823/// # Errors
824///
825/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
826/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
827/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
828/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
829#[inline]
830pub fn hankel1_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
831 hankel_internal(HankelKind::First, nu, z, Scaling::Exponential)
832}
833
834/// Scaled Hankel function of the second kind, exp(iz) · H<sub>ν</sub><sup>(2)</sup>(z).
835///
836/// H^(2) grows exponentially in the upper half-plane;
837/// the scaling factor removes this growth, preventing overflow.
838///
839/// Supports negative ν via the same reflection formula as [`hankel2`].
840///
841/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
842///
843/// # Example
844///
845/// ```
846/// use complex_bessel::hankel2_scaled;
847/// use num_complex::Complex;
848///
849/// let z = Complex::new(1.0_f64, 0.0);
850/// let h_s = hankel2_scaled(0.0, z).unwrap();
851/// assert!((h_s.re - 0.4877).abs() < 1e-3); // exp(i) * H^(2)_0(1), Re ≈ 0.4877
852/// ```
853///
854/// # Errors
855///
856/// - [`Error::InvalidInput`] if z or ν is not finite (NaN or infinite), or if z = 0.
857/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
858/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
859/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
860#[inline]
861pub fn hankel2_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
862 hankel_internal(HankelKind::Second, nu, z, Scaling::Exponential)
863}
864
865/// Scaled Airy function, exp(ζ) · Ai(z), where ζ = (2/3) z√z.
866///
867/// Ai(z) decays super-exponentially for large positive real z.
868/// The scaling factor exp(ζ) keeps the result representable.
869///
870/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
871///
872/// # Example
873///
874/// ```
875/// use complex_bessel::airy_scaled;
876/// use num_complex::Complex;
877///
878/// let z = Complex::new(5.0_f64, 0.0);
879/// let ai_s = airy_scaled(z).unwrap();
880/// assert!((ai_s.re - 0.1870).abs() < 1e-3); // exp(ζ) * Ai(5) ≈ 0.1870
881/// ```
882///
883/// # Errors
884///
885/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
886/// - [`Error::Overflow`] if |z| is too large for a finite result.
887/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
888/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
889#[inline]
890pub fn airy_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
891 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Value, Scaling::Exponential)?;
892 Ok(result)
893}
894
895/// Scaled derivative of the Airy function, exp(ζ) · Ai'(z), where ζ = (2/3) z√z.
896///
897/// Ai'(z) decays super-exponentially for large positive real z, just as Ai(z) does.
898/// The scaling factor exp(ζ) keeps the result representable.
899///
900/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
901///
902/// # Example
903///
904/// ```
905/// use complex_bessel::airyprime_scaled;
906/// use num_complex::Complex;
907///
908/// let z = Complex::new(5.0_f64, 0.0);
909/// let ai_s = airyprime_scaled(z).unwrap();
910/// assert!((ai_s.re + 0.4270).abs() < 1e-3); // exp(ζ) * Ai'(5) ≈ -0.4270
911/// ```
912///
913/// # Errors
914///
915/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
916/// - [`Error::Overflow`] if |z| is too large for a finite result.
917/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
918/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
919#[inline]
920pub fn airyprime_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
921 let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Derivative, Scaling::Exponential)?;
922 Ok(result)
923}
924
925/// Scaled Airy function of the second kind, exp(−|Re(ζ)|) · Bi(z), where ζ = (2/3) z√z.
926///
927/// Bi(z) grows super-exponentially for large positive real z.
928/// The scaling factor exp(−|Re(ζ)|) keeps the result representable.
929///
930/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
931///
932/// # Example
933///
934/// ```
935/// use complex_bessel::biry_scaled;
936/// use num_complex::Complex;
937///
938/// let z = Complex::new(5.0_f64, 0.0);
939/// let bi_s = biry_scaled(z).unwrap();
940/// assert!((bi_s.re - 0.3811).abs() < 1e-3); // exp(-|Re(ζ)|) * Bi(5) ≈ 0.3811
941/// ```
942///
943/// # Errors
944///
945/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
946/// - [`Error::Overflow`] if |z| is too large for a finite result.
947/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
948/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
949#[inline]
950pub fn biry_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
951 let (result, _status) = airy::zbiry(z, AiryDerivative::Value, Scaling::Exponential)?;
952 Ok(result)
953}
954
955/// Scaled derivative of the Airy function of the second kind, exp(−|Re(ζ)|) · Bi'(z), where ζ = (2/3) z√z.
956///
957/// Bi'(z) grows super-exponentially for large positive real z, just as Bi(z) does.
958/// The scaling factor exp(−|Re(ζ)|) keeps the result representable.
959///
960/// See [crate-level docs](crate#exponential-scaling) for the full scaling table.
961///
962/// # Example
963///
964/// ```
965/// use complex_bessel::biryprime_scaled;
966/// use num_complex::Complex;
967///
968/// let z = Complex::new(5.0_f64, 0.0);
969/// let bi_s = biryprime_scaled(z).unwrap();
970/// assert!((bi_s.re - 0.8319).abs() < 1e-3); // exp(-|Re(ζ)|) * Bi'(5) ≈ 0.8319
971/// ```
972///
973/// # Errors
974///
975/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
976/// - [`Error::Overflow`] if |z| is too large for a finite result.
977/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
978/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
979#[inline]
980pub fn biryprime_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
981 let (result, _status) = airy::zbiry(z, AiryDerivative::Derivative, Scaling::Exponential)?;
982 Ok(result)
983}
984
985// ── Airy _raw functions (expose Accuracy) ──
986
987/// Airy function Ai(z) with accuracy status.
988///
989/// Like [`airy`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
990/// - [`Accuracy::Normal`] — no significant precision loss
991/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
992///
993/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
994/// see [crate-level docs](crate#exponential-scaling) for details.
995///
996/// # Example
997///
998/// ```
999/// use complex_bessel::*;
1000/// use num_complex::Complex;
1001///
1002/// let z = Complex::new(0.0_f64, 0.0);
1003/// let result = airy_raw(z, Scaling::Unscaled).unwrap();
1004/// assert!((result.value.re - 0.3550).abs() < 1e-3); // Ai(0) ≈ 0.3550
1005/// assert!(matches!(result.status, Accuracy::Normal));
1006/// ```
1007///
1008/// # Errors
1009///
1010/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1011/// - [`Error::Overflow`] if |z| is too large for a finite result.
1012/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1013/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1014#[inline]
1015pub fn airy_raw<T: BesselFloat>(z: Complex<T>, scaling: Scaling) -> Result<AiryResult<T>, Error> {
1016 let (value, _nz, status) = airy::zairy(z, AiryDerivative::Value, scaling)?;
1017 Ok(AiryResult { value, status })
1018}
1019
1020/// Derivative of the Airy function Ai'(z) with accuracy status.
1021///
1022/// Like [`airyprime`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1023/// - [`Accuracy::Normal`] — no significant precision loss
1024/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1025///
1026/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1027/// see [crate-level docs](crate#exponential-scaling) for details.
1028///
1029/// # Example
1030///
1031/// ```
1032/// use complex_bessel::*;
1033/// use num_complex::Complex;
1034///
1035/// let z = Complex::new(0.0_f64, 0.0);
1036/// let result = airyprime_raw(z, Scaling::Unscaled).unwrap();
1037/// assert!((result.value.re + 0.2588).abs() < 1e-3); // Ai'(0) ≈ -0.2588
1038/// assert!(matches!(result.status, Accuracy::Normal));
1039/// ```
1040///
1041/// # Errors
1042///
1043/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1044/// - [`Error::Overflow`] if |z| is too large for a finite result.
1045/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1046/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1047#[inline]
1048pub fn airyprime_raw<T: BesselFloat>(
1049 z: Complex<T>,
1050 scaling: Scaling,
1051) -> Result<AiryResult<T>, Error> {
1052 let (value, _nz, status) = airy::zairy(z, AiryDerivative::Derivative, scaling)?;
1053 Ok(AiryResult { value, status })
1054}
1055
1056/// Airy function of the second kind Bi(z) with accuracy status.
1057///
1058/// Like [`biry`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1059/// - [`Accuracy::Normal`] — no significant precision loss
1060/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1061///
1062/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1063/// see [crate-level docs](crate#exponential-scaling) for details.
1064///
1065/// # Example
1066///
1067/// ```
1068/// use complex_bessel::*;
1069/// use num_complex::Complex;
1070///
1071/// let z = Complex::new(0.0_f64, 0.0);
1072/// let result = biry_raw(z, Scaling::Unscaled).unwrap();
1073/// assert!((result.value.re - 0.6149).abs() < 1e-3); // Bi(0) ≈ 0.6149
1074/// assert!(matches!(result.status, Accuracy::Normal));
1075/// ```
1076///
1077/// # Errors
1078///
1079/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1080/// - [`Error::Overflow`] if |z| is too large for a finite result.
1081/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1082/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1083#[inline]
1084pub fn biry_raw<T: BesselFloat>(z: Complex<T>, scaling: Scaling) -> Result<AiryResult<T>, Error> {
1085 let (value, status) = airy::zbiry(z, AiryDerivative::Value, scaling)?;
1086 Ok(AiryResult { value, status })
1087}
1088
1089/// Derivative of the Airy function of the second kind Bi'(z) with accuracy status.
1090///
1091/// Like [`biryprime`], but returns an [`AiryResult`] that includes an [`Accuracy`] status:
1092/// - [`Accuracy::Normal`] — no significant precision loss
1093/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| very large)
1094///
1095/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1096/// see [crate-level docs](crate#exponential-scaling) for details.
1097///
1098/// # Example
1099///
1100/// ```
1101/// use complex_bessel::*;
1102/// use num_complex::Complex;
1103///
1104/// let z = Complex::new(0.0_f64, 0.0);
1105/// let result = biryprime_raw(z, Scaling::Unscaled).unwrap();
1106/// assert!((result.value.re - 0.4483).abs() < 1e-3); // Bi'(0) ≈ 0.4483
1107/// assert!(matches!(result.status, Accuracy::Normal));
1108/// ```
1109///
1110/// # Errors
1111///
1112/// - [`Error::InvalidInput`] if z is not finite (NaN or infinite).
1113/// - [`Error::Overflow`] if |z| is too large for a finite result.
1114/// - [`Error::TotalPrecisionLoss`] if |z| is too large for any significant digits (roughly > 10⁶ for f64).
1115/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1116#[inline]
1117pub fn biryprime_raw<T: BesselFloat>(
1118 z: Complex<T>,
1119 scaling: Scaling,
1120) -> Result<AiryResult<T>, Error> {
1121 let (value, status) = airy::zbiry(z, AiryDerivative::Derivative, scaling)?;
1122 Ok(AiryResult { value, status })
1123}
1124
1125// ── Sequence functions with scaling option (require alloc) ──
1126
1127#[cfg(feature = "alloc")]
1128extern crate alloc as alloc_crate;
1129
1130#[cfg(feature = "alloc")]
1131fn seq_helper<T: BesselFloat>(
1132 n: usize,
1133 f: impl FnOnce(&mut [Complex<T>]) -> Result<(usize, Accuracy), Error>,
1134) -> Result<BesselResult<T>, Error> {
1135 let zero = T::zero();
1136 let mut values = alloc_crate::vec![Complex::new(zero, zero); n];
1137 let (underflow_count, status) = f(&mut values)?;
1138 Ok(BesselResult {
1139 values,
1140 underflow_count,
1141 status,
1142 })
1143}
1144
1145/// Take the worse of two statuses (Reduced > Normal).
1146#[cfg(feature = "alloc")]
1147#[inline]
1148fn worse_status(a: Accuracy, b: Accuracy) -> Accuracy {
1149 match (a, b) {
1150 (Accuracy::Reduced, _) | (_, Accuracy::Reduced) => Accuracy::Reduced,
1151 _ => Accuracy::Normal,
1152 }
1153}
1154
1155/// Recount underflow: number of leading zero elements in the output.
1156#[cfg(feature = "alloc")]
1157#[inline]
1158fn recount_underflow<T: BesselFloat>(values: &[Complex<T>]) -> usize {
1159 let zero = Complex::new(T::zero(), T::zero());
1160 values.iter().take_while(|v| **v == zero).count()
1161}
1162
1163/// Compute the number of negative-order elements (before crossing zero).
1164/// For orders ν, ν+1, …, ν+n−1, this is the count where ν+j < 0.
1165#[cfg(feature = "alloc")]
1166#[inline]
1167fn neg_count<T: BesselFloat>(nu: T, n: usize) -> usize {
1168 if nu >= T::zero() {
1169 return 0;
1170 }
1171 // Number of j in [0,n) with nu + j < 0
1172 let abs_nu = nu.abs();
1173 // ceil(|nu|) gives the first j where nu+j >= 0
1174 let c = abs_nu.ceil();
1175 let cn = if let Some(v) = c.to_usize() { v } else { n };
1176 cn.min(n)
1177}
1178
1179// ── Negative-order _seq implementations ──
1180
1181/// K_seq with negative order support: K_{-ν} = K_ν (even function).
1182///
1183/// Orders: ν, ν+1, …, ν+n−1. Negative orders map to |order|.
1184/// - All negative: single call zbesk(|ν|-(n-1), n), reverse.
1185/// - All positive: single call zbesk(ν, n).
1186/// - Integer crossing: single call covering max(|ν|, ν+n-1), mirror.
1187/// - Non-integer crossing: two calls (neg lattice + pos lattice).
1188#[cfg(feature = "alloc")]
1189#[allow(clippy::needless_range_loop, clippy::manual_memcpy)]
1190fn besselk_seq_neg<T: BesselFloat>(
1191 nu: T,
1192 z: Complex<T>,
1193 n: usize,
1194 scaling: Scaling,
1195) -> Result<BesselResult<T>, Error> {
1196 // n = 0 would underflow the `n - 1` order arithmetic below
1197 if n == 0 {
1198 return Err(Error::InvalidInput);
1199 }
1200 let zero = T::zero();
1201 let czero = Complex::new(zero, zero);
1202 let abs_nu = nu.abs();
1203 let nc = neg_count(nu, n);
1204
1205 // All negative: absolute values are |ν|, |ν|-1, …, |ν|-(nc-1) (descending)
1206 // = ascending from |ν|-(nc-1) to |ν|
1207 if nc == n {
1208 // All orders negative, e.g., ν=-3.7, n=3 → orders -3.7,-2.7,-1.7
1209 // |orders| = 3.7, 2.7, 1.7 → start at 1.7, compute 3 ascending → reverse
1210 let start = abs_nu - T::from_f64((n - 1) as f64);
1211 let mut buf = alloc_crate::vec![czero; n];
1212 let (_, status) = besk::zbesk(z, start, scaling, &mut buf)?;
1213 buf.reverse();
1214 let uc = recount_underflow(&buf);
1215 return Ok(BesselResult {
1216 values: buf,
1217 underflow_count: uc,
1218 status,
1219 });
1220 }
1221
1222 // Check if integer crossing (fractional part is 0 for integer nu)
1223 let is_int = as_integer(abs_nu).is_some();
1224
1225 if is_int {
1226 // Integer crossing: e.g., ν=-2, n=5 → orders -2,-1,0,1,2
1227 // All have same fractional part (0). One call covers all |orders|.
1228 // Max absolute order = max(|ν|, ν+n-1)
1229 let last_order = nu + T::from_f64((n - 1) as f64);
1230 let max_abs = if abs_nu > last_order {
1231 abs_nu
1232 } else {
1233 last_order
1234 };
1235 let buf_n = max_abs.to_usize().unwrap_or(0) + 1; // orders 0..=max_abs
1236 let mut buf = alloc_crate::vec![czero; buf_n];
1237 let (_, status) = besk::zbesk(z, zero, scaling, &mut buf)?;
1238
1239 let mut values = alloc_crate::vec![czero; n];
1240 for j in 0..n {
1241 let order = nu + T::from_f64(j as f64);
1242 let idx = order.abs().to_usize().unwrap_or(0);
1243 values[j] = buf[idx];
1244 }
1245 let uc = recount_underflow(&values);
1246 return Ok(BesselResult {
1247 values,
1248 underflow_count: uc,
1249 status,
1250 });
1251 }
1252
1253 // Non-integer crossing: two separate lattices
1254 // Negative part: orders ν, ν+1, …, ν+(nc-1) → |orders| descending
1255 // frac_neg = fractional part of |ν|
1256 let frac_neg = abs_nu - abs_nu.floor();
1257 let neg_start = frac_neg; // smallest |order| in negative lattice
1258 let mut neg_buf = alloc_crate::vec![czero; nc];
1259 let (_, status1) = besk::zbesk(z, neg_start, scaling, &mut neg_buf)?;
1260
1261 // Positive part: orders ν+nc, ν+nc+1, …, ν+n-1
1262 let pos_start = nu + T::from_f64(nc as f64);
1263 let pos_n = n - nc;
1264 let mut pos_buf = alloc_crate::vec![czero; pos_n];
1265 let (_, status2) = besk::zbesk(z, pos_start, scaling, &mut pos_buf)?;
1266
1267 let mut values = alloc_crate::vec![czero; n];
1268 // Negative part: buf[0]=K(frac_neg), buf[1]=K(frac_neg+1), …
1269 // Need to map: order ν+j → |ν+j| → index in neg_buf
1270 // |ν+j| = abs_nu - j, and neg_buf[k] = K(frac_neg + k)
1271 // So k = |ν+j| - frac_neg = (abs_nu - j) - frac_neg = floor(abs_nu) - j
1272 for j in 0..nc {
1273 let k = (abs_nu - T::from_f64(j as f64))
1274 .floor()
1275 .to_usize()
1276 .unwrap_or(0);
1277 values[j] = neg_buf[k];
1278 }
1279 // Positive part: direct copy
1280 for j in 0..pos_n {
1281 values[nc + j] = pos_buf[j];
1282 }
1283
1284 let status = worse_status(status1, status2);
1285 let uc = recount_underflow(&values);
1286 Ok(BesselResult {
1287 values,
1288 underflow_count: uc,
1289 status,
1290 })
1291}
1292
1293/// H_seq with negative order support: H^(m)_{-ν} = exp(±νπi)·H^(m)_ν.
1294///
1295/// Same lattice-splitting as K, but negative-order elements get rotation.
1296#[cfg(feature = "alloc")]
1297#[allow(clippy::needless_range_loop, clippy::manual_memcpy)]
1298fn hankel_seq_neg<T: BesselFloat>(
1299 kind: HankelKind,
1300 nu: T,
1301 z: Complex<T>,
1302 n: usize,
1303 scaling: Scaling,
1304) -> Result<BesselResult<T>, Error> {
1305 // n = 0 would underflow the `n - 1` order arithmetic below
1306 if n == 0 {
1307 return Err(Error::InvalidInput);
1308 }
1309 let zero = T::zero();
1310 let czero = Complex::new(zero, zero);
1311 let abs_nu = nu.abs();
1312 let nc = neg_count(nu, n);
1313 let is_int = as_integer(abs_nu).is_some();
1314
1315 if nc == n {
1316 // All negative: compute at positive |orders| then rotate each
1317 let start = abs_nu - T::from_f64((n - 1) as f64);
1318 let mut buf = alloc_crate::vec![czero; n];
1319 let (_, status) = besh::zbesh(z, start, kind, scaling, &mut buf)?;
1320 buf.reverse();
1321 // Apply rotation: element j has |order| = abs_nu - j
1322 for j in 0..n {
1323 let order = abs_nu - T::from_f64(j as f64);
1324 buf[j] = reflect_h_element(order, kind, buf[j]);
1325 }
1326 let uc = recount_underflow(&buf);
1327 return Ok(BesselResult {
1328 values: buf,
1329 underflow_count: uc,
1330 status,
1331 });
1332 }
1333
1334 if is_int {
1335 // Integer crossing: single call, rotate negative elements
1336 let last_order = nu + T::from_f64((n - 1) as f64);
1337 let max_abs = if abs_nu > last_order {
1338 abs_nu
1339 } else {
1340 last_order
1341 };
1342 let buf_n = max_abs.to_usize().unwrap_or(0) + 1;
1343 let mut buf = alloc_crate::vec![czero; buf_n];
1344 let (_, status) = besh::zbesh(z, zero, kind, scaling, &mut buf)?;
1345
1346 let mut values = alloc_crate::vec![czero; n];
1347 for j in 0..n {
1348 let order = nu + T::from_f64(j as f64);
1349 let idx = order.abs().to_usize().unwrap_or(0);
1350 values[j] = buf[idx];
1351 if order < zero {
1352 values[j] = reflect_h_element(order.abs(), kind, values[j]);
1353 }
1354 }
1355 let uc = recount_underflow(&values);
1356 return Ok(BesselResult {
1357 values,
1358 underflow_count: uc,
1359 status,
1360 });
1361 }
1362
1363 // Non-integer crossing: two lattices
1364 let frac_neg = abs_nu - abs_nu.floor();
1365 let mut neg_buf = alloc_crate::vec![czero; nc];
1366 let (_, status1) = besh::zbesh(z, frac_neg, kind, scaling, &mut neg_buf)?;
1367
1368 let pos_start = nu + T::from_f64(nc as f64);
1369 let pos_n = n - nc;
1370 let mut pos_buf = alloc_crate::vec![czero; pos_n];
1371 let (_, status2) = besh::zbesh(z, pos_start, kind, scaling, &mut pos_buf)?;
1372
1373 let mut values = alloc_crate::vec![czero; n];
1374 for j in 0..nc {
1375 let abs_order = abs_nu - T::from_f64(j as f64);
1376 let k = abs_order.floor().to_usize().unwrap_or(0);
1377 values[j] = reflect_h_element(abs_order, kind, neg_buf[k]);
1378 }
1379 for j in 0..pos_n {
1380 values[nc + j] = pos_buf[j];
1381 }
1382
1383 let status = worse_status(status1, status2);
1384 let uc = recount_underflow(&values);
1385 Ok(BesselResult {
1386 values,
1387 underflow_count: uc,
1388 status,
1389 })
1390}
1391
1392/// J_seq with negative order support.
1393/// J_{-ν} = cos(νπ)·J_ν − sin(νπ)·Y_ν (non-integer)
1394/// J_{-n} = (-1)^n · J_n (integer)
1395#[cfg(feature = "alloc")]
1396#[allow(clippy::needless_range_loop, clippy::manual_memcpy)]
1397fn besselj_seq_neg<T: BesselFloat>(
1398 nu: T,
1399 z: Complex<T>,
1400 n: usize,
1401 scaling: Scaling,
1402) -> Result<BesselResult<T>, Error> {
1403 // n = 0 would underflow the `n - 1` order arithmetic below
1404 if n == 0 {
1405 return Err(Error::InvalidInput);
1406 }
1407 let zero = T::zero();
1408 let czero = Complex::new(zero, zero);
1409 let abs_nu = nu.abs();
1410 let nc = neg_count(nu, n);
1411 let is_int = as_integer(abs_nu).is_some();
1412
1413 if is_int {
1414 // Integer: J_{-n}(z) = (-1)^n J_n(z). Same lattice.
1415 let (min_abs, max_abs) = if nc == n {
1416 // All orders negative: |orders| span [|ν|-(n-1), |ν|] (O(n) work)
1417 (abs_nu - T::from_f64((n - 1) as f64), abs_nu)
1418 } else {
1419 // Crossing zero: |orders| span [0, max(|ν|, ν+n-1)], all needed anyway
1420 let last_order = nu + T::from_f64((n - 1) as f64);
1421 (zero, abs_nu.max(last_order))
1422 };
1423 let buf_n = (max_abs - min_abs).to_usize().unwrap_or(0) + 1;
1424 let mut buf = alloc_crate::vec![czero; buf_n];
1425 let (_, status) = besj::zbesj(z, min_abs, scaling, &mut buf)?;
1426
1427 let mut values = alloc_crate::vec![czero; n];
1428 for j in 0..n {
1429 let order = nu + T::from_f64(j as f64);
1430 let idx = (order.abs() - min_abs).to_usize().unwrap_or(0);
1431 values[j] = buf[idx];
1432 if order < zero {
1433 let int_order = order.abs().to_i64().unwrap_or(0);
1434 values[j] = values[j] * integer_sign::<T>(int_order);
1435 }
1436 }
1437 let uc = recount_underflow(&values);
1438 return Ok(BesselResult {
1439 values,
1440 underflow_count: uc,
1441 status,
1442 });
1443 }
1444
1445 if nc == n {
1446 // All negative, non-integer: need J and Y at positive |orders|
1447 let start = abs_nu - T::from_f64((n - 1) as f64);
1448 let mut j_buf = alloc_crate::vec![czero; n];
1449 let mut y_buf = alloc_crate::vec![czero; n];
1450 let (_, s1) = besj::zbesj(z, start, scaling, &mut j_buf)?;
1451 let (_, s2) = besy::zbesy(z, start, scaling, &mut y_buf)?;
1452
1453 let mut values = alloc_crate::vec![czero; n];
1454 // buf[k] = f(start+k). For output j, |order| = abs_nu - j, k = |order| - start = n-1-j
1455 for j in 0..n {
1456 let k = n - 1 - j;
1457 let order = abs_nu - T::from_f64(j as f64);
1458 values[j] = reflect_j_element(order, j_buf[k], y_buf[k]);
1459 }
1460 let uc = recount_underflow(&values);
1461 return Ok(BesselResult {
1462 values,
1463 underflow_count: uc,
1464 status: worse_status(s1, s2),
1465 });
1466 }
1467
1468 // Non-integer crossing: two lattices
1469 let frac_neg = abs_nu - abs_nu.floor();
1470 let mut neg_j = alloc_crate::vec![czero; nc];
1471 let mut neg_y = alloc_crate::vec![czero; nc];
1472 let (_, s1) = besj::zbesj(z, frac_neg, scaling, &mut neg_j)?;
1473 let (_, s2) = besy::zbesy(z, frac_neg, scaling, &mut neg_y)?;
1474
1475 let pos_start = nu + T::from_f64(nc as f64);
1476 let pos_n = n - nc;
1477 let mut pos_buf = alloc_crate::vec![czero; pos_n];
1478 let (_, s3) = besj::zbesj(z, pos_start, scaling, &mut pos_buf)?;
1479
1480 let mut values = alloc_crate::vec![czero; n];
1481 for j in 0..nc {
1482 let abs_order = abs_nu - T::from_f64(j as f64);
1483 let k = abs_order.floor().to_usize().unwrap_or(0);
1484 values[j] = reflect_j_element(abs_order, neg_j[k], neg_y[k]);
1485 }
1486 for j in 0..pos_n {
1487 values[nc + j] = pos_buf[j];
1488 }
1489
1490 let status = worse_status(worse_status(s1, s2), s3);
1491 let uc = recount_underflow(&values);
1492 Ok(BesselResult {
1493 values,
1494 underflow_count: uc,
1495 status,
1496 })
1497}
1498
1499/// Y_seq with negative order support.
1500/// Y_{-ν} = sin(νπ)·J_ν + cos(νπ)·Y_ν (non-integer)
1501/// Y_{-n} = (-1)^n · Y_n (integer)
1502#[cfg(feature = "alloc")]
1503#[allow(clippy::needless_range_loop, clippy::manual_memcpy)]
1504fn bessely_seq_neg<T: BesselFloat>(
1505 nu: T,
1506 z: Complex<T>,
1507 n: usize,
1508 scaling: Scaling,
1509) -> Result<BesselResult<T>, Error> {
1510 // n = 0 would underflow the `n - 1` order arithmetic below
1511 if n == 0 {
1512 return Err(Error::InvalidInput);
1513 }
1514 let zero = T::zero();
1515 let czero = Complex::new(zero, zero);
1516 let abs_nu = nu.abs();
1517 let nc = neg_count(nu, n);
1518 let is_int = as_integer(abs_nu).is_some();
1519
1520 if is_int {
1521 let (min_abs, max_abs) = if nc == n {
1522 // All orders negative: |orders| span [|ν|-(n-1), |ν|] (O(n) work)
1523 (abs_nu - T::from_f64((n - 1) as f64), abs_nu)
1524 } else {
1525 // Crossing zero: |orders| span [0, max(|ν|, ν+n-1)], all needed anyway
1526 let last_order = nu + T::from_f64((n - 1) as f64);
1527 (zero, abs_nu.max(last_order))
1528 };
1529 let buf_n = (max_abs - min_abs).to_usize().unwrap_or(0) + 1;
1530 let mut buf = alloc_crate::vec![czero; buf_n];
1531 let (_, status) = besy::zbesy(z, min_abs, scaling, &mut buf)?;
1532
1533 let mut values = alloc_crate::vec![czero; n];
1534 for j in 0..n {
1535 let order = nu + T::from_f64(j as f64);
1536 let idx = (order.abs() - min_abs).to_usize().unwrap_or(0);
1537 values[j] = buf[idx];
1538 if order < zero {
1539 let int_order = order.abs().to_i64().unwrap_or(0);
1540 values[j] = values[j] * integer_sign::<T>(int_order);
1541 }
1542 }
1543 let uc = recount_underflow(&values);
1544 return Ok(BesselResult {
1545 values,
1546 underflow_count: uc,
1547 status,
1548 });
1549 }
1550
1551 if nc == n {
1552 let start = abs_nu - T::from_f64((n - 1) as f64);
1553 let mut j_buf = alloc_crate::vec![czero; n];
1554 let mut y_buf = alloc_crate::vec![czero; n];
1555 let (_, s1) = besj::zbesj(z, start, scaling, &mut j_buf)?;
1556 let (_, s2) = besy::zbesy(z, start, scaling, &mut y_buf)?;
1557
1558 let mut values = alloc_crate::vec![czero; n];
1559 for j in 0..n {
1560 let k = n - 1 - j;
1561 let order = abs_nu - T::from_f64(j as f64);
1562 values[j] = reflect_y_element(order, j_buf[k], y_buf[k]);
1563 }
1564 let uc = recount_underflow(&values);
1565 return Ok(BesselResult {
1566 values,
1567 underflow_count: uc,
1568 status: worse_status(s1, s2),
1569 });
1570 }
1571
1572 // Non-integer crossing: two lattices
1573 let frac_neg = abs_nu - abs_nu.floor();
1574 let mut neg_j = alloc_crate::vec![czero; nc];
1575 let mut neg_y = alloc_crate::vec![czero; nc];
1576 let (_, s1) = besj::zbesj(z, frac_neg, scaling, &mut neg_j)?;
1577 let (_, s2) = besy::zbesy(z, frac_neg, scaling, &mut neg_y)?;
1578
1579 let pos_start = nu + T::from_f64(nc as f64);
1580 let pos_n = n - nc;
1581 let mut pos_buf = alloc_crate::vec![czero; pos_n];
1582 let (_, s3) = besy::zbesy(z, pos_start, scaling, &mut pos_buf)?;
1583
1584 let mut values = alloc_crate::vec![czero; n];
1585 for j in 0..nc {
1586 let abs_order = abs_nu - T::from_f64(j as f64);
1587 let k = abs_order.floor().to_usize().unwrap_or(0);
1588 values[j] = reflect_y_element(abs_order, neg_j[k], neg_y[k]);
1589 }
1590 for j in 0..pos_n {
1591 values[nc + j] = pos_buf[j];
1592 }
1593
1594 let status = worse_status(worse_status(s1, s2), s3);
1595 let uc = recount_underflow(&values);
1596 Ok(BesselResult {
1597 values,
1598 underflow_count: uc,
1599 status,
1600 })
1601}
1602
1603/// I_seq with negative order support.
1604/// I_{-ν} = I_ν + (2/π)sin(νπ)K_ν (non-integer)
1605/// I_{-n} = I_n (integer)
1606#[cfg(feature = "alloc")]
1607#[allow(clippy::needless_range_loop, clippy::manual_memcpy)]
1608fn besseli_seq_neg<T: BesselFloat>(
1609 nu: T,
1610 z: Complex<T>,
1611 n: usize,
1612 scaling: Scaling,
1613) -> Result<BesselResult<T>, Error> {
1614 // n = 0 would underflow the `n - 1` order arithmetic below
1615 if n == 0 {
1616 return Err(Error::InvalidInput);
1617 }
1618 let zero = T::zero();
1619 let czero = Complex::new(zero, zero);
1620 let abs_nu = nu.abs();
1621 let nc = neg_count(nu, n);
1622 let is_int = as_integer(abs_nu).is_some();
1623
1624 if is_int {
1625 // I_{-n} = I_n. Same lattice, no correction needed.
1626 let (min_abs, max_abs) = if nc == n {
1627 // All orders negative: |orders| span [|ν|-(n-1), |ν|] (O(n) work)
1628 (abs_nu - T::from_f64((n - 1) as f64), abs_nu)
1629 } else {
1630 // Crossing zero: |orders| span [0, max(|ν|, ν+n-1)], all needed anyway
1631 let last_order = nu + T::from_f64((n - 1) as f64);
1632 (zero, abs_nu.max(last_order))
1633 };
1634 let buf_n = (max_abs - min_abs).to_usize().unwrap_or(0) + 1;
1635 let mut buf = alloc_crate::vec![czero; buf_n];
1636 let (_, status) = besi::zbesi(z, min_abs, scaling, &mut buf)?;
1637
1638 let mut values = alloc_crate::vec![czero; n];
1639 for j in 0..n {
1640 let order = nu + T::from_f64(j as f64);
1641 let idx = (order.abs() - min_abs).to_usize().unwrap_or(0);
1642 values[j] = buf[idx];
1643 }
1644 let uc = recount_underflow(&values);
1645 return Ok(BesselResult {
1646 values,
1647 underflow_count: uc,
1648 status,
1649 });
1650 }
1651
1652 if nc == n {
1653 // All negative, non-integer: need I and K at positive |orders|
1654 let start = abs_nu - T::from_f64((n - 1) as f64);
1655 let mut i_buf = alloc_crate::vec![czero; n];
1656 let mut k_buf = alloc_crate::vec![czero; n];
1657 let (_, s1) = besi::zbesi(z, start, scaling, &mut i_buf)?;
1658 let (_, s2) = besk::zbesk(z, start, scaling, &mut k_buf)?;
1659
1660 let mut values = alloc_crate::vec![czero; n];
1661 for j in 0..n {
1662 let k_idx = n - 1 - j;
1663 let order = abs_nu - T::from_f64(j as f64);
1664 let mut k_val = k_buf[k_idx];
1665 if scaling == Scaling::Exponential {
1666 k_val = k_to_i_scaling_correction(z, k_val);
1667 }
1668 values[j] = reflect_i_element(order, i_buf[k_idx], k_val);
1669 }
1670 let uc = recount_underflow(&values);
1671 return Ok(BesselResult {
1672 values,
1673 underflow_count: uc,
1674 status: worse_status(s1, s2),
1675 });
1676 }
1677
1678 // Non-integer crossing: two lattices
1679 let frac_neg = abs_nu - abs_nu.floor();
1680 let mut neg_i = alloc_crate::vec![czero; nc];
1681 let mut neg_k = alloc_crate::vec![czero; nc];
1682 let (_, s1) = besi::zbesi(z, frac_neg, scaling, &mut neg_i)?;
1683 let (_, s2) = besk::zbesk(z, frac_neg, scaling, &mut neg_k)?;
1684
1685 let pos_start = nu + T::from_f64(nc as f64);
1686 let pos_n = n - nc;
1687 let mut pos_buf = alloc_crate::vec![czero; pos_n];
1688 let (_, s3) = besi::zbesi(z, pos_start, scaling, &mut pos_buf)?;
1689
1690 let mut values = alloc_crate::vec![czero; n];
1691 for j in 0..nc {
1692 let abs_order = abs_nu - T::from_f64(j as f64);
1693 let k_idx = abs_order.floor().to_usize().unwrap_or(0);
1694 let mut k_val = neg_k[k_idx];
1695 if scaling == Scaling::Exponential {
1696 k_val = k_to_i_scaling_correction(z, k_val);
1697 }
1698 values[j] = reflect_i_element(abs_order, neg_i[k_idx], k_val);
1699 }
1700 for j in 0..pos_n {
1701 values[nc + j] = pos_buf[j];
1702 }
1703
1704 let status = worse_status(worse_status(s1, s2), s3);
1705 let uc = recount_underflow(&values);
1706 Ok(BesselResult {
1707 values,
1708 underflow_count: uc,
1709 status,
1710 })
1711}
1712
1713#[cfg(feature = "alloc")]
1714/// Compute J<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
1715///
1716/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
1717/// - [`Accuracy::Normal`] — no significant precision loss
1718/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
1719///
1720/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1721/// see [crate-level docs](crate#exponential-scaling) for details.
1722///
1723/// Supports negative ν via the same reflection formula as [`besselj`].
1724///
1725/// # Example
1726///
1727/// ```
1728/// # #[cfg(feature = "alloc")] {
1729/// use complex_bessel::*;
1730/// use num_complex::Complex;
1731///
1732/// let z = Complex::new(1.0_f64, 0.0);
1733///
1734/// // J_0(z), J_1(z), J_2(z) in one call
1735/// let result = besselj_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
1736/// assert_eq!(result.values.len(), 3);
1737/// assert!((result.values[0].re - 0.7652).abs() < 1e-3); // J_0(1) ≈ 0.7652
1738/// # }
1739/// ```
1740///
1741/// # Errors
1742///
1743/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
1744/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
1745/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1746/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1747pub fn besselj_seq<T: BesselFloat>(
1748 nu: T,
1749 z: Complex<T>,
1750 n: usize,
1751 scaling: Scaling,
1752) -> Result<BesselResult<T>, Error> {
1753 if nu < T::zero() {
1754 return besselj_seq_neg(nu, z, n, scaling);
1755 }
1756 seq_helper(n, |y| besj::zbesj(z, nu, scaling, y))
1757}
1758
1759#[cfg(feature = "alloc")]
1760/// Compute Y<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
1761///
1762/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
1763/// - [`Accuracy::Normal`] — no significant precision loss
1764/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
1765///
1766/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1767/// see [crate-level docs](crate#exponential-scaling) for details.
1768///
1769/// Supports negative ν via the same reflection formula as [`bessely`].
1770///
1771/// # Example
1772///
1773/// ```
1774/// # #[cfg(feature = "alloc")] {
1775/// use complex_bessel::*;
1776/// use num_complex::Complex;
1777///
1778/// let z = Complex::new(1.0_f64, 0.0);
1779///
1780/// // Y_0(z), Y_1(z), Y_2(z) in one call
1781/// let result = bessely_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
1782/// assert_eq!(result.values.len(), 3);
1783/// assert!((result.values[0].re - 0.0883).abs() < 1e-3); // Y_0(1) ≈ 0.0883
1784/// # }
1785/// ```
1786///
1787/// # Errors
1788///
1789/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
1790/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1791/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1792/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1793pub fn bessely_seq<T: BesselFloat>(
1794 nu: T,
1795 z: Complex<T>,
1796 n: usize,
1797 scaling: Scaling,
1798) -> Result<BesselResult<T>, Error> {
1799 if nu < T::zero() {
1800 return bessely_seq_neg(nu, z, n, scaling);
1801 }
1802 seq_helper(n, |y| besy::zbesy(z, nu, scaling, y))
1803}
1804
1805#[cfg(feature = "alloc")]
1806/// Compute I<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
1807///
1808/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
1809/// - [`Accuracy::Normal`] — no significant precision loss
1810/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
1811///
1812/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1813/// see [crate-level docs](crate#exponential-scaling) for details.
1814///
1815/// Supports negative ν via the same reflection formula as [`besseli`].
1816///
1817/// # Example
1818///
1819/// ```
1820/// # #[cfg(feature = "alloc")] {
1821/// use complex_bessel::*;
1822/// use num_complex::Complex;
1823///
1824/// let z = Complex::new(1.0_f64, 0.0);
1825///
1826/// // I_0(z), I_1(z), I_2(z) in one call
1827/// let result = besseli_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
1828/// assert_eq!(result.values.len(), 3);
1829/// assert!((result.values[0].re - 1.2661).abs() < 1e-3); // I_0(1) ≈ 1.2661
1830/// # }
1831/// ```
1832///
1833/// # Errors
1834///
1835/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0 and ν is a negative non-integer.
1836/// - [`Error::Overflow`] if |z| or |ν| is too large for a finite result.
1837/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1838/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1839pub fn besseli_seq<T: BesselFloat>(
1840 nu: T,
1841 z: Complex<T>,
1842 n: usize,
1843 scaling: Scaling,
1844) -> Result<BesselResult<T>, Error> {
1845 if nu < T::zero() {
1846 return besseli_seq_neg(nu, z, n, scaling);
1847 }
1848 seq_helper(n, |y| besi::zbesi(z, nu, scaling, y))
1849}
1850
1851#[cfg(feature = "alloc")]
1852/// Compute K<sub>ν+j</sub>(z) for j = 0, 1, …, n−1 in a single call.
1853///
1854/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
1855/// - [`Accuracy::Normal`] — no significant precision loss
1856/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
1857///
1858/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1859/// see [crate-level docs](crate#exponential-scaling) for details.
1860///
1861/// Supports negative ν via the same reflection formula as [`besselk`].
1862///
1863/// # Example
1864///
1865/// ```
1866/// # #[cfg(feature = "alloc")] {
1867/// use complex_bessel::*;
1868/// use num_complex::Complex;
1869///
1870/// let z = Complex::new(1.0_f64, 0.0);
1871///
1872/// // K_0(z), K_1(z), K_2(z) in one call
1873/// let result = besselk_seq(0.0, z, 3, Scaling::Unscaled).unwrap();
1874/// assert_eq!(result.values.len(), 3);
1875/// assert!((result.values[0].re - 0.4211).abs() < 1e-3); // K_0(1) ≈ 0.4211
1876/// # }
1877/// ```
1878///
1879/// # Errors
1880///
1881/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
1882/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1883/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1884/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1885pub fn besselk_seq<T: BesselFloat>(
1886 nu: T,
1887 z: Complex<T>,
1888 n: usize,
1889 scaling: Scaling,
1890) -> Result<BesselResult<T>, Error> {
1891 if nu < T::zero() {
1892 return besselk_seq_neg(nu, z, n, scaling);
1893 }
1894 seq_helper(n, |y| besk::zbesk(z, nu, scaling, y))
1895}
1896
1897#[cfg(feature = "alloc")]
1898/// Compute H<sub>ν+j</sub><sup>(1)</sup>(z) for j = 0, 1, …, n−1 in a single call.
1899///
1900/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
1901/// - [`Accuracy::Normal`] — no significant precision loss
1902/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
1903///
1904/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1905/// see [crate-level docs](crate#exponential-scaling) for details.
1906///
1907/// Supports negative ν via the same reflection formula as [`hankel1`].
1908///
1909/// # Example
1910///
1911/// ```
1912/// # #[cfg(feature = "alloc")] {
1913/// use complex_bessel::*;
1914/// use num_complex::Complex;
1915///
1916/// let z = Complex::new(1.0_f64, 0.0);
1917///
1918/// // H^(1)_0(z), H^(1)_1(z) in one call
1919/// let result = hankel1_seq(0.0, z, 2, Scaling::Unscaled).unwrap();
1920/// assert_eq!(result.values.len(), 2);
1921/// assert!((result.values[0].im - 0.0883).abs() < 1e-3); // Im = Y_0(1) ≈ 0.0883
1922/// # }
1923/// ```
1924///
1925/// # Errors
1926///
1927/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
1928/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1929/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1930/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1931pub fn hankel1_seq<T: BesselFloat>(
1932 nu: T,
1933 z: Complex<T>,
1934 n: usize,
1935 scaling: Scaling,
1936) -> Result<BesselResult<T>, Error> {
1937 if nu < T::zero() {
1938 return hankel_seq_neg(HankelKind::First, nu, z, n, scaling);
1939 }
1940 seq_helper(n, |y| besh::zbesh(z, nu, HankelKind::First, scaling, y))
1941}
1942
1943#[cfg(feature = "alloc")]
1944/// Compute H<sub>ν+j</sub><sup>(2)</sup>(z) for j = 0, 1, …, n−1 in a single call.
1945///
1946/// Returns a [`BesselResult`] containing `n` values and an [`Accuracy`]:
1947/// - [`Accuracy::Normal`] — no significant precision loss
1948/// - [`Accuracy::Reduced`] — more than half of significant digits may be lost (|z| or |ν| very large)
1949///
1950/// The `scaling` parameter selects [`Scaling::Unscaled`] or [`Scaling::Exponential`];
1951/// see [crate-level docs](crate#exponential-scaling) for details.
1952///
1953/// Supports negative ν via the same reflection formula as [`hankel2`].
1954///
1955/// # Example
1956///
1957/// ```
1958/// # #[cfg(feature = "alloc")] {
1959/// use complex_bessel::*;
1960/// use num_complex::Complex;
1961///
1962/// let z = Complex::new(1.0_f64, 0.0);
1963///
1964/// // H^(2)_0(z), H^(2)_1(z) in one call
1965/// let result = hankel2_seq(0.0, z, 2, Scaling::Unscaled).unwrap();
1966/// assert_eq!(result.values.len(), 2);
1967/// assert!((result.values[0].im + 0.0883).abs() < 1e-3); // Im = -Y_0(1) ≈ -0.0883
1968/// # }
1969/// ```
1970///
1971/// # Errors
1972///
1973/// - [`Error::InvalidInput`] if n < 1, if z or ν is not finite (NaN or infinite), or if z = 0.
1974/// - [`Error::Overflow`] if |z| is too small or too large for a finite result.
1975/// - [`Error::TotalPrecisionLoss`] if |z| or |ν| is too large for any significant digits (roughly > 10⁹ for f64).
1976/// - [`Error::ConvergenceFailure`] if an internal series or recurrence does not converge (rare).
1977pub fn hankel2_seq<T: BesselFloat>(
1978 nu: T,
1979 z: Complex<T>,
1980 n: usize,
1981 scaling: Scaling,
1982) -> Result<BesselResult<T>, Error> {
1983 if nu < T::zero() {
1984 return hankel_seq_neg(HankelKind::Second, nu, z, n, scaling);
1985 }
1986 seq_helper(n, |y| besh::zbesh(z, nu, HankelKind::Second, scaling, y))
1987}
1988
1989// ── Tests for non-finite input rejection at the public API level ──
1990// Deliberately gated on test only (not alloc), so they run in every
1991// feature configuration.
1992
1993#[cfg(test)]
1994mod non_finite_tests {
1995 use super::*;
1996 use num_complex::{Complex32, Complex64};
1997
1998 #[test]
1999 fn bessel_non_finite_z_returns_invalid_input() {
2000 let nan = f64::NAN;
2001 for z in [
2002 Complex64::new(nan, 0.0),
2003 Complex64::new(0.0, nan),
2004 Complex64::new(nan, nan),
2005 Complex64::new(f64::INFINITY, 0.0),
2006 ] {
2007 assert!(matches!(besselj(0.0, z), Err(Error::InvalidInput)));
2008 assert!(matches!(besselj_scaled(1.0, z), Err(Error::InvalidInput)));
2009 assert!(matches!(bessely(0.0, z), Err(Error::InvalidInput)));
2010 assert!(matches!(besseli(0.0, z), Err(Error::InvalidInput)));
2011 assert!(matches!(besselk(0.0, z), Err(Error::InvalidInput)));
2012 assert!(matches!(hankel1(0.0, z), Err(Error::InvalidInput)));
2013 assert!(matches!(hankel2(0.0, z), Err(Error::InvalidInput)));
2014 // Negative order exercises the reflection path in the wrappers.
2015 assert!(matches!(besselj(-2.5, z), Err(Error::InvalidInput)));
2016 assert!(matches!(besselk(-2.5, z), Err(Error::InvalidInput)));
2017 }
2018 }
2019
2020 #[test]
2021 fn bessel_non_finite_nu_returns_invalid_input() {
2022 let z = Complex64::new(1.0, 1.0);
2023 for nu in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2024 assert!(matches!(besselj(nu, z), Err(Error::InvalidInput)));
2025 assert!(matches!(bessely(nu, z), Err(Error::InvalidInput)));
2026 assert!(matches!(besseli(nu, z), Err(Error::InvalidInput)));
2027 assert!(matches!(besselk(nu, z), Err(Error::InvalidInput)));
2028 assert!(matches!(hankel1(nu, z), Err(Error::InvalidInput)));
2029 assert!(matches!(hankel2(nu, z), Err(Error::InvalidInput)));
2030 }
2031 }
2032
2033 #[test]
2034 fn bessel_non_finite_f32_returns_invalid_input() {
2035 let z = Complex32::new(1.0, 1.0);
2036 assert!(matches!(
2037 besselj(0.0f32, Complex32::new(f32::NAN, 0.0)),
2038 Err(Error::InvalidInput)
2039 ));
2040 assert!(matches!(besselj(f32::NAN, z), Err(Error::InvalidInput)));
2041 }
2042
2043 #[test]
2044 fn airy_non_finite_z_returns_invalid_input() {
2045 let nan = f64::NAN;
2046 for z in [
2047 Complex64::new(nan, 0.0),
2048 Complex64::new(0.0, nan),
2049 Complex64::new(f64::INFINITY, 0.0),
2050 ] {
2051 assert!(matches!(airy(z), Err(Error::InvalidInput)));
2052 assert!(matches!(airyprime(z), Err(Error::InvalidInput)));
2053 assert!(matches!(biry(z), Err(Error::InvalidInput)));
2054 assert!(matches!(biryprime(z), Err(Error::InvalidInput)));
2055 assert!(matches!(
2056 airy_raw(z, Scaling::Unscaled),
2057 Err(Error::InvalidInput)
2058 ));
2059 assert!(matches!(
2060 biry_raw(z, Scaling::Unscaled),
2061 Err(Error::InvalidInput)
2062 ));
2063 }
2064 }
2065
2066 #[test]
2067 fn finite_overflowing_modulus_keeps_total_precision_loss() {
2068 // |z| overflows to infinity while both components are finite: this
2069 // must stay TotalPrecisionLoss (range check), not InvalidInput.
2070 let z = Complex64::new(1.7e308, 1.7e308);
2071 assert!(matches!(besselj(0.0, z), Err(Error::TotalPrecisionLoss)));
2072 }
2073
2074 #[cfg(feature = "alloc")]
2075 #[test]
2076 fn seq_non_finite_inputs_return_invalid_input() {
2077 let z_nan = Complex64::new(f64::NAN, 0.0);
2078 let z = Complex64::new(1.0, 1.0);
2079 assert!(matches!(
2080 besselj_seq(f64::NAN, z, 2, Scaling::Unscaled),
2081 Err(Error::InvalidInput)
2082 ));
2083 assert!(matches!(
2084 besselj_seq(0.0, z_nan, 2, Scaling::Unscaled),
2085 Err(Error::InvalidInput)
2086 ));
2087 assert!(matches!(
2088 besselk_seq(-2.5, z_nan, 3, Scaling::Unscaled),
2089 Err(Error::InvalidInput)
2090 ));
2091 }
2092}
2093
2094// ── Scaled vs unscaled consistency ──
2095//
2096// Every `_scaled` function is the unscaled one times an elementary factor, so
2097// the two must agree wherever both are representable. The grid reaches the
2098// regions where the scaled paths switch to underflow/overflow handling
2099// (2|Im z| above ELIM for Y, large orders), which a value comparison at
2100// moderate arguments does not.
2101
2102#[cfg(test)]
2103mod scaling_tests {
2104 use super::*;
2105 use num_complex::Complex64;
2106
2107 type F<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
2108
2109 fn fns<T: BesselFloat>() -> [(&'static str, F<T>, F<T>); 6] {
2110 [
2111 ("J", besselj, besselj_scaled),
2112 ("Y", bessely, bessely_scaled),
2113 ("I", besseli, besseli_scaled),
2114 ("K", besselk, besselk_scaled),
2115 ("H1", hankel1, hankel1_scaled),
2116 ("H2", hankel2, hankel2_scaled),
2117 ]
2118 }
2119
2120 /// Log of the factor in scaled = exp(c)·unscaled.
2121 fn log_factor(name: &str, z: Complex64) -> Complex64 {
2122 let i = Complex64::new(0.0, 1.0);
2123 match name {
2124 "J" | "Y" => Complex64::new(-z.im.abs(), 0.0),
2125 "I" => Complex64::new(-z.re.abs(), 0.0),
2126 "K" => z,
2127 "H1" => -i * z,
2128 _ => i * z,
2129 }
2130 }
2131
2132 /// Compare scaled with exp(c)·unscaled, formed in log space so neither
2133 /// the factor nor the product overflows. Pairs where either value fails,
2134 /// is non-finite, or lies below `floor` (underflow handling) are skipped.
2135 fn check<T: BesselFloat>(nus: &[f64], radii: &[f64], floor: f64, tol: f64) {
2136 let to64 = |v: Complex<T>| Complex64::new(v.re.to_f64().unwrap(), v.im.to_f64().unwrap());
2137 let mut compared = 0;
2138 for (name, unscaled, scaled) in fns::<T>() {
2139 for &nu in nus {
2140 for &r in radii {
2141 for k in 0..12 {
2142 // 12 angles over the full circle, including both axes
2143 let th =
2144 -core::f64::consts::PI + core::f64::consts::FRAC_PI_6 * f64::from(k);
2145 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
2146 let nu_t = T::from_f64(nu);
2147 let (Ok(u), Ok(s)) = (unscaled(nu_t, z), scaled(nu_t, z)) else {
2148 continue;
2149 };
2150 let (u, s) = (to64(u), to64(s));
2151 let ok = |v: Complex64| v.norm().is_finite() && v.norm() > floor;
2152 if !ok(u) || !ok(s) {
2153 continue;
2154 }
2155 let expected = (u.ln() + log_factor(name, to64(z))).exp();
2156 let err = (s - expected).norm() / expected.norm();
2157 assert!(
2158 err < tol,
2159 "{name}: nu={nu}, z={z:?}: scaled={s:?}, exp(c)*unscaled={expected:?}, \
2160 rel_err={err:e}"
2161 );
2162 compared += 1;
2163 }
2164 }
2165 }
2166 }
2167 assert!(compared > 0);
2168 }
2169
2170 #[test]
2171 fn scaled_matches_unscaled_f64() {
2172 // Radii past ELIM/2 ≈ 350 with orders up to beyond |z|. Observed up to
2173 // ~6e-13 (large ν and |z|); a lost or mis-scaled term gives O(1).
2174 check::<f64>(
2175 &[0.0, 0.3, 2.5, 45.7, 250.0, 575.0, 800.0, 1200.0],
2176 &[0.5, 5.0, 40.0, 200.0, 360.0, 450.0, 600.0],
2177 1e-290,
2178 1e-10,
2179 );
2180 }
2181
2182 #[test]
2183 fn scaled_matches_unscaled_f32() {
2184 // ELIM/2 ≈ 40 in f32. Observed up to ~4e-5.
2185 check::<f32>(
2186 &[0.0, 0.3, 2.5, 25.7, 65.0, 90.0, 130.0],
2187 &[0.5, 5.0, 20.0, 41.0, 50.0, 60.0, 75.0],
2188 1e-30,
2189 1e-3,
2190 );
2191 }
2192}
2193
2194// ── Tests for _seq functions (negative orders, recurrence paths) ──
2195
2196#[cfg(all(test, feature = "alloc"))]
2197mod seq_tests {
2198 use super::*;
2199 use num_complex::Complex64;
2200
2201 /// Power series path (ZSERI) where the top orders underflow after the
2202 /// near-underflow scaling was switched on: the lower orders retried after
2203 /// that must keep the scaling consistent (they came out multiplied by TOL,
2204 /// and so did every order below them through the recurrence).
2205 /// Whether a top order underflows is a rounding-level ZUCHK decision on
2206 /// the real axis, so scan several top orders and require at least one hit.
2207 #[test]
2208 fn seq_power_series_underflow_retry_keeps_scaling() {
2209 fn check<T: BesselFloat>(
2210 tops: core::ops::Range<i32>,
2211 ns: &[usize],
2212 z: Complex<T>,
2213 tol: f64,
2214 ) {
2215 for &n in ns {
2216 let mut hits = 0;
2217 for top in tops.clone() {
2218 let nu = T::from_f64(f64::from(top) - (n - 1) as f64);
2219 let r = besselj_seq(nu, z, n, Scaling::Unscaled).unwrap();
2220 if r.underflow_count == 0 || r.values[0].norm() == T::zero() {
2221 continue;
2222 }
2223 hits += 1;
2224 for (j, &got) in r.values.iter().enumerate() {
2225 if got.norm() == T::zero() {
2226 continue;
2227 }
2228 let order = nu + T::from_f64(j as f64);
2229 let single = besselj(order, z).unwrap();
2230 let err = ((got - single).norm() / single.norm()).to_f64().unwrap();
2231 // Loose bound: the scaled path near underflow differs from the
2232 // single value by ~1e-13 (as in Fortran); the bug was a factor of TOL.
2233 assert!(
2234 err < tol,
2235 "n={n} order={:?}: {got:?} vs {single:?}",
2236 order.to_f64()
2237 );
2238 }
2239 }
2240 assert!(hits > 0, "n={n}: no case with a partial underflow");
2241 }
2242 }
2243 check(202..219, &[3, 40], Complex64::new(6.5, 0.0), 1e-10);
2244 check(36..48, &[3, 20], Complex::new(6.5f32, 0.0), 1e-4);
2245 }
2246
2247 // ── Edge: n = 0 with negative ν ──
2248
2249 #[test]
2250 fn seq_neg_n_zero_is_invalid_input() {
2251 let z = Complex64::new(1.5, 0.5);
2252 let s = Scaling::Unscaled;
2253 // Non-integer and integer ν take different code paths
2254 for nu in [-1.5, -2.0] {
2255 assert_eq!(besselj_seq(nu, z, 0, s).unwrap_err(), Error::InvalidInput);
2256 assert_eq!(bessely_seq(nu, z, 0, s).unwrap_err(), Error::InvalidInput);
2257 assert_eq!(besseli_seq(nu, z, 0, s).unwrap_err(), Error::InvalidInput);
2258 assert_eq!(besselk_seq(nu, z, 0, s).unwrap_err(), Error::InvalidInput);
2259 assert_eq!(hankel1_seq(nu, z, 0, s).unwrap_err(), Error::InvalidInput);
2260 assert_eq!(hankel2_seq(nu, z, 0, s).unwrap_err(), Error::InvalidInput);
2261 }
2262 }
2263
2264 // ── Seq/single consistency over a grid of regions ──
2265 //
2266 // Every element of a `_seq` result must match the single-value function at
2267 // the same order. The grid spans negative and positive orders, small to
2268 // near-overflow |z|, all four quadrants, and long sequences, so it reaches
2269 // the recurrence, rescaling, and analytic-continuation paths that only run
2270 // for n > 1.
2271
2272 type SeqFn<T> = fn(T, Complex<T>, usize, Scaling) -> Result<BesselResult<T>, Error>;
2273 type SingleFn<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
2274 /// (name, seq, single unscaled, single scaled)
2275 type SeqCase<T> = (&'static str, SeqFn<T>, SingleFn<T>, SingleFn<T>);
2276
2277 fn seq_fns<T: BesselFloat>() -> [SeqCase<T>; 6] {
2278 [
2279 ("J", besselj_seq, besselj, besselj_scaled),
2280 ("Y", bessely_seq, bessely, bessely_scaled),
2281 ("I", besseli_seq, besseli, besseli_scaled),
2282 ("K", besselk_seq, besselk, besselk_scaled),
2283 ("H1", hankel1_seq, hankel1, hankel1_scaled),
2284 ("H2", hankel2_seq, hankel2, hankel2_scaled),
2285 ]
2286 }
2287
2288 /// Compare seq[j] with single(ν+j) for every (ν, |z|, arg z) on the grid.
2289 /// A sequence may fail as a whole only if some of its orders fail on their
2290 /// own (e.g. orders below the requested range must not cause an error).
2291 /// The converse does not hold: Amos's overflow tests depend on the starting
2292 /// order of the call, so a single value can report `Overflow` where a
2293 /// sequence that contains it succeeds (as in the Fortran, e.g. f32 K at
2294 /// ν = 10.5 alone vs. from ν = 7.5 with n = 9, z = 90·e^{-2.6i}). Elements whose single value
2295 /// is an error, non-finite, or below 1e3 times the smallest normal number
2296 /// are skipped.
2297 fn check_seq_grid<T: BesselFloat>(
2298 nus: &[f64],
2299 radii: &[f64],
2300 args: &[f64],
2301 n: usize,
2302 tol: f64,
2303 ) {
2304 let floor = T::MACH_TINY * T::from_f64(1e3);
2305 for (name, seq, single, single_scaled) in seq_fns::<T>() {
2306 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
2307 let single = if scaling == Scaling::Unscaled {
2308 single
2309 } else {
2310 single_scaled
2311 };
2312 for &nu in nus {
2313 for &r in radii {
2314 for &th in args {
2315 let nu_t = T::from_f64(nu);
2316 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
2317 let res = match seq(nu_t, z, n, scaling) {
2318 Ok(res) => res,
2319 Err(e) => {
2320 let some_order_fails = (0..n)
2321 .any(|j| single(nu_t + T::from_f64(j as f64), z).is_err());
2322 assert!(
2323 some_order_fails,
2324 "{name} {scaling:?}: nu={nu}, z={z:?}, n={n}: seq \
2325 failed with {e:?} but every single value succeeds"
2326 );
2327 continue;
2328 }
2329 };
2330 assert_eq!(res.values.len(), n);
2331 for (j, &w) in res.values.iter().enumerate() {
2332 let order = nu_t + T::from_f64(j as f64);
2333 let Ok(v) = single(order, z) else {
2334 continue;
2335 };
2336 if !(v.norm() > floor && v.norm().is_finite()) {
2337 continue;
2338 }
2339 let err = ((w - v).norm() / v.norm()).to_f64().unwrap();
2340 assert!(
2341 err < tol,
2342 "{name} {scaling:?}: nu={nu}, z={z:?}, n={n}, j={j}: \
2343 seq={w:?}, single={v:?}, rel_err={err:e}"
2344 );
2345 }
2346 }
2347 }
2348 }
2349 }
2350 }
2351 }
2352
2353 /// Angles covering all four quadrants, the real and imaginary axes, and
2354 /// both sides of the |Im z| = √3·|Re z| region boundary.
2355 const ARGS: [f64; 8] = [
2356 0.0,
2357 0.5,
2358 1.2,
2359 core::f64::consts::FRAC_PI_2,
2360 2.2,
2361 3.0,
2362 -1.0,
2363 -2.6,
2364 ];
2365
2366 /// Recurrence paths in f64. The seq values come from recurrence while the
2367 /// single values come from direct expansions, so their ~1e-14 errors do not
2368 /// cancel (observed up to ~6e-12 for n = 100); a misplaced or mis-scaled
2369 /// element gives O(1e-3) error or worse.
2370 const TOL_GRID_F64: f64 = 1e-10;
2371
2372 #[test]
2373 fn seq_matches_single_grid_f64() {
2374 // Orders: all-negative / crossing (integer and not), small, and beyond
2375 // FNUL (≈ 86) so the uniform expansions recur. Radii include values near
2376 // the ELIM/ALIM scaling thresholds (≈ 665–700) and beyond, where low
2377 // orders overflow but ν = -200 does not. ν = 575 at |z| = 360 reaches
2378 // scaled Y with 2|Im z| > ELIM, where the K term still dominates. -0.0
2379 // is an edge case of the sign dispatch; n = 1 is the one-element
2380 // lattice edge case.
2381 for n in [1, 9] {
2382 check_seq_grid::<f64>(
2383 &[
2384 -200.0, -20.0, -12.3, -5.3, -3.0, -2.5, -0.0, 0.0, 0.4, 13.5, 46.2, 81.7,
2385 121.3, 575.0,
2386 ],
2387 &[
2388 0.3, 3.0, 17.0, 45.0, 120.0, 330.0, 360.0, 666.0, 698.0, 705.0, 709.0,
2389 ],
2390 &ARGS,
2391 n,
2392 TOL_GRID_F64,
2393 );
2394 }
2395 }
2396
2397 #[test]
2398 fn seq_matches_single_grid_f64_long() {
2399 // n = 100 with small ν: ν + n − 1 exceeds FNUL while ν itself does not
2400 check_seq_grid::<f64>(&[0.5, 3.3], &[2.2, 25.0, 90.0], &ARGS, 100, TOL_GRID_F64);
2401 }
2402
2403 /// f32 hits the scaling thresholds at |values| ≈ 1e28, so moderate |z|
2404 /// exercises the rescaling branches. Observed up to ~1.4e-4 on this grid;
2405 /// misplaced or mis-scaled elements give O(1) error or worse. (Off this grid,
2406 /// near the turning point ν ≈ |z| ≈ FNUL, Amos's single-precision algorithm
2407 /// itself loses up to ~2e-2, so random points would need a looser bound.)
2408 const TOL_GRID_F32: f64 = 1e-3;
2409
2410 #[test]
2411 fn seq_matches_single_grid_f32() {
2412 // At |z| = 90, low orders overflow but ν = -60 does not. ν = 70 at
2413 // |z| = 45 reaches scaled Y with 2|Im z| > ELIM (K term dominant).
2414 check_seq_grid::<f32>(
2415 &[
2416 -60.0, -9.0, -4.6, -0.0, 0.0, 0.4, 7.5, 21.2, 29.9, 46.3, 70.0,
2417 ],
2418 &[0.3, 3.0, 17.0, 45.0, 70.0, 90.0],
2419 &ARGS,
2420 9,
2421 TOL_GRID_F32,
2422 );
2423 }
2424
2425 // ── Three-term recurrence across ν = 0 ──
2426 //
2427 // Negative orders come from reflection formulas shared by the single-value
2428 // and `_seq` functions, so comparing the two cannot catch an error in a
2429 // formula. The recurrence can: across ν = 0 it links reflected values to
2430 // directly computed ones. Every scaling factor is independent of the order,
2431 // so scaled values satisfy the same recurrence.
2432
2433 /// Check C(ν-1) + s·C(ν+1) = t·(2ν/z)·C(ν) on consecutive `_seq` elements,
2434 /// with (s, t) = (1, 1) for J, Y, H1, H2, (-1, 1) for I, and (-1, -1) for K
2435 /// (DLMF 10.6.1, 10.29.1). The residual is relative to the largest term.
2436 fn check_recurrence_grid<T: BesselFloat>(
2437 nus: &[f64],
2438 radii: &[f64],
2439 args: &[f64],
2440 n: usize,
2441 tol: f64,
2442 ) {
2443 for (name, seq, _, _) in seq_fns::<T>() {
2444 let (s, t) = match name {
2445 "I" => (-1.0, 1.0),
2446 "K" => (-1.0, -1.0),
2447 _ => (1.0, 1.0),
2448 };
2449 let (s, t) = (T::from_f64(s), T::from_f64(t));
2450 for scaling in [Scaling::Unscaled, Scaling::Exponential] {
2451 for &nu in nus {
2452 for &r in radii {
2453 for &th in args {
2454 let nu_t = T::from_f64(nu);
2455 let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
2456 let Ok(res) = seq(nu_t, z, n, scaling) else {
2457 continue;
2458 };
2459 for (j, w) in res.values.windows(3).enumerate() {
2460 // Underflowed (zeroed) or overflowed elements break the recurrence
2461 if w.iter()
2462 .any(|v| v.norm() == T::zero() || !v.norm().is_finite())
2463 {
2464 continue;
2465 }
2466 let order = nu_t + T::from_f64((j + 1) as f64);
2467 let lhs = w[0] + w[2] * s;
2468 let rhs = w[1] * (z.inv() * (T::from_f64(2.0) * order * t));
2469 let mag = w[0].norm().max(w[2].norm()).max(rhs.norm());
2470 let err = ((lhs - rhs).norm() / mag).to_f64().unwrap();
2471 assert!(
2472 err < tol,
2473 "{name} {scaling:?}: nu={nu}, z={z:?}, order={:?}: \
2474 recurrence residual {err:e}",
2475 order.to_f64()
2476 );
2477 }
2478 }
2479 }
2480 }
2481 }
2482 }
2483 }
2484
2485 #[test]
2486 fn seq_recurrence_across_zero_f64() {
2487 // Non-integer (generic and half-integer) and integer orders crossing
2488 // zero, plus all-negative sequences.
2489 check_recurrence_grid::<f64>(
2490 &[-12.3, -5.3, -4.0, -3.7, -2.5, -0.3],
2491 &[0.3, 3.0, 17.0, 45.0],
2492 &ARGS,
2493 9,
2494 1e-11, // observed up to ~1.3e-13
2495 );
2496 }
2497
2498 #[test]
2499 fn seq_recurrence_across_zero_f32() {
2500 check_recurrence_grid::<f32>(
2501 &[-12.3, -5.3, -4.0, -3.7, -2.5, -0.3],
2502 &[0.3, 3.0, 17.0, 45.0],
2503 &ARGS,
2504 9,
2505 1e-4, // observed up to ~1e-5
2506 );
2507 }
2508}