use std::cell::RefCell;
use astro_float::{BigFloat, Consts, RoundingMode};
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{Signed, ToPrimitive, Zero};
use crate::base::errors::SymplexError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Kind {
J,
Y,
I,
K,
}
const MAX_TERMS: usize = 60;
const MIN_ORDER: f64 = 16.0;
struct UPoly {
coeffs: Vec<(usize, Ratio<BigInt>)>,
lg: Vec<f64>,
}
thread_local! {
static U: RefCell<Vec<UPoly>> = const { RefCell::new(Vec::new()) };
}
fn lg_upper(c: &Ratio<BigInt>) -> f64 {
let lg_int = |n: &BigInt| -> (f64, f64) {
let bits = n.bits() as f64;
match n.abs().to_f64() {
Some(f) if f.is_finite() && f > 0.0 => {
let l = f.log2();
(l - 1e-9, l + 1e-9)
}
_ => (bits - 1.0, bits),
}
};
let (_, num_hi) = lg_int(c.numer());
let (den_lo, _) = lg_int(c.denom());
num_hi - den_lo
}
fn next_poly(u: &UPoly) -> UPoly {
let top = u.coeffs.iter().map(|(j, _)| *j).max().unwrap_or(0) + 3;
let mut dense: Vec<Ratio<BigInt>> = vec![Ratio::zero(); top + 1];
for (j, c) in &u.coeffs {
let j = *j;
let jb = BigInt::from(j as u64);
let a = Ratio::new(
BigInt::from(4u64) * &jb * BigInt::from(j as u64 + 1) + BigInt::from(1u64),
BigInt::from(8u64) * BigInt::from(j as u64 + 1),
);
let b = Ratio::new(
BigInt::from(4u64) * &jb * BigInt::from(j as u64 + 3) + BigInt::from(5u64),
BigInt::from(8u64) * BigInt::from(j as u64 + 3),
);
dense[j + 1] += c * a;
dense[j + 3] -= c * b;
}
let coeffs: Vec<(usize, Ratio<BigInt>)> = dense
.into_iter()
.enumerate()
.filter(|(_, c)| !c.is_zero())
.collect();
let lg = coeffs.iter().map(|(_, c)| lg_upper(c)).collect();
UPoly { coeffs, lg }
}
fn with_u<R>(n: usize, f: impl FnOnce(&[UPoly]) -> R) -> R {
U.with(|cell| {
let mut us = cell.borrow_mut();
if us.is_empty() {
us.push(UPoly {
coeffs: vec![(0, Ratio::from_integer(BigInt::from(1u64)))],
lg: vec![0.0],
});
}
while us.len() <= n {
let next = match us.last() {
Some(u) => next_poly(u),
None => break,
};
us.push(next);
}
f(&us[..=n.min(us.len() - 1)])
})
}
fn lg_add(a: f64, b: f64) -> f64 {
if a == f64::NEG_INFINITY {
return b;
}
if b == f64::NEG_INFINITY {
return a;
}
let (hi, lo) = if a >= b { (a, b) } else { (b, a) };
hi + (lo - hi).exp2().ln_1p() / std::f64::consts::LN_2 + 1e-12
}
fn lg_abs_sum(u: &UPoly, lg_t: f64) -> f64 {
u.coeffs
.iter()
.zip(&u.lg)
.fold(f64::NEG_INFINITY, |acc, ((j, _), lc)| {
lg_add(acc, lc + (*j as f64) * lg_t)
})
}
struct Plan {
n: usize,
lg_trunc: f64,
s_low: f64,
abs_sum: f64,
}
fn plan(lg_t: f64, nu_f: f64, wp: usize) -> Option<Plan> {
let lg_nu = nu_f.log2();
let lg_abs = |k: usize| with_u(k, |us| us.get(k).map(|u| lg_abs_sum(u, lg_t)));
let v1 = lg_abs(1)?.exp2();
if !v1.is_finite() {
return None;
}
let factor = 1.0 + 2.0 * v1 / nu_f * std::f64::consts::LOG2_E;
let mut tail = 0.0f64;
for k in 1..=MAX_TERMS {
let lg_term = lg_abs(k)? - (k as f64) * lg_nu;
let lg_trunc = factor + lg_term;
if lg_trunc < -((wp + 6) as f64) {
let s_low = 1.0 - tail;
return (s_low >= 0.5).then_some(Plan {
n: k,
lg_trunc,
s_low,
abs_sum: 1.0 + tail,
});
}
tail += lg_term.exp2();
if !tail.is_finite() || tail > 0.5 {
return None;
}
}
None
}
fn sum_terms(
n: usize,
t: &BigFloat,
nu: &BigFloat,
alternating: bool,
wp: usize,
rm: RoundingMode,
) -> BigFloat {
with_u(n, |us| {
let inv_nu = BigFloat::from_i32(1, wp).div(nu, wp, rm);
let mut scale = BigFloat::from_i32(1, wp);
let mut sum = BigFloat::new(wp);
for (k, u) in us.iter().enumerate().take(n) {
if k > 0 {
scale = scale.mul(&inv_nu, wp, rm);
}
let term = eval_dense(u, k, t, false, wp, rm).mul(&scale, wp, rm);
sum = if alternating && k % 2 == 1 {
sum.sub(&term, wp, rm)
} else {
sum.add(&term, wp, rm)
};
}
sum
})
}
fn eval_dense(
u: &UPoly,
k: usize,
t: &BigFloat,
alt: bool,
wp: usize,
rm: RoundingMode,
) -> BigFloat {
let t2 = t.mul(t, wp, rm);
let mut acc = BigFloat::new(wp);
let mut idx = u.coeffs.len();
let top = u.coeffs.last().map_or(k, |(j, _)| *j);
let mut j = top;
loop {
let c = if idx > 0 && u.coeffs[idx - 1].0 == j {
idx -= 1;
let m = (j - k) / 2;
let cf = super::ratio_to_bigfloat(&u.coeffs[idx].1, wp, rm);
if alt && m % 2 == 1 { cf.neg() } else { cf }
} else {
BigFloat::new(wp)
};
acc = acc.mul(&t2, wp, rm).add(&c, wp, rm);
if j < k + 2 {
break;
}
j -= 2;
}
acc.mul(&t.powi(k, wp, rm), wp, rm)
}
fn lg(x: &BigFloat) -> f64 {
match x.exponent() {
Some(e) if !x.is_zero() => {
let mut m = x.abs();
m.set_exponent(0);
let mf = super::bigfloat_to_f64_rounded(&m, RoundingMode::ToEven).unwrap_or(0.5);
f64::from(e) + mf.log2()
}
_ => f64::NEG_INFINITY,
}
}
fn int_bits(v: f64) -> usize {
if v.is_finite() && v > 1.0 {
v.log2().ceil() as usize + 1
} else {
1
}
}
fn overflow(kind: Kind, lg2: f64) -> SymplexError {
let what = match kind {
Kind::J => "besselj",
Kind::Y => "bessely",
Kind::I => "besseli",
Kind::K => "besselk",
};
SymplexError::Unevaluable {
reason: format!(
"{what}: the value (about 2^{lg2:.3e}) overflows the arbitrary-precision exponent range"
),
}
}
fn ln_stirling_ratio(
nu: &BigFloat,
lp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
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(&nu.ln(lp, rm, cc), lp, rm)
.sub(nu, lp, rm)
.sub(&lg.0, lp, rm))
}
fn scale_exp(
kind: Kind,
e: &BigFloat,
f: &BigFloat,
prec: usize,
wp: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<BigFloat, SymplexError> {
let l2 = super::bigfloat_to_f64_rounded(e, rm)? * std::f64::consts::LOG2_E + lg(f);
let min = f64::from(astro_float::EXPONENT_MIN) + 64.0;
let max = f64::from(astro_float::EXPONENT_MAX) - 64.0;
if l2 < min {
return Ok(BigFloat::new(prec));
}
if l2 > max {
return Err(overflow(kind, l2));
}
let v = e.exp(wp, rm, cc).mul(f, wp, rm);
if v.is_zero() || v.is_inf() || v.is_nan() {
return Err(overflow(kind, l2));
}
Ok(super::round_to(v, prec, rm))
}
pub(super) fn debye(
kind: Kind,
nu: &BigFloat,
x: &BigFloat,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
if !super::bf_strictly_positive(nu)
|| !super::bf_strictly_positive(x)
|| nu.is_inf()
|| x.is_inf()
|| nu.is_nan()
|| x.is_nan()
{
return None;
}
let nu_f = super::bigfloat_to_f64_rounded(nu, rm).ok()?;
let x_f = super::bigfloat_to_f64_rounded(x, rm).ok()?;
if nu_f.is_nan() || nu_f < MIN_ORDER || !nu_f.is_finite() || !x_f.is_finite() || x_f <= 0.0 {
return None;
}
match kind {
Kind::K | Kind::I => modified(kind, nu, x, nu_f, x_f, prec, rm, cc),
Kind::J | Kind::Y => match x.cmp(nu) {
Some(c) if c < 0 => {
if kind == Kind::Y {
return None;
}
j_below(nu, x, nu_f, x_f, prec, rm, cc)
}
Some(c) if c > 0 => hankel(kind, nu, x, nu_f, x_f, prec, rm, cc),
_ => None,
},
}
}
#[allow(clippy::too_many_arguments)]
fn modified(
kind: Kind,
nu: &BigFloat,
x: &BigFloat,
nu_f: f64,
x_f: f64,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
let wp = prec + 40;
let r_f = nu_f.hypot(x_f);
let ln_part = nu_f * ((nu_f + r_f) / x_f).ln().abs();
let stirling = if kind == Kind::I {
nu_f * nu_f.ln().abs() + nu_f
} else {
0.0
};
let lp = wp + 16 + int_bits(r_f + ln_part + stirling + 1.0);
let r = nu
.mul(nu, lp, rm)
.add(&x.mul(x, lp, rm), lp, rm)
.sqrt(lp, rm);
let p = nu.div(&r, wp, rm);
let lg_t = if kind == Kind::I { 0.0 } else { lg(&p) + 1e-9 };
let pl = plan(lg_t, nu_f, wp)?;
let s = sum_terms(pl.n, &p, nu, kind == Kind::K, wp, rm);
let nu_r = nu.add(&r, lp, rm);
let (e, f) = if kind == Kind::K {
let e = nu
.mul(&nu_r.div(x, lp, rm).ln(lp, rm, cc), lp, rm)
.sub(&r, lp, rm);
let pi = cc.pi(wp, rm).clone();
let f = pi
.div(&r.mul(&BigFloat::from_i32(2, 64), wp, rm), wp, rm)
.sqrt(wp, rm);
(e, f)
} else {
let e = match ln_stirling_ratio(nu, lp, rm, cc) {
Ok(v) => v,
Err(err) => return Some(Err(err)),
};
let e = e.add(&r, lp, rm).add(
&nu.mul(&x.div(&nu_r, lp, rm).ln(lp, rm, cc), lp, rm),
lp,
rm,
);
let s1 = sum_terms(pl.n, &BigFloat::from_i32(1, wp), nu, false, wp, rm);
let f = p.sqrt(wp, rm).div(&s1, wp, rm);
(e, f)
};
let n = pl.n as f64;
let rounding = ((10.0 * n + 10.0) * pl.abs_sum).log2() - wp as f64;
let rel = lg_add(
lg_add(pl.lg_trunc, rounding) - pl.s_low.log2() + 1.0,
-(wp as f64) + 4.0,
);
if rel > -((prec + 4) as f64) {
return None;
}
let factor = f.mul(&s, wp, rm);
Some(scale_exp(kind, &e, &factor, prec, wp, rm, cc))
}
#[allow(clippy::too_many_arguments)]
fn j_below(
nu: &BigFloat,
x: &BigFloat,
nu_f: f64,
x_f: f64,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
let wp = prec + 40;
let stirling = nu_f * nu_f.ln().abs() + nu_f;
let ln_part = nu_f * (x_f / (2.0 * nu_f)).ln().abs();
let lp = wp + 16 + int_bits(stirling + ln_part + nu_f + 1.0);
let d = nu.sub(x, lp, rm);
let w = d.mul(&nu.add(x, lp, rm), lp, rm).sqrt(lp, rm);
let p = nu.div(&w, wp, rm);
let lg_t = lg(&p) + 1e-9;
let pl = plan(lg_t, nu_f, wp)?;
let s = sum_terms(pl.n, &p, nu, false, wp, rm);
let s1 = sum_terms(pl.n, &BigFloat::from_i32(1, wp), nu, false, wp, rm);
let e = match ln_stirling_ratio(nu, lp, rm, cc) {
Ok(v) => v,
Err(err) => return Some(Err(err)),
};
let nu_w = nu.add(&w, lp, rm);
let e = e.add(&w, lp, rm).add(
&nu.mul(&x.div(&nu_w, lp, rm).ln(lp, rm, cc), lp, rm),
lp,
rm,
);
let f = p.sqrt(wp, rm).div(&s1, wp, rm);
let n = pl.n as f64;
let rounding = ((10.0 * n + 10.0) * pl.abs_sum).log2() - wp as f64;
let rel = lg_add(
lg_add(pl.lg_trunc, rounding) - pl.s_low.log2() + 1.0,
-(wp as f64) + 4.0,
);
if rel > -((prec + 4) as f64) {
return None;
}
let factor = f.mul(&s, wp, rm);
Some(scale_exp(Kind::J, &e, &factor, prec, wp, rm, cc))
}
#[allow(clippy::too_many_arguments)]
fn hankel(
kind: Kind,
nu: &BigFloat,
x: &BigFloat,
nu_f: f64,
x_f: f64,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<Result<BigFloat, SymplexError>> {
let cap = prec * 4 + 1024;
let mut wp = prec + 40;
loop {
let lp = wp + 16 + int_bits(x_f + nu_f + 1.0);
let d = x.sub(nu, lp, rm);
let w = d.mul(&x.add(nu, lp, rm), lp, rm).sqrt(lp, rm);
let q = nu.div(&w, wp, rm);
let lg_t = lg(&q) + 1e-9;
let pl = plan(lg_t, nu_f, wp)?;
let (re_s, im_s) = with_signs(pl.n, &q, nu, wp, rm);
let pi = cc.pi(lp, rm).clone();
let xi = w
.sub(&nu.mul(&w.div(nu, lp, rm).atan(lp, rm, cc), lp, rm), lp, rm)
.sub(&pi.div(&BigFloat::from_i32(4, 64), lp, rm), lp, rm);
let (c, s) = (xi.cos(lp, rm, cc), xi.sin(lp, rm, cc));
let amp = BigFloat::from_i32(2, wp)
.div(&pi.mul(&w, wp, rm), wp, rm)
.sqrt(wp, rm);
let combo = if kind == Kind::J {
c.mul(&re_s, wp, rm).sub(&s.mul(&im_s, wp, rm), wp, rm)
} else {
s.mul(&re_s, wp, rm).add(&c.mul(&im_s, wp, rm), wp, rm)
};
let n = pl.n as f64;
let rounding = ((10.0 * n + 10.0) * pl.abs_sum).log2() - wp as f64;
let abs_err = lg_add(lg_add(pl.lg_trunc, rounding) + 1.0, -(wp as f64) + 4.0);
let lost = -lg(&combo);
if combo.is_zero() || abs_err + lost > -((prec + 4) as f64) {
if wp >= cap {
return None;
}
let more = if combo.is_zero() {
wp
} else {
lost.ceil() as usize + 16
};
wp = (wp + more).min(cap);
continue;
}
return Some(Ok(super::round_to(amp.mul(&combo, wp, rm), prec, rm)));
}
}
fn with_signs(
n: usize,
q: &BigFloat,
nu: &BigFloat,
wp: usize,
rm: RoundingMode,
) -> (BigFloat, BigFloat) {
with_u(n, |us| {
let inv_nu = BigFloat::from_i32(1, wp).div(nu, wp, rm);
let mut scale = BigFloat::from_i32(1, wp);
let mut re = BigFloat::new(wp);
let mut im = BigFloat::new(wp);
for (k, u) in us.iter().enumerate().take(n) {
if k > 0 {
scale = scale.mul(&inv_nu, wp, rm);
}
let term = eval_dense(u, k, q, true, wp, rm).mul(&scale, wp, rm);
match k % 4 {
0 => re = re.add(&term, wp, rm),
1 => im = im.sub(&term, wp, rm),
2 => re = re.sub(&term, wp, rm),
_ => im = im.add(&term, wp, rm),
}
}
(re, im)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_polynomials_match_dlmf() {
with_u(3, |us| {
let r = |n: i64, d: i64| Ratio::new(BigInt::from(n), BigInt::from(d));
assert_eq!(us[1].coeffs, vec![(1, r(1, 8)), (3, r(-5, 24))]);
assert_eq!(
us[2].coeffs,
vec![(2, r(81, 1152)), (4, r(-462, 1152)), (6, r(385, 1152))]
);
assert_eq!(
us[3].coeffs,
vec![
(3, r(30375, 414720)),
(5, r(-369603, 414720)),
(7, r(765765, 414720)),
(9, r(-425425, 414720))
]
);
});
}
}