#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), allow(rustdoc::broken_intra_doc_links))]
#![no_std]
#[cfg(feature = "std")]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
pub(crate) mod airy;
pub(crate) mod algo;
pub(crate) mod axis;
pub(crate) mod besh;
pub(crate) mod besi;
pub(crate) mod besj;
pub(crate) mod besk;
pub(crate) mod besy;
pub mod machine;
pub mod types;
pub(crate) mod utils;
pub use machine::BesselFloat;
#[cfg(feature = "std")]
pub use types::BesselResult;
pub use types::{Accuracy, AiryResult, Error, Scaling, SeqInfo};
use num_complex::Complex;
use types::{AiryDerivative, HankelKind};
use utils::{Workspace, with_workspace};
#[inline]
fn as_integer<T: BesselFloat>(nu: T) -> Option<i64> {
if nu == nu.floor() {
nu.to_i64()
} else {
None
}
}
#[inline]
fn reflect_j_element<T: BesselFloat>(order: T, j: Complex<T>, y: Complex<T>) -> Complex<T> {
j * utils::cospi(order) - y * utils::sinpi(order)
}
#[inline]
fn reflect_y_element<T: BesselFloat>(order: T, j: Complex<T>, y: Complex<T>) -> Complex<T> {
j * utils::sinpi(order) + y * utils::cospi(order)
}
#[inline]
fn reflect_i_element<T: BesselFloat>(order: T, i_val: Complex<T>, k_val: Complex<T>) -> Complex<T> {
let pi = T::from_f64(core::f64::consts::PI);
let two = T::from_f64(2.0);
utils::mul_add_scalar(k_val, two / pi * utils::sinpi(order), i_val)
}
#[inline]
fn reflect_h_element<T: BesselFloat>(order: T, kind: HankelKind, h: Complex<T>) -> Complex<T> {
let cos_nu_pi = utils::cospi(order);
let sin_nu_pi = utils::sinpi(order);
let rotation = match kind {
HankelKind::First => Complex::new(cos_nu_pi, sin_nu_pi),
HankelKind::Second => Complex::new(cos_nu_pi, -sin_nu_pi),
};
h * rotation
}
#[inline]
fn k_to_i_scaling_correction<T: BesselFloat>(z: Complex<T>, k_val: Complex<T>) -> Complex<T> {
let two = T::from_f64(2.0);
let (sin_a, cos_a) = (-z.im).sin_cos();
let mut result = k_val * Complex::new(cos_a, sin_a);
if z.re > T::zero() {
result = result * (-two * z.re).exp();
}
result
}
#[inline]
fn integer_sign<T: BesselFloat>(n: i64) -> T {
if n % 2 == 0 { T::one() } else { -T::one() }
}
#[inline]
fn besselj_value<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let zero = T::zero();
let czero = Complex::new(zero, zero);
if nu >= zero {
let mut y = [czero];
besj::zbesj(z, nu, scaling, &mut y)?;
return Ok(y[0]);
}
let abs_nu = nu.abs();
if let Some(n) = as_integer(abs_nu) {
let mut y = [czero];
besj::zbesj(z, abs_nu, scaling, &mut y)?;
return Ok(y[0] * integer_sign::<T>(n));
}
if utils::cospi(abs_nu) == zero {
let mut y_buf = [czero];
besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
return Ok(reflect_j_element(abs_nu, czero, y_buf[0]));
}
let mut j_buf = [czero];
let mut y_buf = [czero];
besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
Ok(reflect_j_element(abs_nu, j_buf[0], y_buf[0]))
}
#[inline]
fn bessely_value<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let zero = T::zero();
let czero = Complex::new(zero, zero);
if nu >= zero {
let mut y = [czero];
besy::zbesy(z, nu, scaling, &mut y)?;
return Ok(y[0]);
}
let abs_nu = nu.abs();
if let Some(n) = as_integer(abs_nu) {
let mut y = [czero];
besy::zbesy(z, abs_nu, scaling, &mut y)?;
return Ok(y[0] * integer_sign::<T>(n));
}
if utils::cospi(abs_nu) == zero {
let mut j_buf = [czero];
besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
return Ok(reflect_y_element(abs_nu, j_buf[0], czero));
}
let mut j_buf = [czero];
let mut y_buf = [czero];
besj::zbesj(z, abs_nu, scaling, &mut j_buf)?;
besy::zbesy(z, abs_nu, scaling, &mut y_buf)?;
Ok(reflect_y_element(abs_nu, j_buf[0], y_buf[0]))
}
#[inline]
fn besseli_internal<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let zero = T::zero();
let czero = Complex::new(zero, zero);
if nu >= zero {
let mut y = [czero];
besi::zbesi(z, nu, scaling, &mut y)?;
return Ok(y[0]);
}
let abs_nu = nu.abs();
if as_integer(abs_nu).is_some() {
let mut y = [czero];
besi::zbesi(z, abs_nu, scaling, &mut y)?;
return Ok(y[0]);
}
let mut i_buf = [czero];
let mut k_buf = [czero];
besi::zbesi(z, abs_nu, scaling, &mut i_buf)?;
besk::zbesk(z, abs_nu, scaling, &mut k_buf)?;
let mut k_val = k_buf[0];
if scaling == Scaling::Exponential {
k_val = k_to_i_scaling_correction(z, k_val);
}
Ok(reflect_i_element(abs_nu, i_buf[0], k_val))
}
#[inline]
fn besselk_value<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let abs_nu = nu.abs();
let zero = T::zero();
let mut y = [Complex::new(zero, zero)];
besk::zbesk(z, abs_nu, scaling, &mut y)?;
Ok(y[0])
}
#[inline]
fn hankel_value<T: BesselFloat>(
kind: HankelKind,
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let zero = T::zero();
if nu >= zero {
let mut y = [Complex::new(zero, zero)];
besh::zbesh(z, nu, kind, scaling, &mut y)?;
return Ok(y[0]);
}
let abs_nu = nu.abs();
let mut y = [Complex::new(zero, zero)];
besh::zbesh(z, abs_nu, kind, scaling, &mut y)?;
Ok(reflect_h_element(abs_nu, kind, y[0]))
}
#[inline]
fn besselj_internal<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
Ok(axis::real_on_positive_axis(
z,
besselj_value(nu, z, scaling)?,
))
}
#[inline]
fn bessely_internal<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let v = axis::real_on_positive_axis(z, bessely_value(nu, z, scaling)?);
Ok(axis::fix_single(axis::AxisFn::Y, nu, z, scaling, v))
}
#[inline]
fn besselk_internal<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let v = besselk_value(nu, z, scaling)?;
Ok(axis::fix_single(axis::AxisFn::K, nu, z, scaling, v))
}
#[inline]
fn hankel_internal<T: BesselFloat>(
kind: HankelKind,
nu: T,
z: Complex<T>,
scaling: Scaling,
) -> Result<Complex<T>, Error> {
let v = hankel_value(kind, nu, z, scaling)?;
Ok(axis::fix_single(
axis::AxisFn::Hankel(kind),
nu,
z,
scaling,
v,
))
}
#[inline]
pub fn besselj<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
besselj_internal(nu, z, Scaling::Unscaled)
}
#[inline]
pub fn bessely<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
bessely_internal(nu, z, Scaling::Unscaled)
}
#[inline]
pub fn besseli<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
besseli_internal(nu, z, Scaling::Unscaled)
}
#[inline]
pub fn besselk<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
besselk_internal(nu, z, Scaling::Unscaled)
}
#[inline]
pub fn hankel1<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
hankel_internal(HankelKind::First, nu, z, Scaling::Unscaled)
}
#[inline]
pub fn hankel2<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
hankel_internal(HankelKind::Second, nu, z, Scaling::Unscaled)
}
#[inline]
pub fn airy<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Value, Scaling::Unscaled)?;
Ok(result)
}
#[inline]
pub fn airyprime<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Derivative, Scaling::Unscaled)?;
Ok(result)
}
#[inline]
pub fn biry<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _status) = airy::zbiry(z, AiryDerivative::Value, Scaling::Unscaled)?;
Ok(result)
}
#[inline]
pub fn biryprime<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _status) = airy::zbiry(z, AiryDerivative::Derivative, Scaling::Unscaled)?;
Ok(result)
}
#[inline]
pub fn besselj_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
besselj_internal(nu, z, Scaling::Exponential)
}
#[inline]
pub fn bessely_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
bessely_internal(nu, z, Scaling::Exponential)
}
#[inline]
pub fn besseli_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
besseli_internal(nu, z, Scaling::Exponential)
}
#[inline]
pub fn besselk_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
besselk_internal(nu, z, Scaling::Exponential)
}
#[inline]
pub fn hankel1_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
hankel_internal(HankelKind::First, nu, z, Scaling::Exponential)
}
#[inline]
pub fn hankel2_scaled<T: BesselFloat>(nu: T, z: Complex<T>) -> Result<Complex<T>, Error> {
hankel_internal(HankelKind::Second, nu, z, Scaling::Exponential)
}
#[inline]
pub fn airy_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Value, Scaling::Exponential)?;
Ok(result)
}
#[inline]
pub fn airyprime_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _nz, _status) = airy::zairy(z, AiryDerivative::Derivative, Scaling::Exponential)?;
Ok(result)
}
#[inline]
pub fn biry_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _status) = airy::zbiry(z, AiryDerivative::Value, Scaling::Exponential)?;
Ok(result)
}
#[inline]
pub fn biryprime_scaled<T: BesselFloat>(z: Complex<T>) -> Result<Complex<T>, Error> {
let (result, _status) = airy::zbiry(z, AiryDerivative::Derivative, Scaling::Exponential)?;
Ok(result)
}
#[inline]
pub fn airy_raw<T: BesselFloat>(z: Complex<T>, scaling: Scaling) -> Result<AiryResult<T>, Error> {
let (value, _nz, status) = airy::zairy(z, AiryDerivative::Value, scaling)?;
Ok(AiryResult { value, status })
}
#[inline]
pub fn airyprime_raw<T: BesselFloat>(
z: Complex<T>,
scaling: Scaling,
) -> Result<AiryResult<T>, Error> {
let (value, _nz, status) = airy::zairy(z, AiryDerivative::Derivative, scaling)?;
Ok(AiryResult { value, status })
}
#[inline]
pub fn biry_raw<T: BesselFloat>(z: Complex<T>, scaling: Scaling) -> Result<AiryResult<T>, Error> {
let (value, status) = airy::zbiry(z, AiryDerivative::Value, scaling)?;
Ok(AiryResult { value, status })
}
#[inline]
pub fn biryprime_raw<T: BesselFloat>(
z: Complex<T>,
scaling: Scaling,
) -> Result<AiryResult<T>, Error> {
let (value, status) = airy::zbiry(z, AiryDerivative::Derivative, scaling)?;
Ok(AiryResult { value, status })
}
#[inline]
fn worse_status(a: Accuracy, b: Accuracy) -> Accuracy {
match (a, b) {
(Accuracy::Reduced, _) | (_, Accuracy::Reduced) => Accuracy::Reduced,
_ => Accuracy::Normal,
}
}
#[inline]
fn recount_underflow<T: BesselFloat>(values: &[Complex<T>]) -> usize {
let zero = Complex::new(T::zero(), T::zero());
values.iter().filter(|v| **v == zero).count()
}
#[inline]
fn neg_count<T: BesselFloat>(nu: T, n: usize) -> usize {
if nu >= T::zero() {
return 0;
}
let abs_nu = nu.abs();
let c = abs_nu.ceil();
let cn = if let Some(v) = c.to_usize() { v } else { n };
cn.min(n)
}
type SeqFn<'a, T> = dyn FnMut(T, &mut [Complex<T>]) -> Result<(usize, Accuracy), Error> + 'a;
#[allow(clippy::needless_range_loop)]
fn fill_abs_orders<T: BesselFloat>(
out: &mut [Complex<T>],
nu: T,
nc: usize,
f: &mut SeqFn<'_, T>,
) -> Result<Accuracy, Error> {
let n = out.len();
if nc == n {
let start = nu.abs() - T::from_f64((n - 1) as f64);
let (_, status) = f(start, out)?;
out.reverse();
return Ok(status);
}
let pos_n = n - nc; if pos_n > nc {
let (_, status) = f(T::zero(), &mut out[nc..])?;
for j in 0..nc {
out[j] = out[2 * nc - j];
}
Ok(status)
} else {
let (_, status) = f(T::zero(), &mut out[..=nc])?;
out[..=nc].reverse();
for m in 1..pos_n {
out[nc + m] = out[nc - m];
}
Ok(status)
}
}
fn fill_non_int_crossing<T: BesselFloat>(
out: &mut [Complex<T>],
nu: T,
nc: usize,
f: &mut SeqFn<'_, T>,
) -> Result<Accuracy, Error> {
let abs_nu = nu.abs();
let frac_neg = abs_nu - abs_nu.floor();
let (_, status1) = f(frac_neg, &mut out[..nc])?;
out[..nc].reverse();
let pos_start = nu + T::from_f64(nc as f64);
let (_, status2) = f(pos_start, &mut out[nc..])?;
Ok(worse_status(status1, status2))
}
fn reflect_lattice<T: BesselFloat>(
seg: &mut [Complex<T>],
abs_nu: T,
chunked_first: bool,
whole: &mut SeqFn<'_, T>,
chunked: &mut SeqFn<'_, T>,
combine: &dyn Fn(T, Complex<T>, Complex<T>) -> Complex<T>,
buf: &mut [Complex<T>],
) -> Result<Accuracy, Error> {
let len = seg.len();
let chunk = buf.len();
assert!(chunk > 0, "empty workspace");
let lo = abs_nu - T::from_f64((len - 1) as f64);
let mut status = Accuracy::Normal;
let mut whole_done = false;
if !chunked_first {
let (_, s) = whole(lo, seg)?;
status = s;
whole_done = true;
}
let mut c0 = 0;
while c0 < len {
let c1 = (c0 + chunk).min(len);
let start = lo + T::from_f64(c0 as f64);
let (_, s) = chunked(start, &mut buf[..c1 - c0])?;
status = worse_status(status, s);
if !whole_done {
let (_, s) = whole(lo, seg)?;
status = worse_status(status, s);
whole_done = true;
}
for k in c0..c1 {
let order = abs_nu - T::from_f64((len - 1 - k) as f64);
seg[k] = combine(order, seg[k], buf[k - c0]);
}
c0 = c1;
}
seg.reverse();
Ok(status)
}
fn apply_integer_sign<T: BesselFloat>(out: &mut [Complex<T>], nu: T) {
for (j, v) in out.iter_mut().enumerate() {
let order = nu + T::from_f64(j as f64);
if order < T::zero() {
let int_order = order.abs().to_i64().unwrap_or(0);
*v = *v * integer_sign::<T>(int_order);
}
}
}
#[inline]
fn check_nonempty<T>(out: &[T]) -> Result<(), Error> {
if out.is_empty() {
Err(Error::InvalidInput)
} else {
Ok(())
}
}
fn besselk_seq_neg<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
check_nonempty(out)?;
let nc = neg_count(nu, out.len());
let f = &mut |start, buf: &mut [Complex<T>]| besk::zbesk(z, start, scaling, buf);
let status = if nc == out.len() || as_integer(nu.abs()).is_some() {
fill_abs_orders(out, nu, nc, f)?
} else {
fill_non_int_crossing(out, nu, nc, f)?
};
Ok(SeqInfo {
underflow_count: recount_underflow(out),
status,
})
}
fn hankel_seq_neg<T: BesselFloat>(
kind: HankelKind,
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
check_nonempty(out)?;
let abs_nu = nu.abs();
let nc = neg_count(nu, out.len());
let f = &mut |start, buf: &mut [Complex<T>]| besh::zbesh(z, start, kind, scaling, buf);
let status = if nc == out.len() || as_integer(abs_nu).is_some() {
fill_abs_orders(out, nu, nc, f)?
} else {
fill_non_int_crossing(out, nu, nc, f)?
};
for (j, v) in out.iter_mut().take(nc).enumerate() {
let order = abs_nu - T::from_f64(j as f64);
*v = reflect_h_element(order, kind, *v);
}
Ok(SeqInfo {
underflow_count: recount_underflow(out),
status,
})
}
fn besselj_seq_neg<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
ws: Workspace<'_, T>,
) -> Result<SeqInfo, Error> {
check_nonempty(out)?;
let n = out.len();
let nc = neg_count(nu, n);
let status = if as_integer(nu.abs()).is_some() {
let f = &mut |start, buf: &mut [Complex<T>]| besj::zbesj(z, start, scaling, buf);
let status = fill_abs_orders(out, nu, nc, f)?;
apply_integer_sign(out, nu);
status
} else if utils::cospi(nu.abs()) == T::zero() {
with_workspace(ws, n, |ws| {
let abs_nu = nu.abs();
let czero = Complex::new(T::zero(), T::zero());
let (neg, pos) = out.split_at_mut(nc);
let status1 = fill_abs_orders(neg, nu, nc, &mut |start, buf| {
besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *ws))
})?;
for (j, v) in neg.iter_mut().enumerate() {
*v = reflect_j_element(abs_nu - T::from_f64(j as f64), czero, *v);
}
if pos.is_empty() {
return Ok(status1);
}
let (_, status2) = besj::zbesj(z, nu + T::from_f64(nc as f64), scaling, pos)?;
Ok(worse_status(status1, status2))
})?
} else {
with_workspace(ws, 2 * n, |ws| {
let (neg, pos) = out.split_at_mut(nc);
let (y_buf, k_buf) = ws.split_at_mut(ws.len() / 2);
let status1 = reflect_lattice(
neg,
nu.abs(),
false,
&mut |start, buf| besj::zbesj(z, start, scaling, buf),
&mut |start, buf| {
besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *k_buf))
},
&|order, j, y| reflect_j_element(order, j, y),
y_buf,
)?;
if pos.is_empty() {
return Ok(status1);
}
let (_, status2) = besj::zbesj(z, nu + T::from_f64(nc as f64), scaling, pos)?;
Ok(worse_status(status1, status2))
})?
};
Ok(SeqInfo {
underflow_count: recount_underflow(out),
status,
})
}
fn bessely_seq_neg<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
ws: Workspace<'_, T>,
) -> Result<SeqInfo, Error> {
check_nonempty(out)?;
let n = out.len();
let nc = neg_count(nu, n);
let is_int = as_integer(nu.abs()).is_some();
let half_int = !is_int && utils::cospi(nu.abs()) == T::zero();
let ws_len = if is_int || half_int { n } else { 2 * n };
let status = with_workspace(ws, ws_len, |ws| {
if is_int {
let f = &mut |start, buf: &mut [Complex<T>]| {
besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *ws))
};
let status = fill_abs_orders(out, nu, nc, f)?;
apply_integer_sign(out, nu);
return Ok(status);
}
let (neg, pos) = out.split_at_mut(nc);
let status1 = if half_int {
let abs_nu = nu.abs();
let czero = Complex::new(T::zero(), T::zero());
let status = fill_abs_orders(neg, nu, nc, &mut |start, buf| {
besj::zbesj(z, start, scaling, buf)
})?;
for (j, v) in neg.iter_mut().enumerate() {
*v = reflect_y_element(abs_nu - T::from_f64(j as f64), *v, czero);
}
status
} else {
let (y_buf, k_buf) = ws.split_at_mut(ws.len() / 2);
reflect_lattice(
neg,
nu.abs(),
false,
&mut |start, buf| besj::zbesj(z, start, scaling, buf),
&mut |start, buf| {
besy::zbesy_with(z, start, scaling, buf, Workspace::Slice(&mut *k_buf))
},
&|order, j, y| reflect_y_element(order, j, y),
y_buf,
)?
};
if pos.is_empty() {
return Ok(status1);
}
let pos_start = nu + T::from_f64(nc as f64);
let (_, status2) = besy::zbesy_with(z, pos_start, scaling, pos, Workspace::Slice(ws))?;
Ok(worse_status(status1, status2))
})?;
Ok(SeqInfo {
underflow_count: recount_underflow(out),
status,
})
}
fn besseli_seq_neg<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
ws: Workspace<'_, T>,
) -> Result<SeqInfo, Error> {
check_nonempty(out)?;
let n = out.len();
let nc = neg_count(nu, n);
let i_fn = &mut |start, buf: &mut [Complex<T>]| besi::zbesi(z, start, scaling, buf);
let status = if as_integer(nu.abs()).is_some() {
fill_abs_orders(out, nu, nc, i_fn)?
} else {
with_workspace(ws, n, |ws| {
let (neg, pos) = out.split_at_mut(nc);
let status1 = reflect_lattice(
neg,
nu.abs(),
true,
&mut |start, buf| besk::zbesk(z, start, scaling, buf),
i_fn,
&|order, k, i| {
let k = if scaling == Scaling::Exponential {
k_to_i_scaling_correction(z, k)
} else {
k
};
reflect_i_element(order, i, k)
},
ws,
)?;
if pos.is_empty() {
return Ok(status1);
}
let (_, status2) = besi::zbesi(z, nu + T::from_f64(nc as f64), scaling, pos)?;
Ok(worse_status(status1, status2))
})?
};
Ok(SeqInfo {
underflow_count: recount_underflow(out),
status,
})
}
fn besselj_seq_impl<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
ws: Workspace<'_, T>,
) -> Result<SeqInfo, Error> {
let info = if nu < T::zero() {
besselj_seq_neg(nu, z, scaling, out, ws)?
} else {
let (underflow_count, status) = besj::zbesj(z, nu, scaling, out)?;
SeqInfo {
underflow_count,
status,
}
};
for v in out.iter_mut() {
*v = axis::real_on_positive_axis(z, *v);
}
Ok(info)
}
fn bessely_seq_impl<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
ws: Workspace<'_, T>,
) -> Result<SeqInfo, Error> {
let info = if nu < T::zero() {
bessely_seq_neg(nu, z, scaling, out, ws)?
} else {
let (underflow_count, status) = besy::zbesy_with(z, nu, scaling, out, ws)?;
SeqInfo {
underflow_count,
status,
}
};
for v in out.iter_mut() {
*v = axis::real_on_positive_axis(z, *v);
}
axis::fix_seq(axis::AxisFn::Y, nu, z, scaling, out);
Ok(info)
}
fn besseli_seq_impl<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
ws: Workspace<'_, T>,
) -> Result<SeqInfo, Error> {
if nu < T::zero() {
return besseli_seq_neg(nu, z, scaling, out, ws);
}
let (underflow_count, status) = besi::zbesi(z, nu, scaling, out)?;
Ok(SeqInfo {
underflow_count,
status,
})
}
fn hankel_seq_impl<T: BesselFloat>(
kind: HankelKind,
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
let info = if nu < T::zero() {
hankel_seq_neg(kind, nu, z, scaling, out)?
} else {
let (underflow_count, status) = besh::zbesh(z, nu, kind, scaling, out)?;
SeqInfo {
underflow_count,
status,
}
};
axis::fix_seq(axis::AxisFn::Hankel(kind), nu, z, scaling, out);
Ok(info)
}
pub fn besselj_seq_into<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
besselj_seq_impl(nu, z, scaling, out, Workspace::Stack)
}
pub fn bessely_seq_into<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
bessely_seq_impl(nu, z, scaling, out, Workspace::Stack)
}
pub fn besseli_seq_into<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
besseli_seq_impl(nu, z, scaling, out, Workspace::Stack)
}
pub fn besselk_seq_into<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
let info = if nu < T::zero() {
besselk_seq_neg(nu, z, scaling, out)?
} else {
let (underflow_count, status) = besk::zbesk(z, nu, scaling, out)?;
SeqInfo {
underflow_count,
status,
}
};
axis::fix_seq(axis::AxisFn::K, nu, z, scaling, out);
Ok(info)
}
pub fn hankel1_seq_into<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
hankel_seq_impl(HankelKind::First, nu, z, scaling, out)
}
pub fn hankel2_seq_into<T: BesselFloat>(
nu: T,
z: Complex<T>,
scaling: Scaling,
out: &mut [Complex<T>],
) -> Result<SeqInfo, Error> {
hankel_seq_impl(HankelKind::Second, nu, z, scaling, out)
}
#[cfg(feature = "std")]
fn seq_helper<T: BesselFloat>(
n: usize,
f: impl FnOnce(&mut [Complex<T>]) -> Result<SeqInfo, Error>,
) -> Result<BesselResult<T>, Error> {
let zero = T::zero();
let mut values = alloc::vec![Complex::new(zero, zero); n];
let info = f(&mut values)?;
Ok(BesselResult {
values,
underflow_count: info.underflow_count,
status: info.status,
})
}
#[cfg(feature = "std")]
pub fn besselj_seq<T: BesselFloat>(
nu: T,
z: Complex<T>,
n: usize,
scaling: Scaling,
) -> Result<BesselResult<T>, Error> {
seq_helper(n, |out| {
besselj_seq_impl(nu, z, scaling, out, Workspace::Heap)
})
}
#[cfg(feature = "std")]
pub fn bessely_seq<T: BesselFloat>(
nu: T,
z: Complex<T>,
n: usize,
scaling: Scaling,
) -> Result<BesselResult<T>, Error> {
seq_helper(n, |out| {
bessely_seq_impl(nu, z, scaling, out, Workspace::Heap)
})
}
#[cfg(feature = "std")]
pub fn besseli_seq<T: BesselFloat>(
nu: T,
z: Complex<T>,
n: usize,
scaling: Scaling,
) -> Result<BesselResult<T>, Error> {
seq_helper(n, |out| {
besseli_seq_impl(nu, z, scaling, out, Workspace::Heap)
})
}
#[cfg(feature = "std")]
pub fn besselk_seq<T: BesselFloat>(
nu: T,
z: Complex<T>,
n: usize,
scaling: Scaling,
) -> Result<BesselResult<T>, Error> {
seq_helper(n, |out| besselk_seq_into(nu, z, scaling, out))
}
#[cfg(feature = "std")]
pub fn hankel1_seq<T: BesselFloat>(
nu: T,
z: Complex<T>,
n: usize,
scaling: Scaling,
) -> Result<BesselResult<T>, Error> {
seq_helper(n, |out| hankel1_seq_into(nu, z, scaling, out))
}
#[cfg(feature = "std")]
pub fn hankel2_seq<T: BesselFloat>(
nu: T,
z: Complex<T>,
n: usize,
scaling: Scaling,
) -> Result<BesselResult<T>, Error> {
seq_helper(n, |out| hankel2_seq_into(nu, z, scaling, out))
}
#[cfg(test)]
mod non_finite_tests {
use super::*;
use num_complex::{Complex32, Complex64};
#[test]
fn bessel_non_finite_z_returns_invalid_input() {
let nan = f64::NAN;
for z in [
Complex64::new(nan, 0.0),
Complex64::new(0.0, nan),
Complex64::new(nan, nan),
Complex64::new(f64::INFINITY, 0.0),
] {
assert!(matches!(besselj(0.0, z), Err(Error::InvalidInput)));
assert!(matches!(besselj_scaled(1.0, z), Err(Error::InvalidInput)));
assert!(matches!(bessely(0.0, z), Err(Error::InvalidInput)));
assert!(matches!(besseli(0.0, z), Err(Error::InvalidInput)));
assert!(matches!(besselk(0.0, z), Err(Error::InvalidInput)));
assert!(matches!(hankel1(0.0, z), Err(Error::InvalidInput)));
assert!(matches!(hankel2(0.0, z), Err(Error::InvalidInput)));
assert!(matches!(besselj(-2.5, z), Err(Error::InvalidInput)));
assert!(matches!(besselj(-2.3, z), Err(Error::InvalidInput)));
assert!(matches!(besselk(-2.5, z), Err(Error::InvalidInput)));
}
}
#[test]
fn bessel_non_finite_nu_returns_invalid_input() {
let z = Complex64::new(1.0, 1.0);
for nu in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(matches!(besselj(nu, z), Err(Error::InvalidInput)));
assert!(matches!(bessely(nu, z), Err(Error::InvalidInput)));
assert!(matches!(besseli(nu, z), Err(Error::InvalidInput)));
assert!(matches!(besselk(nu, z), Err(Error::InvalidInput)));
assert!(matches!(hankel1(nu, z), Err(Error::InvalidInput)));
assert!(matches!(hankel2(nu, z), Err(Error::InvalidInput)));
}
}
#[test]
fn bessel_non_finite_f32_returns_invalid_input() {
let z = Complex32::new(1.0, 1.0);
assert!(matches!(
besselj(0.0f32, Complex32::new(f32::NAN, 0.0)),
Err(Error::InvalidInput)
));
assert!(matches!(besselj(f32::NAN, z), Err(Error::InvalidInput)));
}
#[test]
fn airy_non_finite_z_returns_invalid_input() {
let nan = f64::NAN;
for z in [
Complex64::new(nan, 0.0),
Complex64::new(0.0, nan),
Complex64::new(f64::INFINITY, 0.0),
] {
assert!(matches!(airy(z), Err(Error::InvalidInput)));
assert!(matches!(airyprime(z), Err(Error::InvalidInput)));
assert!(matches!(biry(z), Err(Error::InvalidInput)));
assert!(matches!(biryprime(z), Err(Error::InvalidInput)));
assert!(matches!(
airy_raw(z, Scaling::Unscaled),
Err(Error::InvalidInput)
));
assert!(matches!(
biry_raw(z, Scaling::Unscaled),
Err(Error::InvalidInput)
));
}
}
#[test]
fn finite_overflowing_modulus_keeps_total_precision_loss() {
let z = Complex64::new(1.7e308, 1.7e308);
assert!(matches!(besselj(0.0, z), Err(Error::TotalPrecisionLoss)));
}
#[test]
fn seq_non_finite_inputs_return_invalid_input() {
let z_nan = Complex64::new(f64::NAN, 0.0);
let z = Complex64::new(1.0, 1.0);
let mut out = [Complex64::new(0.0, 0.0); 3];
assert!(matches!(
besselj_seq_into(f64::NAN, z, Scaling::Unscaled, &mut out[..2]),
Err(Error::InvalidInput)
));
assert!(matches!(
besselj_seq_into(0.0, z_nan, Scaling::Unscaled, &mut out[..2]),
Err(Error::InvalidInput)
));
assert!(matches!(
besselk_seq_into(-2.5, z_nan, Scaling::Unscaled, &mut out),
Err(Error::InvalidInput)
));
}
}
#[cfg(test)]
mod scaling_tests {
use super::*;
use num_complex::Complex64;
pub(super) type F<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
pub(super) fn fns<T: BesselFloat>() -> [(&'static str, F<T>, F<T>); 6] {
[
("J", besselj, besselj_scaled),
("Y", bessely, bessely_scaled),
("I", besseli, besseli_scaled),
("K", besselk, besselk_scaled),
("H1", hankel1, hankel1_scaled),
("H2", hankel2, hankel2_scaled),
]
}
fn log_factor(name: &str, z: Complex64) -> Complex64 {
let i = Complex64::new(0.0, 1.0);
match name {
"J" | "Y" => Complex64::new(-z.im.abs(), 0.0),
"I" => Complex64::new(-z.re.abs(), 0.0),
"K" => z,
"H1" => -i * z,
_ => i * z,
}
}
fn check<T: BesselFloat>(nus: &[f64], radii: &[f64], floor: f64, tol: f64) {
let to64 = |v: Complex<T>| Complex64::new(v.re.to_f64().unwrap(), v.im.to_f64().unwrap());
let mut compared = 0;
for (name, unscaled, scaled) in fns::<T>() {
for &nu in nus {
for &r in radii {
for k in 0..12 {
let th =
-core::f64::consts::PI + core::f64::consts::FRAC_PI_6 * f64::from(k);
let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
let nu_t = T::from_f64(nu);
let (Ok(u), Ok(s)) = (unscaled(nu_t, z), scaled(nu_t, z)) else {
continue;
};
let (u, s) = (to64(u), to64(s));
let ok = |v: Complex64| v.norm().is_finite() && v.norm() > floor;
if !ok(u) || !ok(s) {
continue;
}
let expected = (u.ln() + log_factor(name, to64(z))).exp();
let err = (s - expected).norm() / expected.norm();
assert!(
err < tol,
"{name}: nu={nu}, z={z:?}: scaled={s:?}, exp(c)*unscaled={expected:?}, \
rel_err={err:e}"
);
compared += 1;
}
}
}
}
assert!(compared > 0);
}
#[test]
fn scaled_matches_unscaled_f64() {
check::<f64>(
&[0.0, 0.3, 2.5, 45.7, 250.0, 575.0, 800.0, 1200.0],
&[0.5, 5.0, 40.0, 200.0, 360.0, 450.0, 600.0],
1e-290,
1e-10,
);
}
#[test]
fn scaled_matches_unscaled_f32() {
check::<f32>(
&[0.0, 0.3, 2.5, 25.7, 65.0, 90.0, 130.0],
&[0.5, 5.0, 20.0, 41.0, 50.0, 60.0, 75.0],
1e-30,
1e-3,
);
}
}
#[cfg(test)]
mod special_value_tests {
use super::scaling_tests::fns;
use super::*;
type AiryFn<T> = fn(Complex<T>) -> Result<Complex<T>, Error>;
type AiryRawFn<T> = fn(Complex<T>, Scaling) -> Result<AiryResult<T>, Error>;
fn check_signed_zero<T: BesselFloat>() {
let airy_fns: [(&str, AiryFn<T>); 8] = [
("Ai", airy),
("Ai'", airyprime),
("Bi", biry),
("Bi'", biryprime),
("Ai scaled", airy_scaled),
("Ai' scaled", airyprime_scaled),
("Bi scaled", biry_scaled),
("Bi' scaled", biryprime_scaled),
];
let raw_fns: [(&str, AiryRawFn<T>); 4] = [
("Ai raw", airy_raw),
("Ai' raw", airyprime_raw),
("Bi raw", biry_raw),
("Bi' raw", biryprime_raw),
];
let zero = T::zero();
for x in [0.5, 2.0, 5.0, 50.0, 105.0] {
let x = T::from_f64(x);
for (plus, minus) in [
(Complex::new(-x, zero), Complex::new(-x, -zero)),
(Complex::new(x, zero), Complex::new(x, -zero)),
(Complex::new(zero, x), Complex::new(-zero, x)),
(Complex::new(zero, -x), Complex::new(-zero, -x)),
] {
for (name, f) in airy_fns {
assert_eq!(f(plus), f(minus), "{name}: {plus:?} vs {minus:?}");
}
for (name, f) in raw_fns {
for scaling in [Scaling::Unscaled, Scaling::Exponential] {
assert_eq!(
f(plus, scaling),
f(minus, scaling),
"{name} {scaling:?}: {plus:?} vs {minus:?}"
);
}
}
for (name, unscaled, scaled) in fns::<T>() {
for nu in [0.0, 0.3, 1.0, 2.5, -0.3, -2.5, 90.5] {
let nu = T::from_f64(nu);
for f in [unscaled, scaled] {
assert_eq!(
f(nu, plus),
f(nu, minus),
"{name}: nu={nu:?}, {plus:?} vs {minus:?}"
);
}
}
}
}
}
}
#[test]
fn signed_zero_matches_positive_zero_f64() {
check_signed_zero::<f64>();
}
#[test]
fn signed_zero_matches_positive_zero_f32() {
check_signed_zero::<f32>();
}
fn check_negative_half_integer<T: BesselFloat>(orders: &[f64]) {
let zero = T::zero();
let zs = [
Complex::new(T::one(), zero),
Complex::new(T::from_f64(100.0), zero),
Complex::from_polar(T::from_f64(0.3), T::one()),
Complex::from_polar(T::from_f64(5.0), T::from_f64(2.0)),
Complex::new(zero, zero),
];
let pairs: [(&str, scaling_tests::F<T>, scaling_tests::F<T>); 2] = [
("unscaled", besselj, bessely),
("scaled", besselj_scaled, bessely_scaled),
];
for &a in orders {
let a = T::from_f64(a);
let s = utils::sinpi(a);
for (name, j, y) in pairs {
for z in zs {
assert_eq!(
y(-a, z),
j(a, z).map(|v| v * s),
"Y {name}: nu={:?}, z={z:?}",
-a
);
assert_eq!(
j(-a, z),
y(a, z).map(|v| -(v * s)),
"J {name}: nu={:?}, z={z:?}",
-a
);
}
}
}
}
#[test]
fn negative_half_integer_needs_one_function_f64() {
check_negative_half_integer::<f64>(&[0.5, 2.5, 10.5, 150.5, 200.5, 519.5]);
}
#[test]
fn negative_half_integer_needs_one_function_f32() {
check_negative_half_integer::<f32>(&[0.5, 2.5, 10.5, 30.5, 40.5, 150.5]);
}
#[test]
fn uniform_expansion_beyond_airy_range_f64() {
use core::f64::consts::{FRAC_PI_2, PI};
let czero = Complex::new(0.0, 0.0);
let check =
|what: &str, nu: f64, z: Complex<f64>, w: Complex<f64>, expected: Complex<f64>| {
let err = (w - expected).norm() / expected.norm();
assert!(err < 1e-5, "{what}: nu={nu}, z={z:?}, rel_err={err:e}");
};
let args = [
0.0,
0.3,
FRAC_PI_2 - 0.3,
FRAC_PI_2,
FRAC_PI_2 + 0.3,
PI - 0.3,
PI,
-0.3,
-FRAC_PI_2,
-FRAC_PI_2 + 0.3,
];
for nu in [86.0, 90.5, 1000.0, 1e5] {
for r in [7.5e8, 9e8, 1.05e9] {
for th in args {
let z = Complex::from_polar(r, th);
let h1 = |n: f64| hankel1_scaled(n, z).unwrap();
let h2 = |n: f64| hankel2_scaled(n, z).unwrap();
let expected = Complex::new(0.0, 4.0 / PI) / z;
let w = h1(nu) * h2(nu + 1.0) - h1(nu + 1.0) * h2(nu);
check("H single", nu, z, w, expected);
let (mut s1, mut s2) = ([czero; 2], [czero; 2]);
hankel1_seq_into(nu, z, Scaling::Exponential, &mut s1).unwrap();
hankel2_seq_into(nu, z, Scaling::Exponential, &mut s2).unwrap();
check("H seq", nu, z, s1[0] * s2[1] - s1[1] * s2[0], expected);
if z.re >= 0.0 {
let i = |n: f64| besseli_scaled(n, z).unwrap();
let k = |n: f64| besselk_scaled(n, z).unwrap();
let w = i(nu) * k(nu + 1.0) + i(nu + 1.0) * k(nu);
let expected = Complex::from_polar(1.0, z.im) / z;
check("I K", nu, z, w, expected);
}
}
for x in [r, -r] {
let z = Complex::new(x, 0.0);
let j = |n: f64| besselj(n, z).unwrap();
let y = |n: f64| bessely(n, z).unwrap();
let w = j(nu) * y(nu + 1.0) - j(nu + 1.0) * y(nu);
check("J Y", nu, z, w, Complex::new(-2.0 / PI, 0.0) / z);
}
}
}
}
fn check_scaled_continuation<T: BesselFloat>(radii: &[f64], tol: f64) {
use core::f64::consts::PI;
let to64 = |z: Complex<T>| Complex::new(z.re.to_f64().unwrap(), z.im.to_f64().unwrap());
let rt = |z: Complex<f64>| Complex::new((2.0 / PI).sqrt(), 0.0) / z.sqrt();
let k_half = |n: usize, z: Complex<f64>| {
let (mut sum, mut term, mut c) = (Complex::new(0.0, 0.0), Complex::new(1.0, 0.0), 1.0);
for k in 0..=n {
sum += term * c;
c *= ((n + k + 1) * (n - k)) as f64 / (k + 1) as f64;
term /= z * 2.0;
}
rt(z) * (PI / 2.0) * sum
};
let check =
|what: &str, z: Complex<T>, v: Complex<T>, expected: Complex<f64>, scale: f64| {
let err = (to64(v) - expected).norm() / scale;
assert!(
err < tol,
"{what}: z={z:?}, value={v:?}, expected={expected:?}, err={err:e}"
);
};
let i = Complex::new(0.0, 1.0);
let half = T::from_f64(0.5);
let three_halves = T::from_f64(1.5);
for &r in radii {
for d in [1e-3, 1.0, 5.0] {
for z in [Complex::new(-d, r), Complex::new(-d, -r)] {
let z = Complex::new(T::from_f64(z.re), T::from_f64(z.im));
let z64 = to64(z);
let k0 = k_half(0, z64);
check("K 1/2", z, besselk_scaled(half, z).unwrap(), k0, k0.norm());
let k1 = k_half(1, z64);
check(
"K 3/2",
z,
besselk_scaled(three_halves, z).unwrap(),
k1,
k1.norm(),
);
let mut seq = [Complex::new(T::zero(), T::zero()); 5];
besselk_seq_into(half, z, Scaling::Exponential, &mut seq).unwrap();
for (n, &v) in seq.iter().enumerate() {
let k = k_half(n, z64);
check("K seq", z, v, k, k.norm());
}
let x = z64.re.abs();
let cosh = (Complex::from_polar((z64.re - x).exp(), z64.im)
+ Complex::from_polar((-z64.re - x).exp(), -z64.im))
/ 2.0;
let ri = rt(z64);
check(
"I -1/2",
z,
besseli_scaled(-half, z).unwrap(),
ri * cosh,
ri.norm(),
);
}
for (z, h2) in [
(Complex::new(r, -d), false),
(Complex::new(-r, -d), false),
(Complex::new(r, d), true),
(Complex::new(-r, d), true),
(Complex::new(-r, 0.0), true),
] {
let z = Complex::new(T::from_f64(z.re), T::from_f64(z.im));
let z64 = to64(z);
let s = rt(z64);
let (f, e0, e1): (scaling_tests::F<T>, _, _) = if h2 {
(hankel2_scaled, i * s, -s * (1.0 - i / z64))
} else {
(hankel1_scaled, -i * s, -s * (1.0 + i / z64))
};
let name = if h2 { "H2" } else { "H1" };
check(name, z, f(half, z).unwrap(), e0, e0.norm());
check(name, z, f(three_halves, z).unwrap(), e1, e1.norm());
}
}
}
}
#[test]
fn scaled_continuation_phase_f64() {
check_scaled_continuation::<f64>(&[1e2, 1e4, 1e6, 1e8, 1e9], 1e-13);
}
#[test]
fn scaled_continuation_phase_f32() {
check_scaled_continuation::<f32>(&[1e2, 1e3, 1e4, 1e6, 4e6], 2e-6);
}
#[test]
fn bessely_negative_order_near_half_integer() {
let cases = [
(
-1.500000001,
Complex::new(1e-5, 0.0),
Complex::new(-0.07926656189549441, 0.0),
),
(
-20.499999999999,
Complex::new(-3.0, 0.5),
Complex::new(22.825467418360233, -109.6379263279235),
),
];
for (nu, z, expected) in cases {
let got = bessely(nu, z).unwrap();
let err = (got - expected).norm() / expected.norm();
assert!(err < 5e-14, "Y_{nu}({z}) = {got}, rel_err = {err:e}");
}
}
#[test]
fn uniform_expansion_beyond_airy_range_f32() {
let polar = Complex::from_polar;
for r in [3e6_f32, 4e6] {
let points = [
Complex::new(r, 0.0),
Complex::new(-r, 0.0),
Complex::new(0.0, r),
Complex::new(0.0, -r),
polar(r, 0.3),
polar(r, 1.27),
polar(r, 1.87),
polar(r, 2.84),
polar(r, -0.3),
polar(r, -1.27),
polar(r, -1.87),
polar(r, -2.84),
];
for (nu, z) in [35.5_f32, 40.5, 100.5]
.into_iter()
.flat_map(|nu| points.map(|z| (nu, z)))
{
let s = (z.norm() * core::f32::consts::FRAC_PI_2).sqrt();
for (name, v) in [
("H1", hankel1_scaled(nu, z).unwrap() * s),
("H2", hankel2_scaled(nu, z).unwrap() * s),
(
"K",
besselk_scaled(nu, z).unwrap() * (s / core::f32::consts::FRAC_PI_2),
),
] {
if name == "H2" && z.im == 0.0 && z.re < 0.0 {
continue;
}
let m = v.norm();
assert!(
(0.5..2.0).contains(&m),
"{name}: nu={nu}, z={z:?}, ratio={m}"
);
}
}
}
}
}
#[cfg(test)]
mod axis_tests {
use super::*;
use crate::algo::gamln::gamln;
use crate::utils::{cospi, sinpi};
const PI: f64 = core::f64::consts::PI;
fn lgamma_sign(t: f64) -> (f64, f64) {
if t > 0.0 {
(1.0, gamln(t).unwrap())
} else {
let s = sinpi(t);
(s.signum(), PI.ln() - s.abs().ln() - gamln(1.0 - t).unwrap())
}
}
fn series(nu: f64, x: f64, alternating: bool, shift: f64) -> f64 {
let lx = (x / 2.0).ln();
let mut sum = 0.0;
for k in 0..5000usize {
let kf = k as f64;
let (sg, lg) = lgamma_sign(kf + nu + 1.0);
let lkf = gamln(kf + 1.0).unwrap();
let mag = ((2.0 * kf + nu) * lx - lkf - lg - shift).exp();
let term = if alternating && k % 2 == 1 {
-sg * mag
} else {
sg * mag
};
sum += term;
if kf > x && term.abs() <= 1e-18 * sum.abs() {
return sum;
}
}
panic!("series did not converge: nu={nu}, x={x}");
}
fn near_integer(m: f64) -> bool {
(m - m.round()).abs() < 1e-3
}
fn half_integer(m: f64) -> bool {
(m - 0.5).fract() == 0.0
}
fn y_oracle(m: f64, x: f64) -> Option<f64> {
if half_integer(m) {
let n = (m - 0.5) as i64;
let sign = if n % 2 == 0 { -1.0 } else { 1.0 };
Some(sign * series(-m, x, true, 0.0))
} else if near_integer(m) || x >= m {
None
} else {
Some((series(m, x, true, 0.0) * cospi(m) - series(-m, x, true, 0.0)) / sinpi(m))
}
}
fn k_scaled_oracle(m: f64, x: f64) -> Option<f64> {
if half_integer(m) {
let n = (m - 0.5) as i64;
let mut term = 1.0;
let mut sum = 1.0;
for k in 1..=n {
let kf = k as f64;
term *= ((n + k) as f64) * ((n - k + 1) as f64) / (kf * 2.0 * x);
sum += term;
}
Some((PI / (2.0 * x)).sqrt() * sum)
} else if x >= 1.0 && m <= 31.0 {
let h = (0.6 / x.sqrt()).min(0.1);
let f = |t: f64| (-2.0 * x * (t / 2.0).sinh().powi(2)).exp() * (m * t).cosh();
let mut sum = f(0.0) / 2.0;
for k in 1..100_000 {
let v = f(k as f64 * h);
sum += v;
if v <= 1e-18 * sum {
break;
}
}
Some(sum * h)
} else if !near_integer(m) && x < m.max(1.0) / 4.0 {
let d = series(-m, x, false, -x) - series(m, x, false, -x);
Some(PI / 2.0 * d / sinpi(m))
} else {
None
}
}
#[allow(clippy::type_complexity)]
fn coeffs(row: &str, nu: f64) -> ((f64, f64), (f64, f64)) {
let m = nu.abs();
let (c, s) = (cospi(m), sinpi(m));
let (c2, s2) = (cospi(2.0 * m), sinpi(2.0 * m));
let (ch, sh) = (cospi(m / 2.0), sinpi(m / 2.0));
let (c3, s3) = (cospi(1.5 * m), sinpi(1.5 * m));
let neg = nu < 0.0;
let h = PI / 2.0;
let t = 2.0 / PI;
match (row, neg) {
("H1+x", false) => ((1.0, 0.0), (0.0, 1.0)),
("H1+x", true) => ((c, -s), (s, c)),
("H2+x", false) => ((1.0, 0.0), (0.0, -1.0)),
("H2+x", true) => ((c, -s), (-s, -c)),
("H1-x", false) => ((-c, s), (s, c)),
("H1-x", true) => ((-1.0, 0.0), (0.0, 1.0)),
("H2-x", false) => ((3.0 * c, -s), (s, -c)),
("H2-x", true) => ((c2 + 2.0, -s2), (-s2, -c2)),
("Y-x", false) => ((0.0, c), (2.0 * c, -s)),
("Y-x", true) => ((s2 / 2.0, c * c), (c * c + 1.0, -s2 / 2.0)),
("K+ix", _) => ((-h * sh, -h * ch), (-h * ch, h * sh)),
("K-ix", _) => ((-h * sh, -h * ch), (h * ch, -h * sh)),
("H2+ix", false) => ((2.0 * ch, t * sh), (2.0 * sh, t * ch)),
("H2+ix", true) => ((2.0 * ch, t * s3), (-2.0 * sh, t * c3)),
("H1-ix", false) => ((2.0 * ch, t * sh), (-2.0 * sh, -t * ch)),
("H1-ix", true) => ((2.0 * ch, t * s3), (2.0 * sh, -t * c3)),
_ => unreachable!(),
}
}
type F<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
type RowCase<T> = (&'static str, F<T>, fn(T) -> Complex<T>, bool, bool);
fn rows<T: BesselFloat>() -> [RowCase<T>; 13] {
[
(
"H1+x",
hankel1,
|x| Complex::new(x, T::zero()),
false,
false,
),
(
"H2+x",
hankel2,
|x| Complex::new(x, T::zero()),
false,
false,
),
(
"H1-x",
hankel1,
|x| Complex::new(-x, T::zero()),
false,
false,
),
(
"H2-x",
hankel2,
|x| Complex::new(-x, T::zero()),
false,
false,
),
(
"Y-x",
bessely,
|x| Complex::new(-x, T::zero()),
false,
false,
),
(
"Y-x",
bessely_scaled,
|x| Complex::new(-x, T::zero()),
false,
false,
),
(
"K+ix",
besselk,
|x| Complex::new(T::zero(), x),
false,
false,
),
(
"K-ix",
besselk,
|x| Complex::new(T::zero(), -x),
false,
false,
),
(
"H2+ix",
hankel2,
|x| Complex::new(T::zero(), x),
false,
true,
),
(
"H2+ix",
hankel2_scaled,
|x| Complex::new(T::zero(), x),
true,
true,
),
(
"H1-ix",
hankel1,
|x| Complex::new(T::zero(), -x),
false,
true,
),
(
"H1-ix",
hankel1_scaled,
|x| Complex::new(T::zero(), -x),
true,
true,
),
(
"K+ix",
besselk,
|x| Complex::new(-T::zero(), x),
false,
false,
),
]
}
fn check_axis<T: BesselFloat>(
nus: &[f64],
jy_x: &[f64],
ik_x: &[f64],
tol: f64,
tiny: f64,
) -> usize {
let mut checked = 0;
for (row, f, zf, scaled, ik) in rows::<T>() {
for &nu_d in nus {
let nu = T::from_f64(nu_d);
let nu_d = nu.to_f64().unwrap();
let m = nu_d.abs();
let xs: &[f64] = if ik { ik_x } else { jy_x };
for &xr in xs {
let x_d = if !ik && xr < 1.0 { xr * m } else { xr };
let x = T::from_f64(x_d);
let x_d = x.to_f64().unwrap();
if x_d == 0.0 || (!ik && x_d > (m / 2.0).min((m + 1.0).sqrt())) {
continue;
}
let Ok(v) = f(nu, zf(x)) else { continue };
let (a, b) = if ik {
let shift = if scaled { x_d } else { 0.0 };
let Some(k) = k_scaled_oracle(m, x_d) else {
continue;
};
(series(m, x_d, false, shift), k * (-x_d - shift).exp())
} else {
(
series(m, x_d, true, 0.0),
y_oracle(m, x_d).unwrap_or(f64::NAN),
)
};
let ((ar, br), (ai, bi)) = coeffs(row, nu_d);
for (got, ca, cb) in [(v.re, ar, br), (v.im, ai, bi)] {
let (ta, tb) = (ca * a, if cb == 0.0 { 0.0 } else { cb * b });
let expected = ta + tb;
if !expected.is_finite()
|| expected.abs() < tiny
|| expected.abs() > 1e300
|| expected.abs() < 1e-3 * (ta.abs() + tb.abs())
{
continue;
}
let got = got.to_f64().unwrap();
let err = ((got - expected) / expected).abs();
assert!(
err < tol,
"{row}: nu={nu_d}, x={x_d}: got {got:e}, expected {expected:e}, rel {err:e}"
);
checked += 1;
}
}
}
}
checked
}
const NUS: [f64; 41] = [
0.5, -0.5, 1.5, -1.5, 2.5, -2.5, 5.5, -5.5, 10.5, -10.5, 30.5, -30.5, 90.5, 1.0, -1.0, 2.0,
-2.0, 10.0, -10.0, 0.3, -0.3, 3.3, -3.3, 10.7, -10.7, 2.000001, -1.999999, 2.500001,
-2.499999, 1.000001, -0.999999, 20.0, 85.5, 2.02, -2.02, 3.97, -3.97, 10.01, -10.01, 1.49,
-2.51,
];
#[test]
fn axis_components_f64() {
let jy_x = [1e-10, 1e-5, 1e-2, 0.1, 0.3, 0.5];
let ik_x = [1e-5, 1e-2, 0.3, 1.0, 5.0, 20.0, 60.0, 200.0, 690.0];
let n = check_axis::<f64>(&NUS, &jy_x, &ik_x, 1e-11, 1e-280);
assert!(n > 2000, "only {n} components checked");
}
#[test]
fn axis_components_f32() {
let nus = [0.5, -0.5, 2.5, -2.5, 5.5, 10.5, 1.0, -2.0, 3.3, -3.3];
let jy_x = [1e-3, 0.1, 0.4];
let ik_x = [1e-2, 1.0, 10.0, 40.0];
let n = check_axis::<f32>(&nus, &jy_x, &ik_x, 1e-4, 1e-28);
assert!(n > 200, "only {n} components checked");
}
#[test]
fn axis_ik_ratio_between_1e3_and_1e2() {
let h = hankel2(9.999_999_99f64, Complex::new(0.0, 8.199_999_991_8)).unwrap();
let err = ((h.im + 0.015700603715088221) / 0.015700603715088221).abs();
assert!(err < 1e-14, "Im H2_9.99999999(8.2i): error {err:e}");
}
fn check_y_neg_axis<T: BesselFloat + core::fmt::Display>(
nus: &[f64],
deltas: &[f64],
fracs: &[f64],
tol: f64,
) -> usize {
let mut checked = 0;
for &nu0 in nus {
for &d in deltas {
for sign in [1.0, -1.0] {
let nu = T::from_f64(sign * (nu0 + d));
let m = nu.abs().to_f64().unwrap();
for &frac in fracs {
let x = T::from_f64(m * frac + 0.3);
let (Ok(v), Ok(y)) = (
bessely(nu, Complex::new(-x, T::zero())),
bessely(nu, Complex::new(x, T::zero())),
) else {
continue;
};
let j = besselj(nu, Complex::new(x, T::zero())).unwrap().re;
let (y, j) = (y.re.to_f64().unwrap(), j.to_f64().unwrap());
if y.abs() < 0.1 * j.abs() {
continue;
}
let expected = cospi(nu).to_f64().unwrap() * y;
let err = ((v.re.to_f64().unwrap() - expected) / expected).abs();
assert!(
err < tol,
"nu={nu}, x={x}: Re Y(-x) = {}, expected {expected:e}",
v.re
);
checked += 1;
}
}
}
}
checked
}
#[test]
fn axis_y_neg_beyond_order_f64() {
let nus = [0.5, 1.5, 4.5, 10.5, 30.5, 70.5];
let deltas = [1e-13, 1e-10, -1e-7, 1e-5, -3e-4, 1e-3];
let fracs = [1.0, 1.1, 1.5, 3.0, 8.0];
let n = check_y_neg_axis::<f64>(&nus, &deltas, &fracs, 1e-11);
assert!(n > 250, "only {n} values checked");
}
#[test]
fn axis_y_neg_beyond_order_f32() {
let nus = [0.5, 1.5, 4.5, 10.5];
let deltas = [3e-6, -3e-5, 3e-4];
let fracs = [1.0, 1.5, 3.0];
let n = check_y_neg_axis::<f32>(&nus, &deltas, &fracs, 1e-4);
assert!(n > 40, "only {n} values checked");
}
fn check_y_neg_seq_orders<T: BesselFloat + core::fmt::Display>(
nus: &[f64],
xs: &[f64],
tol: f64,
) -> usize {
const N: usize = 30;
let czero = Complex::new(T::zero(), T::zero());
let (mut neg, mut pos, mut jpos) = ([czero; N], [czero; N], [czero; N]);
let mut inexact = 0;
for &nu in nus {
let nu = T::from_f64(nu);
let nc = if nu < T::zero() {
nu.abs().ceil().to_usize().unwrap()
} else {
0
};
let base = nu + T::from_f64(nc as f64);
for &x in xs {
let x = T::from_f64(x);
let (zn, zp) = (Complex::new(-x, T::zero()), Complex::new(x, T::zero()));
bessely_seq_into(nu, zn, Scaling::Unscaled, &mut neg).unwrap();
bessely_seq_into(nu, zp, Scaling::Unscaled, &mut pos).unwrap();
besselj_seq_into(nu, zp, Scaling::Unscaled, &mut jpos).unwrap();
for j in nc..N {
let p = j - nc;
let (y, jv) = (pos[j].re, jpos[j].re);
if y.abs() < T::from_f64(0.1) * jv.abs() {
continue;
}
let sign = if p % 2 == 0 { T::one() } else { -T::one() };
let expected = sign * cospi(base) * y;
let err = ((neg[j].re - expected) / expected).abs().to_f64().unwrap();
assert!(
err < tol,
"nu={nu}, x={x}, j={j}: Re Y(-x) = {}, expected {expected}, rel {err:e}",
neg[j].re
);
let pt = T::from_f64(p as f64);
let s = base + pt;
let bb = s - base;
if (base - (s - bb)) + (pt - bb) != T::zero() {
inexact += 1;
}
}
}
}
inexact
}
#[test]
fn axis_y_neg_seq_orders_off_the_float_grid() {
let nus = [0.4999999, 0.50000031, 2.4999997, -2.4999999, -0.50000013];
let n = check_y_neg_seq_orders::<f64>(&nus, &[0.3, 6.0, 15.0, 40.0], 1e-12);
assert!(n > 200, "only {n} inexact orders checked");
let nus = [0.4999, 0.50031, 2.4997, -2.4999, -0.50013];
let n = check_y_neg_seq_orders::<f32>(&nus, &[2.0, 6.0, 15.0], 1e-4);
assert!(n > 150, "only {n} inexact orders checked");
}
fn check_k_imag_seq_orders<T: BesselFloat + core::fmt::Display>(
nus: &[f64],
xs: &[f64],
tol: f64,
) -> usize {
const N: usize = 30;
let czero = Complex::new(T::zero(), T::zero());
let (mut k, mut ypos, mut jpos) = ([czero; N], [czero; N], [czero; N]);
let hpi = T::from_f64(PI / 2.0);
let mut checked = 0;
for &nu in nus {
let nu = T::from_f64(nu);
let nc = if nu < T::zero() {
nu.abs().ceil().to_usize().unwrap()
} else {
0
};
let base = nu + T::from_f64(nc as f64);
let half = base / T::from_f64(2.0);
let (s0, c0) = (sinpi(half), cospi(half));
for &x in xs {
let x = T::from_f64(x);
let zp = Complex::new(x, T::zero());
besselk_seq_into(nu, Complex::new(T::zero(), x), Scaling::Unscaled, &mut k)
.unwrap();
bessely_seq_into(nu, zp, Scaling::Unscaled, &mut ypos).unwrap();
besselj_seq_into(nu, zp, Scaling::Unscaled, &mut jpos).unwrap();
for j in nc..N {
let p = j - nc;
let m = base + T::from_f64(p as f64);
if x >= m {
continue;
}
let (s, c) = match p % 4 {
0 => (s0, c0),
1 => (c0, -s0),
2 => (-s0, -c0),
_ => (-c0, s0),
};
let (jv, y) = (jpos[j].re, ypos[j].re);
let terms = [(s * jv, c * y), (c * jv, -(s * y))];
let got = [k[j].re, k[j].im];
let (i, (ta, tb)) = if got[0].abs() < got[1].abs() {
(0, terms[0])
} else {
(1, terms[1])
};
let expected = -hpi * (ta + tb);
if got[i].abs() >= T::from_f64(0.1) * got[1 - i].abs()
|| (ta + tb).abs() < T::from_f64(0.1) * (ta.abs() + tb.abs())
{
continue;
}
let err = ((got[i] - expected) / expected).abs().to_f64().unwrap();
assert!(
err < tol,
"nu={nu}, x={x}, j={j}: K(ix) = {}, expected {expected} in part {i}, rel {err:e}",
k[j]
);
checked += 1;
}
}
}
checked
}
#[test]
fn axis_k_imag_seq_orders_off_the_float_grid() {
let nus = [1.9999997, 3.00000031, 0.9999997, -2.0000001, -1.00000013];
let n = check_k_imag_seq_orders::<f64>(&nus, &[0.3, 2.0, 6.0], 1e-12);
assert!(n > 150, "only {n} components checked");
let nus = [1.9997, 3.00031, 0.9997, -2.0001, -1.00013];
let n = check_k_imag_seq_orders::<f32>(&nus, &[2.0, 6.0], 1e-4);
assert!(n > 100, "only {n} components checked");
}
fn neg_axis_selected(kind: char, nu: f64, k: usize) -> bool {
let m = nu.abs();
let (c, s) = (cospi(m), sinpi(m));
let (c2, s2) = (cospi(2.0 * m), sinpi(2.0 * m));
let (a, b) = match (kind, nu < 0.0, k) {
('Y', false, 0) => (0.0, c),
('Y', false, _) => (2.0 * c, -s),
('Y', true, 0) => (s2 / 2.0, c * c),
('Y', true, _) => (c * c + 1.0, -s2 / 2.0),
('2', false, 0) => (3.0 * c, -s),
('2', false, _) => (s, -c),
('2', true, 0) => (c2 + 2.0, -s2),
('2', true, _) => (-s2, -c2),
_ => return false,
};
(m < 18.0 && a.abs() < 1e-3 * b.abs()) || (kind == 'Y' && k == 0 && a.abs() + b.abs() < 0.1)
}
#[test]
fn axis_neg_next_to_zeros() {
type Case = (char, f64, f64, usize, f64, f64);
fn check<T: BesselFloat + core::fmt::Display>(cases: &[Case]) {
let czero = Complex::new(T::zero(), T::zero());
let mut buf = [czero; 45];
for &(kind, nu, x, k, want, scale) in cases {
let z = Complex::new(T::from_f64(-x), T::zero());
let tnu = T::from_f64(nu);
let exact = |j: usize| {
let (jt, nu0) = (T::from_f64(j as f64), tnu - T::from_f64(j as f64));
if nu < 0.0 {
nu0.abs() - jt == tnu.abs()
} else {
nu0 + jt == tnu
}
};
let j = (0..=20).rev().find(|&j| exact(j)).unwrap();
let nu0 = tnu - T::from_f64(j as f64);
let (singles, seq) = if kind == 'Y' {
let seq = bessely_seq_into(nu0, z, Scaling::Unscaled, &mut buf);
let s = [bessely(tnu, z).unwrap(), bessely_scaled(tnu, z).unwrap()];
(s, seq)
} else {
let seq = hankel2_seq_into(nu0, z, Scaling::Unscaled, &mut buf);
let v = hankel2(tnu, z).unwrap();
([v, v], seq)
};
seq.unwrap();
for v in singles.into_iter().chain([buf[j]]) {
let got = if k == 0 { v.re } else { v.im };
let units = (got.to_f64().unwrap() - want).abs()
/ (T::epsilon().to_f64().unwrap() * scale);
assert!(
units < 16.0,
"{kind} nu={nu}, x={x}, k={k}: {v}, {units} units"
);
}
}
}
check::<f64>(&[
(
'Y',
0.5404395744314369,
14.199204647196062,
0,
1.7790766124893162e-07,
0.02683,
),
(
'Y',
5.4579621742598405,
17.955980580074737,
0,
6.434329040379513e-07,
0.02539,
),
(
'Y',
8.553187617888364,
14.839639619602085,
0,
6.376630874085129e-06,
0.03803,
),
(
'Y',
10.466204201871951,
20.809512383347556,
0,
-0.0003560905884186947,
0.01992,
),
(
'Y',
12.541375369126225,
19.383109940166836,
0,
-0.0010992113067365456,
0.02685,
),
(
'Y',
15.457226611852212,
17.934577879174395,
0,
-0.0009456667810636375,
0.03461,
),
(
'Y',
-1.0000000505613758,
18.058798586324336,
0,
-0.002889004423008248,
0.1879,
),
(
'Y',
-9.0,
15.361301343580108,
0,
-7.680400842858634e-13,
0.2257,
),
(
'Y',
-1e-08,
13.361097489864841,
0,
6.9195873309076905e-09,
0.2182,
),
(
'Y',
-14.0,
84.44679697015918,
0,
-1.9655202108533837e-08,
0.08743,
),
(
'Y',
-16.0002,
100.03440128478886,
0,
-5.04102359239503e-05,
0.08034,
),
(
'Y',
16.5001,
100.74840631508634,
1,
-1.572667517633897e-11,
0.08008,
),
(
'Y',
16.50001,
100.74885123627298,
1,
3.4050198315155154e-12,
0.08004,
),
(
'2',
2.0,
13.209986713986947,
1,
8.25474504044067e-10,
0.2207,
),
(
'2',
14.0,
84.44679697015918,
1,
1.9655202108533837e-08,
0.08743,
),
(
'2',
17.0,
56.32193676896589,
1,
1.067480680320756e-12,
0.1089,
),
(
'2',
5.5,
7.293692200815263,
0,
2.0120730332658653e-11,
0.3557,
),
(
'2',
14.5,
85.15744139214142,
0,
3.7729005022425324e-05,
0.0871,
),
(
'2',
-5.5,
21.283248938558053,
1,
1.5177917276336462e-11,
0.1759,
),
(
'2',
-17.5,
60.27562453458215,
1,
-1.0842178117582853e-11,
0.1051,
),
(
'2',
0.0001,
13.360940502860311,
1,
0.0001371041719352024,
0.2183,
),
(
'2',
-16.5001,
100.74840631508634,
1,
2.0689729735150853e-11,
0.08008,
),
(
'2',
16.5001,
100.74840631508634,
0,
-2.5143155227226787e-05,
0.08011,
),
]);
check::<f32>(&[
(
'Y',
0.42830756306648254,
7.750723838806152,
0,
0.0003261815181377399,
0.06399,
),
(
'Y',
3.5858168601989746,
8.841499328613281,
0,
3.636876710628325e-05,
0.0746,
),
(
'Y',
11.286245346069336,
38.86476516723633,
0,
0.0005308631669817854,
0.0814,
),
(
'Y',
12.348158836364746,
40.268272399902344,
0,
8.929377383150825e-08,
0.05917,
),
(
'Y',
17.424158096313477,
53.573184967041016,
0,
6.935261359731625e-07,
0.02646,
),
(
'Y',
-13.999799728393555,
39.09739303588867,
0,
0.00014275489424510885,
0.1321,
),
(
'Y',
-17.0,
49.711029052734375,
0,
0.0025988763574300126,
0.1167,
),
(
'Y',
14.500100135803223,
39.73927688598633,
1,
-1.494391009619099e-06,
0.1312,
),
(
'2',
17.0,
49.711029052734375,
1,
0.0025988763574300126,
0.1167,
),
(
'2',
12.0,
39.807090759277344,
1,
-5.3511832603744025e-06,
0.1295,
),
(
'2',
16.5,
45.684940338134766,
0,
-3.289102085362203e-06,
0.1222,
),
(
'2',
-16.500099182128906,
38.856502532958984,
1,
4.2589171368920826e-05,
0.1346,
),
]);
}
#[test]
fn axis_neg_keeps_amos() {
for (kind, nu, x) in [
('Y', 41.0f64, 50.623088092303156),
('Y', 0.7876708575032857, 29.094981102136426),
('Y', 2.000000000007951, 24.27010247905739),
('Y', -7.9, 18.043947370535463),
('Y', -16.999, 59.607268325147366),
('2', -16.0002, 100.03440128478886),
('2', 16.5, 99.21561951113041),
('2', 20.0, 56.92777653004243),
('1', -17.4999, 29.133873186247307),
] {
let z = Complex::new(-x, 0.0);
let pairs = if kind == 'Y' {
[
(bessely(nu, z), bessely_value(nu, z, Scaling::Unscaled)),
(
bessely_scaled(nu, z),
bessely_value(nu, z, Scaling::Exponential),
),
]
} else {
let (f, h): (F<f64>, _) = if kind == '1' {
(hankel1, HankelKind::First)
} else {
(hankel2, HankelKind::Second)
};
let p = (f(nu, z), hankel_value(h, nu, z, Scaling::Unscaled));
[p, p]
};
for (v, r) in pairs {
let (v, r) = (v.unwrap(), r.unwrap());
assert_eq!(
(v.re.to_bits(), v.im.to_bits()),
(r.re.to_bits(), r.im.to_bits()),
"{kind} nu={nu}, x={x}"
);
}
}
}
#[test]
fn hankel_real_part_matches_besselj() {
for nu in [1.0, 5.0, 10.0, 30.5, -10.0, -3.3, 90.0, 150.5] {
for frac in [1e-6, 0.01, 0.3, 0.9] {
let z = Complex::new(frac * f64::abs(nu), 0.0);
let j = besselj(nu, z).unwrap().re;
if j == 0.0 {
continue;
}
for h in [hankel1(nu, z).unwrap(), hankel2(nu, z).unwrap()] {
let err = ((h.re - j) / j).abs();
assert!(
err < 1e-11,
"nu={nu}, x={}: Re H = {:e}, J = {j:e}",
z.re,
h.re
);
}
}
}
}
#[test]
fn bessel_jy_real_on_positive_axis() {
let mut out = [Complex::new(0.0f64, 0.0); 9];
for nu in [0.0f64, 0.3, 1.0, 5.5, 120.0, -0.3, -2.0, -7.5, -40.25] {
for x in [1e-8f64, 0.5, 5.0, 37.0, 1e3, 1e5] {
let z = Complex::new(x, 0.0);
for f in [besselj, besselj_scaled, bessely, bessely_scaled] {
if let Ok(v) = f(nu, z) {
assert_eq!(v.im.to_bits(), 0, "nu={nu}, x={x}: {v}");
}
}
for s in [Scaling::Unscaled, Scaling::Exponential] {
if besselj_seq_into(nu, z, s, &mut out).is_ok() {
assert!(
out.iter().all(|v| v.im.to_bits() == 0),
"J seq nu={nu}, x={x}"
);
}
if bessely_seq_into(nu, z, s, &mut out).is_ok() {
assert!(
out.iter().all(|v| v.im.to_bits() == 0),
"Y seq nu={nu}, x={x}"
);
}
}
}
}
}
#[test]
fn axis_fix_leaves_other_values_alone() {
type Raw = fn(f64, Complex<f64>, Scaling) -> Result<Complex<f64>, Error>;
let raw_h1: Raw = |nu, z, s| hankel_value(HankelKind::First, nu, z, s);
let raw_h2: Raw = |nu, z, s| hankel_value(HankelKind::Second, nu, z, s);
let fns: [(F<f64>, Raw, Scaling, char); 7] = [
(hankel1, raw_h1, Scaling::Unscaled, 'H'),
(hankel2, raw_h2, Scaling::Unscaled, '2'),
(hankel1_scaled, raw_h1, Scaling::Exponential, 'H'),
(hankel2_scaled, raw_h2, Scaling::Exponential, 'H'),
(besselk, besselk_value, Scaling::Unscaled, 'K'),
(bessely, bessely_value, Scaling::Unscaled, 'Y'),
(bessely_scaled, bessely_value, Scaling::Exponential, 'Y'),
];
let tiny = f64::MIN_POSITIVE;
for (f, raw, scaling, kind) in fns {
for nu in [0.0, 0.3, 2.0, 5.5, 20.0, -0.3, -2.0, -5.5, -20.0, 90.5] {
for x in [0.01, 0.7, 1.9, 6.0, 25.0, 100.0] {
let zs = [
Complex::new(x, 0.0),
Complex::new(-x, 0.0),
Complex::new(0.0, x),
Complex::new(0.0, -x),
Complex::new(x, tiny),
Complex::new(-x, -tiny),
Complex::new(tiny, x),
Complex::new(-tiny, -x),
Complex::new(x, x),
];
for (iz, z) in zs.into_iter().enumerate() {
let (Ok(v), Ok(r)) = (f(nu, z), raw(nu, z, scaling)) else {
continue;
};
let (sr, si) = (r.re.abs(), r.im.abs());
let ratio = sr.min(si) / sr.max(si);
let imag_axis = z.re == 0.0;
let ik = imag_axis && matches!(kind, 'H' | '2');
let selectable = iz < 4
&& if ik {
ratio < 1e-2
} else if x < f64::abs(nu) {
ratio < 0.1
} else {
iz == 1
&& ratio < 0.1
&& neg_axis_selected(kind, nu, usize::from(sr >= si))
};
let r = if kind == 'Y' && iz == 0 {
Complex::new(r.re, 0.0)
} else {
r
};
if !selectable {
assert!(
v.re.to_bits() == r.re.to_bits()
&& v.im.to_bits() == r.im.to_bits(),
"nu={nu}, z={z}: {v} vs Amos {r}"
);
}
}
}
}
}
}
type SeqF = fn(f64, Complex<f64>, Scaling, &mut [Complex<f64>]) -> Result<SeqInfo, Error>;
#[test]
fn axis_seq_matches_single() {
type SeqCase = (SeqF, F<f64>, Scaling, fn(f64) -> Complex<f64>, f64, char);
let cases: [SeqCase; 9] = [
(
hankel1_seq_into,
hankel1,
Scaling::Unscaled,
|x| Complex::new(x, 0.0),
0.1,
' ',
),
(
hankel2_seq_into,
hankel2,
Scaling::Unscaled,
|x| Complex::new(-x, 0.0),
0.1,
'2',
),
(
bessely_seq_into,
bessely,
Scaling::Unscaled,
|x| Complex::new(-x, 0.0),
0.1,
'Y',
),
(
bessely_seq_into,
bessely_scaled,
Scaling::Exponential,
|x| Complex::new(-x, 0.0),
0.1,
'Y',
),
(
besselk_seq_into,
besselk,
Scaling::Unscaled,
|x| Complex::new(0.0, x),
0.1,
' ',
),
(
besselk_seq_into,
besselk,
Scaling::Unscaled,
|x| Complex::new(0.0, -x),
0.1,
' ',
),
(
hankel2_seq_into,
hankel2,
Scaling::Unscaled,
|x| Complex::new(0.0, x),
1e-2,
' ',
),
(
hankel2_seq_into,
hankel2_scaled,
Scaling::Exponential,
|x| Complex::new(0.0, x),
1e-2,
' ',
),
(
hankel1_seq_into,
hankel1_scaled,
Scaling::Exponential,
|x| Complex::new(0.0, -x),
1e-2,
' ',
),
];
let mut buf = [Complex::new(0.0, 0.0); 70];
for (seq, single, scaling, zf, ratio, neg) in cases {
for nu0 in [-7.3, -6.0, -40.5, 0.5, 2.0, 30.5] {
for n in [1usize, 5, 40, 64, 70] {
for x in [1e-10, 0.01, 1.3, 7.5, 30.0] {
let z = zf(x);
let out = &mut buf[..n];
if seq(nu0, z, scaling, out).is_err() {
continue;
}
for (j, v) in out.iter().enumerate() {
let nu = nu0 + j as f64;
let Ok(s) = single(nu, z) else { continue };
let (sr, si) = (s.re.abs(), s.im.abs());
let (small, large) = (sr.min(si), sr.max(si));
let jy_ok = ratio == 1e-2
|| x < nu.abs()
|| neg_axis_selected(neg, nu, usize::from(sr >= si));
if jy_ok && small < 1e-3 * ratio * large && small > 0.0 {
let (gs, ss) = if sr < si { (v.re, s.re) } else { (v.im, s.im) };
let err = ((gs - ss) / ss).abs();
assert!(
err < 1e-12,
"nu={nu}, z={z}, n={n}: seq {v} vs single {s}"
);
} else if s.norm() > 0.0 {
let err = (v - s).norm() / s.norm();
assert!(
err < 1e-10,
"nu={nu}, z={z}, n={n}: seq {v} vs single {s}"
);
if jy_ok && small == 0.0 {
let gs = if sr < si { v.re } else { v.im };
assert!(
gs == 0.0,
"nu={nu}, z={z}, n={n}: seq {v} vs single {s}"
);
}
}
}
}
}
}
}
}
}
#[cfg(test)]
mod seq_tests {
use super::*;
use crate::utils::SEQ_WORKSPACE;
use num_complex::Complex64;
const MAX_N: usize = 128;
fn zeros<T: BesselFloat>() -> [Complex<T>; MAX_N] {
[Complex::new(T::zero(), T::zero()); MAX_N]
}
#[test]
fn seq_power_series_underflow_retry_keeps_scaling() {
fn check<T: BesselFloat>(
tops: core::ops::Range<i32>,
ns: &[usize],
z: Complex<T>,
tol: f64,
) {
for &n in ns {
let mut hits = 0;
for top in tops.clone() {
let nu = T::from_f64(f64::from(top) - (n - 1) as f64);
let mut buf = zeros::<T>();
let values = &mut buf[..n];
let info = besselj_seq_into(nu, z, Scaling::Unscaled, values).unwrap();
if info.underflow_count == 0 || values[0].norm() == T::zero() {
continue;
}
hits += 1;
for (j, &got) in values.iter().enumerate() {
if got.norm() == T::zero() {
continue;
}
let order = nu + T::from_f64(j as f64);
let single = besselj(order, z).unwrap();
let err = ((got - single).norm() / single.norm()).to_f64().unwrap();
assert!(
err < tol,
"n={n} order={:?}: {got:?} vs {single:?}",
order.to_f64()
);
}
}
assert!(hits > 0, "n={n}: no case with a partial underflow");
}
}
check(202..219, &[3, 40], Complex64::new(6.5, 0.0), 1e-10);
check(36..48, &[3, 20], Complex::new(6.5f32, 0.0), 1e-4);
}
#[test]
fn seq_neg_underflow_count_counts_all_zeros() {
fn check<T: BesselFloat>(radii: &[f64]) {
let mut hits = 0;
for nu in [-0.5, -3.0, -40.3, -100.0, -127.0] {
for &r in radii {
for deg in [0.0, 60.0, 135.0, -100.0] {
let a = f64::to_radians(deg);
let z = Complex::new(T::from_f64(r * a.cos()), T::from_f64(r * a.sin()));
for s in [Scaling::Unscaled, Scaling::Exponential] {
for (name, seq, _, _) in seq_fns::<T>() {
let mut out = zeros::<T>();
let Ok(info) = seq(T::from_f64(nu), z, s, &mut out) else {
continue;
};
let count = out.iter().filter(|v| v.norm() == T::zero()).count();
assert_eq!(
info.underflow_count, count,
"{name} ν={nu} r={r} {deg}°"
);
hits += usize::from(count > 0);
}
}
}
}
}
assert!(hits > 0, "no case with underflow");
}
check::<f64>(&[0.1, 6.5, 40.0, 700.0]);
check::<f32>(&[0.1, 6.5, 40.0, 90.0]);
let z = Complex64::new(0.1, 0.0);
let mut out = zeros::<f64>();
let info = besselj_seq_into(-0.5, z, Scaling::Unscaled, &mut out).unwrap();
assert!(out[0].norm() > 0.0 && out[MAX_N - 1].norm() == 0.0);
assert!(info.underflow_count > 0);
let z = Complex64::new(0.0, 0.0);
for nu in [-2.0, 2.0] {
for seq in [besselj_seq_into::<f64>, besseli_seq_into::<f64>] {
let mut out = [Complex64::new(0.0, 0.0); 5];
let info = seq(nu, z, Scaling::Unscaled, &mut out).unwrap();
let expected = if nu < 0.0 { 4 } else { 5 };
assert_eq!(info.underflow_count, expected, "ν={nu}");
}
}
}
#[test]
fn seq_reports_reduced_accuracy() {
fn check<T: BesselFloat>(r: f64) {
let z = Complex::new(T::from_f64(r * 0.6), T::from_f64(r * 0.8));
for nu in [-80.5, -80.3, -3.0, -2.5, 0.5] {
for n in [5, 70] {
for (name, seq, _, _) in seq_fns::<T>() {
let mut out = zeros::<T>();
let info =
seq(T::from_f64(nu), z, Scaling::Exponential, &mut out[..n]).unwrap();
assert_eq!(info.status, Accuracy::Reduced, "{name} ν={nu} n={n}");
}
}
}
}
check::<f64>(4.0e4);
check::<f32>(3.0e3);
}
#[test]
fn seq_into_empty_output_is_invalid_input() {
let z = Complex64::new(1.5, 0.5);
let s = Scaling::Unscaled;
for nu in [-1.5, -2.0, 1.5] {
for (name, seq, _, _) in seq_fns::<f64>() {
assert_eq!(seq(nu, z, s, &mut []), Err(Error::InvalidInput), "{name}");
}
}
}
type SeqIntoFn<T> = fn(T, Complex<T>, Scaling, &mut [Complex<T>]) -> Result<SeqInfo, Error>;
type SingleFn<T> = fn(T, Complex<T>) -> Result<Complex<T>, Error>;
type SeqCase<T> = (&'static str, SeqIntoFn<T>, SingleFn<T>, SingleFn<T>);
fn seq_fns<T: BesselFloat>() -> [SeqCase<T>; 6] {
[
("J", besselj_seq_into, besselj, besselj_scaled),
("Y", bessely_seq_into, bessely, bessely_scaled),
("I", besseli_seq_into, besseli, besseli_scaled),
("K", besselk_seq_into, besselk, besselk_scaled),
("H1", hankel1_seq_into, hankel1, hankel1_scaled),
("H2", hankel2_seq_into, hankel2, hankel2_scaled),
]
}
fn check_seq_grid<T: BesselFloat>(
nus: &[f64],
radii: &[f64],
args: &[f64],
n: usize,
tol: f64,
) {
let floor = T::MACH_TINY * T::from_f64(1e3);
for (name, seq, single, single_scaled) in seq_fns::<T>() {
for scaling in [Scaling::Unscaled, Scaling::Exponential] {
let single = if scaling == Scaling::Unscaled {
single
} else {
single_scaled
};
for &nu in nus {
for &r in radii {
for &th in args {
let nu_t = T::from_f64(nu);
let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
let mut buf = zeros::<T>();
let values = &mut buf[..n];
match seq(nu_t, z, scaling, values) {
Ok(_) => {}
Err(e) => {
let some_order_fails = (0..n)
.any(|j| single(nu_t + T::from_f64(j as f64), z).is_err());
assert!(
some_order_fails,
"{name} {scaling:?}: nu={nu}, z={z:?}, n={n}: seq \
failed with {e:?} but every single value succeeds"
);
continue;
}
}
for (j, &w) in values.iter().enumerate() {
let order = nu_t + T::from_f64(j as f64);
let Ok(v) = single(order, z) else {
continue;
};
if !(v.norm() > floor && v.norm().is_finite()) {
continue;
}
let err = ((w - v).norm() / v.norm()).to_f64().unwrap();
assert!(
err < tol,
"{name} {scaling:?}: nu={nu}, z={z:?}, n={n}, j={j}: \
seq={w:?}, single={v:?}, rel_err={err:e}"
);
}
}
}
}
}
}
}
const ARGS: [f64; 8] = [
0.0,
0.5,
1.2,
core::f64::consts::FRAC_PI_2,
2.2,
3.0,
-1.0,
-2.6,
];
const TOL_GRID_F64: f64 = 1e-10;
#[test]
fn seq_matches_single_grid_f64() {
for n in [1, 9] {
check_seq_grid::<f64>(
&[
-200.5, -200.0, -20.0, -12.3, -7.0, -5.3, -4.0, -3.0, -2.5, -0.0, 0.0, 0.4,
13.5, 46.2, 81.7, 121.3, 575.0,
],
&[
0.3, 3.0, 17.0, 45.0, 120.0, 330.0, 360.0, 666.0, 698.0, 705.0, 709.0,
],
&ARGS,
n,
TOL_GRID_F64,
);
}
}
#[test]
fn seq_matches_single_grid_f64_long() {
check_seq_grid::<f64>(
&[-80.3, -40.3, 0.5, 3.3],
&[2.2, 25.0, 90.0],
&ARGS,
100,
TOL_GRID_F64,
);
}
const TOL_GRID_F32: f64 = 1e-3;
#[test]
fn seq_matches_single_grid_f32() {
check_seq_grid::<f32>(
&[
-60.0, -25.5, -9.0, -4.6, -0.0, 0.0, 0.4, 7.5, 21.2, 29.9, 46.3, 70.0,
],
&[0.3, 3.0, 17.0, 45.0, 70.0, 90.0],
&ARGS,
9,
TOL_GRID_F32,
);
}
fn check_recurrence_grid<T: BesselFloat>(
nus: &[f64],
radii: &[f64],
args: &[f64],
n: usize,
tol: f64,
) {
for (name, seq, _, _) in seq_fns::<T>() {
let (s, t) = match name {
"I" => (-1.0, 1.0),
"K" => (-1.0, -1.0),
_ => (1.0, 1.0),
};
let (s, t) = (T::from_f64(s), T::from_f64(t));
for scaling in [Scaling::Unscaled, Scaling::Exponential] {
for &nu in nus {
for &r in radii {
for &th in args {
let nu_t = T::from_f64(nu);
let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
let mut buf = zeros::<T>();
let values = &mut buf[..n];
if seq(nu_t, z, scaling, values).is_err() {
continue;
}
for (j, w) in values.windows(3).enumerate() {
if w.iter()
.any(|v| v.norm() == T::zero() || !v.norm().is_finite())
{
continue;
}
let order = nu_t + T::from_f64((j + 1) as f64);
let lhs = w[0] + w[2] * s;
let rhs = w[1] * (z.inv() * (T::from_f64(2.0) * order * t));
let mag = w[0].norm().max(w[2].norm()).max(rhs.norm());
let err = ((lhs - rhs).norm() / mag).to_f64().unwrap();
assert!(
err < tol,
"{name} {scaling:?}: nu={nu}, z={z:?}, order={:?}: \
recurrence residual {err:e}",
order.to_f64()
);
}
}
}
}
}
}
}
#[test]
fn seq_recurrence_across_zero_f64() {
check_recurrence_grid::<f64>(
&[-12.3, -5.3, -4.0, -3.7, -2.5, -0.3],
&[0.3, 3.0, 17.0, 45.0],
&ARGS,
9,
1e-11, );
}
#[test]
fn seq_recurrence_across_zero_f32() {
check_recurrence_grid::<f32>(
&[-12.3, -5.3, -4.0, -3.7, -2.5, -0.3],
&[0.3, 3.0, 17.0, 45.0],
&ARGS,
9,
1e-4, );
}
type ImplFn<T> = for<'a> fn(
T,
Complex<T>,
Scaling,
&mut [Complex<T>],
Workspace<'a, T>,
) -> Result<SeqInfo, Error>;
type ImplCase<T> = (&'static str, ImplFn<T>, SingleFn<T>, SingleFn<T>);
fn impl_fns<T: BesselFloat>() -> [ImplCase<T>; 3] {
[
("J", besselj_seq_impl, besselj, besselj_scaled),
("Y", bessely_seq_impl, bessely, bessely_scaled),
("I", besseli_seq_impl, besseli, besseli_scaled),
]
}
fn same_bits<T: BesselFloat>(x: Complex<T>, y: Complex<T>) -> bool {
let bits = |v: T| v.to_f64().unwrap().to_bits();
bits(x.re) == bits(y.re) && bits(x.im) == bits(y.im)
}
fn check_chunk_invariance<T: BesselFloat>(
nus: &[f64],
radii: &[f64],
ns: &[usize],
ws_len: usize,
tol: f64,
) {
let floor = T::MACH_TINY * T::from_f64(1e3);
let czero = Complex::new(T::zero(), T::zero());
for (name, f, single, single_scaled) in impl_fns::<T>() {
for scaling in [Scaling::Unscaled, Scaling::Exponential] {
let single = if scaling == Scaling::Unscaled {
single
} else {
single_scaled
};
for &nu in nus {
for &r in radii {
for &th in &ARGS {
for &n in ns {
let nu_t = T::from_f64(nu);
let z = Complex::from_polar(T::from_f64(r), T::from_f64(th));
let (mut a, mut b) = (zeros::<T>(), zeros::<T>());
let (a, b) = (&mut a[..n], &mut b[..n]);
let mut ws_whole = [czero; 2 * MAX_N];
let mut ws_short = [czero; 2 * MAX_N];
let whole = f(nu_t, z, scaling, a, Workspace::Slice(&mut ws_whole));
let chunked = f(
nu_t,
z,
scaling,
b,
Workspace::Slice(&mut ws_short[..ws_len]),
);
let some_single_fails = || {
(0..n).any(|j| single(nu_t + T::from_f64(j as f64), z).is_err())
};
let (info_a, info_b) = match (whole, chunked) {
(Ok(ia), Ok(ib)) => (ia, ib),
(Ok(_), Err(e)) => {
assert!(
some_single_fails(),
"{name} {scaling:?} ws={ws_len}: nu={nu}, z={z:?}, \
n={n}: chunked failed with {e:?}, whole call and \
every single value succeed"
);
continue;
}
(Err(_), Ok(_)) => {
for (j, &y) in b.iter().enumerate() {
let Ok(v) = single(nu_t + T::from_f64(j as f64), z)
else {
continue;
};
if !(v.norm() > floor && v.norm().is_finite()) {
continue;
}
let err = ((y - v).norm() / v.norm()).to_f64().unwrap();
assert!(
err < tol,
"{name} {scaling:?} ws={ws_len}: nu={nu}, \
z={z:?}, n={n}, j={j}: chunked={y:?}, \
single={v:?}"
);
}
continue;
}
(Err(_), Err(_)) => continue,
};
if 2 * n <= ws_len {
assert_eq!(
info_a, info_b,
"{name} {scaling:?} ws={ws_len}: nu={nu}, n={n}"
);
for (&x, &y) in a.iter().zip(b.iter()) {
assert!(
same_bits(x, y),
"{name} {scaling:?} ws={ws_len}: nu={nu}, z={z:?}, \
n={n}: {x:?} vs {y:?}"
);
}
continue;
}
assert_eq!(
info_a.status, info_b.status,
"{name} {scaling:?} ws={ws_len}: nu={nu}, n={n}"
);
for (j, (&x, &y)) in a.iter().zip(b.iter()).enumerate() {
if !(x.norm() > floor && x.norm().is_finite()) {
continue;
}
let err = ((x - y).norm() / x.norm()).to_f64().unwrap();
assert!(
err < tol,
"{name} {scaling:?} ws={ws_len}: nu={nu}, z={z:?}, \
n={n}, j={j}: whole={x:?}, chunked={y:?}, \
rel_err={err:e}"
);
}
}
}
}
}
}
}
}
const WS_LENS: [usize; 4] = [2, 4, 14, SEQ_WORKSPACE];
#[test]
fn seq_chunk_size_invariance_f64() {
let nus = [
-80.3, -70.0, -40.5, -40.3, -40.0, -12.3, -0.7, 0.4, 3.3, 81.7,
];
let radii = [0.3, 3.0, 17.0, 45.0, 120.0, 666.0, 698.0, 705.0];
let ns = [9, 32, 33, 65, 100];
for ws_len in WS_LENS {
check_chunk_invariance::<f64>(&nus, &radii, &ns, ws_len, TOL_CHUNK_F64);
}
}
#[test]
fn seq_chunk_size_invariance_f32() {
let nus = [-70.0, -40.3, -40.0, -15.5, -4.6, 0.4, 7.5, 29.9];
let radii = [3.0, 17.0, 34.0, 45.0, 70.0, 90.0, 100.0];
let ns = [9, 32, 33, 65, 100];
for ws_len in WS_LENS {
check_chunk_invariance::<f32>(&nus, &radii, &ns, ws_len, TOL_CHUNK_F32);
}
}
const TOL_CHUNK_F64: f64 = 1e-8;
const TOL_CHUNK_F32: f64 = 2e-2;
#[test]
fn seq_into_ignores_initial_out_contents() {
let z_list = [Complex64::new(1.5, 0.5), Complex64::new(-30.0, 20.0)];
for (name, seq, _, _) in seq_fns::<f64>() {
for nu in [-40.3, -7.0, -2.5, 0.4, 3.0] {
for z in z_list {
for n in [1, 9, 40] {
let mut a = [Complex64::new(0.0, 0.0); 40];
let mut b = [Complex64::new(f64::NAN, f64::NAN); 40];
for scaling in [Scaling::Unscaled, Scaling::Exponential] {
let ra = seq(nu, z, scaling, &mut a[..n]);
let rb = seq(nu, z, scaling, &mut b[..n]);
assert_eq!(ra, rb, "{name}: nu={nu}, z={z:?}, n={n}");
if ra.is_err() {
continue;
}
for (&x, &y) in a[..n].iter().zip(b[..n].iter()) {
assert!(
same_bits(x, y),
"{name}: nu={nu}, z={z:?}, n={n}: {x:?} vs {y:?}"
);
}
}
}
}
}
}
}
}
#[cfg(all(test, feature = "std"))]
mod seq_vec_tests {
use super::*;
use num_complex::Complex64;
#[test]
fn seq_computes_each_function_in_one_call() {
type Seq = fn(f64, Complex64, usize, Scaling) -> Result<BesselResult<f64>, Error>;
type Impl = for<'a> fn(
f64,
Complex64,
Scaling,
&mut [Complex64],
Workspace<'a, f64>,
) -> Result<SeqInfo, Error>;
let cases: [(Seq, Impl, f64, Complex64); 6] = [
(
bessely_seq,
bessely_seq_impl,
0.4,
Complex64::from_polar(25.0, 0.5),
),
(
bessely_seq,
bessely_seq_impl,
-40.3,
Complex64::from_polar(17.0, -1.0),
),
(
besselj_seq,
besselj_seq_impl,
-80.3,
Complex64::from_polar(45.0, 3.0),
),
(
besseli_seq,
besseli_seq_impl,
-40.3,
Complex64::from_polar(17.0, 2.2),
),
(
besselj_seq,
besselj_seq_impl,
-80.5,
Complex64::from_polar(45.0, 3.0),
),
(
bessely_seq,
bessely_seq_impl,
-80.5,
Complex64::from_polar(17.0, -1.0),
),
];
const N: usize = 100;
for (seq, imp, nu, z) in cases {
let r = seq(nu, z, N, Scaling::Unscaled).unwrap();
let mut out = [Complex64::new(0.0, 0.0); N];
let mut ws = [Complex64::new(0.0, 0.0); 2 * N];
let info = imp(
nu,
z,
Scaling::Unscaled,
&mut out,
Workspace::Slice(&mut ws),
)
.unwrap();
assert_eq!(
(r.underflow_count, r.status),
(info.underflow_count, info.status)
);
for (j, (x, y)) in r.values.iter().zip(out.iter()).enumerate() {
assert!(
x.re.to_bits() == y.re.to_bits() && x.im.to_bits() == y.im.to_bits(),
"nu={nu}, z={z:?}, j={j}: {x:?} vs {y:?}"
);
}
}
}
#[test]
fn seq_matches_seq_into() {
type Pair = (
fn(f64, Complex64, usize, Scaling) -> Result<BesselResult<f64>, Error>,
fn(f64, Complex64, Scaling, &mut [Complex64]) -> Result<SeqInfo, Error>,
);
let pairs: [Pair; 6] = [
(besselj_seq, besselj_seq_into),
(bessely_seq, bessely_seq_into),
(besseli_seq, besseli_seq_into),
(besselk_seq, besselk_seq_into),
(hankel1_seq, hankel1_seq_into),
(hankel2_seq, hankel2_seq_into),
];
let z = Complex64::new(1.5, -0.5);
for (seq, seq_into) in pairs {
for nu in [-5.3, -2.0, 0.0, 1.7] {
for n in [0, 1, 6] {
let mut out = [Complex64::new(0.0, 0.0); 6];
let expected = seq_into(nu, z, Scaling::Unscaled, &mut out[..n]);
match seq(nu, z, n, Scaling::Unscaled) {
Ok(r) => {
let info = expected.unwrap();
assert_eq!(r.values, &out[..n]);
assert_eq!(r.underflow_count, info.underflow_count);
assert_eq!(r.status, info.status);
}
Err(e) => assert_eq!(Err(e), expected),
}
}
}
}
}
}