use astro_float::{BigFloat, Consts, RoundingMode};
use crate::base::errors::SymplexError;
pub(super) const LARGE_ORDER: f64 = 1.0e4;
const MAX_TERMS: f64 = 50_000.0;
const MARGIN: f64 = 64.0;
pub(super) fn is_odd(n: &BigFloat) -> bool {
if n.is_zero() {
return false;
}
let mut half = n.clone();
match n.exponent() {
Some(e) => {
half.set_exponent(e - 1);
!half.is_int()
}
None => false,
}
}
fn ln_abs_f64(x: &BigFloat, rm: RoundingMode) -> f64 {
let Some(e) = x.exponent() else {
return f64::NEG_INFINITY;
};
let mut m = x.abs();
m.set_exponent(0);
let mf = super::bigfloat_to_f64_rounded(&m, rm).unwrap_or(0.5);
f64::from(e) * std::f64::consts::LN_2 + mf.ln()
}
fn ln_gamma_f64(y: f64) -> f64 {
(y - 0.5) * y.ln() - y + 0.5 * (2.0 * std::f64::consts::PI).ln() + 1.0 / (12.0 * y)
}
fn overflow(what: &str, lg: f64) -> SymplexError {
SymplexError::Unevaluable {
reason: format!(
"{what}: the value (about 2^{lg:.3e}) overflows the arbitrary-precision exponent range"
),
}
}
fn out_of_range(what: &str, lg: f64) -> SymplexError {
SymplexError::Unevaluable {
reason: format!(
"{what} of large order: the factor (x/2)^nu/Gamma(nu + 1) (about 2^{lg:.3e}) is \
beyond the arbitrary-precision exponent range"
),
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Lg2Bounds {
pub(super) lo: f64,
pub(super) hi: f64,
}
pub(super) fn series_bounds(nu: f64, x: f64, alternating: bool) -> Lg2Bounds {
if nu.is_nan() || nu <= 0.0 || !x.is_finite() {
return Lg2Bounds {
lo: f64::NEG_INFINITY,
hi: f64::INFINITY,
};
}
let y = x * x / (4.0 * (nu + 1.0));
let hi = y * std::f64::consts::LOG2_E;
let lo = if !alternating {
0.0
} else if y < 1.0 {
(1.0 - y).log2()
} else {
f64::NEG_INFINITY
};
Lg2Bounds { lo, hi }
}
pub(super) fn power_over_gamma(
nu: &BigFloat,
x_half: &BigFloat,
bounds: Lg2Bounds,
what: &str,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<Option<BigFloat>, SymplexError> {
let min = f64::from(astro_float::EXPONENT_MIN) + MARGIN;
let max = f64::from(astro_float::EXPONENT_MAX) - MARGIN;
let decide = |l2: f64| -> Option<Result<Option<BigFloat>, SymplexError>> {
if l2 + bounds.hi < min {
Some(Ok(None))
} else if l2 + bounds.lo > max {
Some(Err(overflow(what, l2 + bounds.lo)))
} else if !(min..=max).contains(&l2) {
Some(Err(out_of_range(what, l2)))
} else {
None
}
};
let nu_f = super::bigfloat_to_f64_rounded(nu, rm)?;
let ln_xh = ln_abs_f64(x_half, rm);
let t = nu_f * ln_xh;
let (lg_est, lg_size) = if nu_f > 0.0 {
let g = ln_gamma_f64(nu_f + 1.0);
(g, g.abs())
} else {
let g = (std::f64::consts::PI).ln() - ln_gamma_f64(-nu_f);
(g, g.abs())
};
let l_est = t - lg_est;
if !l_est.is_finite() || !lg_size.is_finite() {
return if nu_f > 0.0 && l_est < 0.0 && bounds.hi.is_finite() {
Ok(None)
} else {
Err(out_of_range(what, l_est * std::f64::consts::LOG2_E))
};
}
let slack = 1e-9 * (t.abs() + lg_size) + 64.0;
let l2_est = l_est * std::f64::consts::LOG2_E;
if l2_est + slack + bounds.hi < min {
return Ok(None);
}
if l2_est + slack < min {
return Err(out_of_range(what, l2_est));
}
if nu_f > 0.0 && l2_est - slack + bounds.lo > max {
return Err(overflow(what, l2_est + bounds.lo));
}
if nu_f > 0.0 && l2_est - slack > max {
return Err(out_of_range(what, l2_est));
}
let l = log_power_over_gamma(nu, x_half, wp, rm, cc)?;
let l2 = super::bigfloat_to_f64_rounded(&l, rm)? * std::f64::consts::LOG2_E;
if let Some(r) = decide(l2) {
return r;
}
let v = l.exp(wp, rm, cc);
if v.is_zero() || v.is_inf() || v.is_nan() {
return Err(out_of_range(what, l2));
}
let nu1 = nu.add(&BigFloat::from_i32(1, 64), super::exact_bits(nu, wp), rm);
let negative = nu1.is_negative() && is_odd(&nu1.floor());
Ok(Some(if negative { v.neg() } else { v }))
}
pub(super) fn log_power_over_gamma(
nu: &BigFloat,
x_half: &BigFloat,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let nu_f = super::bigfloat_to_f64_rounded(nu, rm)?;
let t = nu_f * ln_abs_f64(x_half, rm);
let lg_size = if nu_f > 0.0 {
ln_gamma_f64(nu_f + 1.0).abs()
} else {
((std::f64::consts::PI).ln() - ln_gamma_f64(-nu_f)).abs()
};
let sin_bits = if nu_f > 0.0 {
0.0
} else {
nu.mantissa_max_bit_len().unwrap_or(wp) as f64
};
let size = t.abs() + lg_size + sin_bits + 2.0;
if !size.is_finite() {
return Err(out_of_range("a Bessel function", f64::INFINITY));
}
let lp = wp + 16 + size.log2().ceil().max(0.0) as usize;
let one = BigFloat::from_i32(1, 64);
let nu1 = nu.add(&one, super::exact_bits(nu, lp), rm);
let lg = super::arb_log_gamma(&nu1, lp, rm, cc)?;
Ok(nu.mul(&x_half.ln(lp, rm, cc), lp, rm).sub(&lg.0, lp, rm))
}
pub(super) fn refuse_long_i_series(nu: f64, x: f64) -> Result<(), SymplexError> {
let nu = nu.abs();
let peak = ((nu * nu + x * x).sqrt() - nu) / 2.0;
if !peak.is_finite() || peak > MAX_TERMS {
return Err(SymplexError::NotImplemented(format!(
"besseli of order {nu:e} at {x:e}: the ascending series would take more than \
{MAX_TERMS:e} terms (no uniform asymptotic expansion)"
)));
}
Ok(())
}
const MAX_RISING_STEPS: u64 = 400_000;
fn neumann_rising(nf: f64, xf: f64, ln_x2: f64) -> Option<(f64, f64)> {
let y = xf * xf / 4.0;
let last = (nf / 2.0).ln() + (nf - 1.0) * y.ln() - 2.0 * ln_gamma_f64(nf);
if !last.is_finite() {
return None;
}
let psi = 2.0 * (nf + 1.0).ln() + 2.0 + 2.0 * y / ((nf + 1.0) * (nf + 1.0));
let ln_psi = psi.ln() + y / (nf + 1.0);
let ln_log = (2.0 * ln_x2.abs()).max(f64::MIN_POSITIVE).ln();
let hi = ln_psi.max(ln_log);
let both = hi + ((ln_psi - hi).exp() + (ln_log - hi).exp()).ln();
let rest = 2.0 * nf * ln_x2 - ln_gamma_f64(nf + 1.0) - ln_gamma_f64(nf) + both;
let l2 = std::f64::consts::LOG2_E;
(rest.is_finite()).then_some((rest * l2, last * l2))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Second {
Y,
K,
}
pub(super) fn neumann_leading(
kind: Second,
n: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
let what = match kind {
Second::Y => "bessely",
Second::K => "besselk",
};
let nf = super::bigfloat_to_f64_rounded(n, rm).ok()?;
let xf = super::bigfloat_to_f64_rounded(x, rm).ok()?;
let rising = xf * xf > 2.0 * (nf - 1.0);
let applies = nf >= 16.0 && (!rising || kind == Second::Y);
if !applies {
return None;
}
let wp = prec + 40;
let ln_x2 = ln_abs_f64(x, rm) - std::f64::consts::LN_2;
let (rest, last) = if rising {
neumann_rising(nf, xf, ln_x2)?
} else {
let rest = 7f64.ln() + 2.0 * nf * ln_x2 - ln_gamma_f64(nf + 1.0) - ln_gamma_f64(nf)
+ (ln_x2.abs() + 2.0 * (nf + 1.0).ln() + 2.0).ln();
(rest * std::f64::consts::LOG2_E, f64::NEG_INFINITY)
};
let negligible = |lg_sum: f64| rest.max(last) - lg_sum < -((wp + 16) as f64);
if !rising && !negligible(0.0) {
return None;
}
let stop_bits = if rising {
(wp + 9) as i64 + nf.log2().ceil() as i64
} else {
(wp + 8) as i64
};
let max_steps: u64 = if rising {
MAX_RISING_STEPS
} else {
(4 * wp) as u64
};
let two = BigFloat::from_i32(2, 64);
let x_half = x.div(&two, wp, rm);
let nm1 = n.sub(&BigFloat::from_i32(1, 64), super::exact_bits(n, wp), rm);
let bounds = Lg2Bounds { lo: -1.0, hi: 1.0 };
let p = match power_over_gamma(&nm1, &x_half, bounds, what, wp, rm, cc) {
Ok(Some(p)) => p,
Ok(None) => {
let l2 = (ln_gamma_f64(nf) - nf * ln_x2) * std::f64::consts::LOG2_E;
return Some(Err(overflow(what, l2)));
}
Err(e) => return Some(Err(e)),
};
let q = p.mul(&x_half, wp, rm).reciprocal(wp, rm);
let xh2 = x_half.mul(&x_half, wp, rm);
let mut c = BigFloat::from_i32(1, wp);
let mut sum = c.clone();
let mut k: u64 = 1;
loop {
let kb = BigFloat::from_u64(k, 64);
let nk = n.sub(&kb, super::exact_bits(n, wp), rm);
c = c.mul(&xh2, wp, rm).div(&kb.mul(&nk, wp, rm), wp, rm);
let kf = k as f64;
let r = xf * xf / 4.0 / (kf * (nf - kf));
let geometric = if !rising {
0
} else if r < 0.99 && kf < nf / 2.0 {
(1.0 / (1.0 - r)).log2().ceil() as i64
} else {
i64::MAX / 4
};
let small = match (c.exponent(), sum.exponent()) {
(Some(ce), Some(se)) => i64::from(se) - i64::from(ce) > stop_bits + geometric,
_ => c.is_zero(),
};
if small {
break;
}
if rising && kf >= nf / 2.0 {
return None;
}
sum = if kind == Second::K && k % 2 == 1 {
sum.sub(&c, wp, rm)
} else {
sum.add(&c, wp, rm)
};
if k as f64 >= nf - 1.0 {
break;
}
k += 1;
if k > max_steps {
return None;
}
}
if rising {
let lg_sum = match sum.exponent() {
Some(e) if !sum.is_zero() => f64::from(e) - 1.0,
_ => return None,
};
if !negligible(lg_sum) {
return None;
}
}
let v = match kind {
Second::Y => q
.mul(&sum, wp, rm)
.div(&cc.pi(wp, rm).clone(), wp, rm)
.neg(),
Second::K => q.mul(&sum, wp, rm).div(&two, wp, rm),
};
if v.is_zero() || v.is_inf() || v.is_nan() {
let l2 = (ln_gamma_f64(nf) - nf * ln_x2) * std::f64::consts::LOG2_E;
return Some(Err(overflow(what, l2)));
}
Some(Ok(super::round_to(v, prec, rm)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parity_of_huge_integers() {
let p = 256;
assert!(is_odd(&BigFloat::from_i32(-7, p)));
assert!(!is_odd(&BigFloat::from_i32(10, p)));
assert!(!is_odd(&BigFloat::from_i32(0, p)));
let e20 = BigFloat::from_u64(10_000_000_000, p).mul(
&BigFloat::from_u64(10_000_000_000, p),
p,
RoundingMode::ToEven,
);
assert!(!is_odd(&e20));
let e20p1 = e20.add(&BigFloat::from_i32(1, p), p, RoundingMode::ToEven);
assert!(is_odd(&e20p1));
}
#[test]
fn series_bounds_of_j_and_i() {
let b = series_bounds(1.0e4, 1.0, true);
assert!(b.lo < 0.0 && b.lo > -1e-4 && b.hi > 0.0 && b.hi < 1e-4);
assert_eq!(series_bounds(1.0e4, 1.0, false).lo, 0.0);
assert_eq!(series_bounds(-1.0e4, 1.0, true).hi, f64::INFINITY);
}
}