use astro_float::{BigFloat, Consts, RoundingMode};
use rustc_hash::FxHashMap;
use super::accuracy::{self, EXACT, ErrExp, UNKNOWN};
use crate::base::arena::Arena;
use crate::base::bigcomplex::{Complex, c_sub};
use crate::base::libfn::LibFn;
use crate::base::node::{ExprId, ExprNode};
const LP: usize = 128;
const LOG2E: f64 = std::f64::consts::LOG2_E;
const LN2: f64 = std::f64::consts::LN_2;
const BALL: f64 = 0.192_645_077_942_396_2;
fn shrink(x: f64) -> f64 {
if x == f64::NEG_INFINITY {
0.0
} else {
-(-x.exp2()).ln_1p() * LOG2E
}
}
fn swell(x: f64) -> f64 {
if x == f64::NEG_INFINITY {
0.0
} else {
x.exp2().ln_1p() * LOG2E
}
}
const LOG2_TWO_OVER_SQRT_PI: f64 = 0.176_656_383_560_998_7;
const EULER_GAMMA_UP: f64 = 0.577_215_664_901_533;
#[derive(Clone, Copy, Debug, PartialEq)]
enum Sens {
Zero,
Deriv(f64),
Change(f64),
Numeric,
Unbounded,
}
fn lg(x: &BigFloat) -> f64 {
if x.is_zero() {
return f64::NEG_INFINITY;
}
if x.is_nan() || x.is_inf() {
return f64::INFINITY;
}
let e = x.exponent().unwrap_or(0);
let mut m = x.abs();
m.set_exponent(0);
let mf = super::bigfloat_to_f64_rounded(&m, RoundingMode::ToEven).unwrap_or(0.75);
f64::from(e) + mf.abs().max(0.5).log2()
}
fn lsum(a: f64, b: f64) -> f64 {
let (hi, lo) = if a >= b { (a, b) } else { (b, a) };
if hi == f64::NEG_INFINITY || !hi.is_finite() {
return hi;
}
hi + (lo - hi).exp2().ln_1p() * LOG2E
}
fn f(x: &BigFloat) -> f64 {
if x.is_zero() {
return 0.0;
}
let l = lg(x);
if l > 1000.0 {
return if neg(x) {
f64::NEG_INFINITY
} else {
f64::INFINITY
};
}
super::bigfloat_to_f64_rounded(x, RoundingMode::ToEven).unwrap_or(0.0)
}
fn pos(x: &BigFloat) -> bool {
x.is_positive() && !x.is_zero()
}
fn neg(x: &BigFloat) -> bool {
x.is_negative() && !x.is_zero()
}
fn diff(a: &BigFloat, b: &BigFloat) -> BigFloat {
if b.is_zero() {
a.clone()
} else if a.is_zero() {
b.neg()
} else {
a.sub_full_prec(b)
}
}
fn sum(a: &BigFloat, b: &BigFloat) -> BigFloat {
diff(a, &b.neg())
}
fn int(n: i32) -> BigFloat {
BigFloat::from_i32(n, LP)
}
fn one_minus(x: &BigFloat) -> BigFloat {
diff(&int(1), x)
}
fn ln1p_abs(x: &BigFloat) -> f64 {
let a = f(x).abs();
if a.is_finite() {
a.ln_1p()
} else {
lg(x) * LN2
}
}
fn inside(ld: f64, e: f64) -> bool {
ld > e + 3.0
}
fn log2_factorial(n: f64) -> f64 {
if n < 2.0 {
return 0.0;
}
if n <= 64.0 {
let mut s = 0.0;
let mut k = 2.0;
while k <= n {
s += f64::log2(k);
k += 1.0;
}
return s;
}
((n + 0.5) * n.ln() - n + 0.5 * (2.0 * std::f64::consts::PI).ln() + 1.0 / (12.0 * n)) * LOG2E
}
fn ln_gamma_f64(x: &BigFloat) -> (f64, f64) {
let xf = f(x);
if !(1e-300..=1e300).contains(&xf) {
let ln_x = lg(x) * LN2;
return if xf < 1.0 {
(-ln_x, 1.0)
} else {
(xf * (ln_x - 1.0), ln_x.abs() + 1.0 + xf * 1e-12)
};
}
let mut z = xf;
let mut shift = 0.0;
while z < 16.0 {
shift += z.ln();
z += 1.0;
}
let z2 = z * z;
let series = 1.0 / (12.0 * z) - 1.0 / (360.0 * z * z2) + 1.0 / (1260.0 * z * z2 * z2);
let v = (z - 0.5) * z.ln() - z + 0.5 * (2.0 * std::f64::consts::PI).ln() + series - shift;
(v, 1e-9 + 1e-14 * (v.abs() + shift.abs() + z * z.ln().abs()))
}
fn log2_beta_bounds(p: &BigFloat, q: &BigFloat) -> (f64, f64) {
let s = sum(p, q);
let (a, ea) = ln_gamma_f64(p);
let (b, eb) = ln_gamma_f64(q);
let (c, ec) = ln_gamma_f64(&s);
let (v, err) = (a + b - c, ea + eb + ec);
((v - err) * LOG2E, (v + err) * LOG2E)
}
fn pole_distance(x: &BigFloat) -> Option<f64> {
if pos(x) {
return Some(lg(x));
}
let frac = diff(x, &x.floor());
let d = frac.clone().min(&one_minus(&frac));
(!d.is_zero()).then(|| lg(&d))
}
fn psi_abs(x: &BigFloat, e: f64) -> Option<f64> {
let ld = pole_distance(x)?;
if !inside(ld, e) {
return None;
}
Some(lsum(
-ld + shrink(e - ld),
(ln1p_abs(x) + 1.0 + EULER_GAMMA_UP).log2(),
))
}
fn psi_diff(u: &BigFloat, v: &BigFloat, h: &BigFloat, e: f64) -> Option<f64> {
let crude = lsum(psi_abs(u, e)?, psi_abs(v, e)?);
if pos(u) && pos(v) {
let lm = lg(u).min(lg(v));
let mvt = lg(h) + lsum(-lm, -2.0 * lm) + 2.0 * shrink(e - lm);
return Some(crude.min(mvt));
}
Some(crude)
}
fn polygamma_deriv(n: f64, x: &BigFloat, e: f64) -> Sens {
let Some(ld) = pole_distance(x) else {
return Sens::Unbounded;
};
if !inside(ld, e) {
return Sens::Unbounded;
}
let p = n + 2.0;
let sum = if pos(x) {
lsum(-p * ld, -(p - 1.0) * ld - (p - 1.0).log2())
} else {
lsum(-p * ld, p + 2.0)
};
let growth = -p * (1.0 - (e - ld).exp2()).log2();
Sens::Deriv(log2_factorial(n + 1.0) + sum + growth.min(p * BALL))
}
fn discrete(e: f64) -> Sens {
if e <= -40.0 {
Sens::Zero
} else {
Sens::Unbounded
}
}
fn deriv_if(ok: bool, d: f64) -> Sens {
if ok { Sens::Deriv(d) } else { Sens::Unbounded }
}
struct Args<'a> {
x: Vec<&'a BigFloat>,
e: Vec<f64>,
v: &'a BigFloat,
rm: RoundingMode,
}
fn radius(e: ErrExp) -> f64 {
e
}
fn analytic(arena: &Arena, node: &ExprNode, i: usize, a: &Args<'_>, cc: &mut Consts) -> Sens {
let x = a.x[i];
let e = a.e[i];
let lv = lg(a.v);
match node {
ExprNode::Gamma(_) => psi_abs(x, e).map_or(Sens::Unbounded, |s| Sens::Deriv(lv + s)),
ExprNode::Factorial(_) => {
let x1 = sum(x, &int(1));
psi_abs(&x1, e).map_or(Sens::Unbounded, |s| Sens::Deriv(lv + s))
}
ExprNode::LogGamma(_) => psi_abs(x, e).map_or(Sens::Unbounded, Sens::Deriv),
ExprNode::Digamma(_) => polygamma_deriv(0.0, x, e),
ExprNode::Polygamma(..) => {
if i == 0 {
discrete(e)
} else {
polygamma_deriv(f(a.x[0]).round().max(0.0), x, e)
}
}
ExprNode::Beta(..) => {
let (p, q) = (a.x[i], a.x[1 - i]);
let s = sum(p, q);
psi_diff(p, &s, q, e).map_or(Sens::Unbounded, |d| Sens::Deriv(lv + d))
}
ExprNode::Binomial(..) => {
if a.v.is_zero() {
let (n, k) = (a.x[0], a.x[1]);
let k_pole = a.e[1] == f64::NEG_INFINITY && k.is_int() && neg(k);
let n1 = sum(n, &int(1));
let n_clear = pole_distance(&n1).is_some_and(|ld| inside(ld, a.e[0]));
return if k_pole && n_clear {
Sens::Zero
} else {
Sens::Unbounded
};
}
let (n, k) = (a.x[0], a.x[1]);
let one = int(1);
let n1 = sum(n, &one);
let nk1 = sum(&diff(n, k), &one);
let k1 = sum(k, &one);
let d = if i == 0 {
psi_diff(&n1, &nk1, k, e)
} else {
psi_diff(&nk1, &k1, &diff(&nk1, &k1), e)
};
d.map_or(Sens::Unbounded, |d| Sens::Deriv(lv + d))
}
ExprNode::Erf(_) | ExprNode::Erfc(_) => {
let lo = (f(x).abs() - e.exp2()).max(0.0);
Sens::Deriv(LOG2_TWO_OVER_SQRT_PI - lo * lo * LOG2E)
}
ExprNode::Si(_) => Sens::Deriv((-lg(x)).min(0.0)),
ExprNode::Ci(_) => deriv_if(inside(lg(x), e), -lg(x) + shrink(e - lg(x))),
ExprNode::Ei(_) => deriv_if(
inside(lg(x), e),
(f(x) + e.exp2()) * LOG2E - lg(x) + shrink(e - lg(x)),
),
ExprNode::Li(_) => {
let u = diff(x, &int(1));
if !inside(lg(&u), e) || !inside(lg(x), e) {
return Sens::Unbounded;
}
if lg(&u) < -1.0 {
Sens::Deriv((1.0 + f(&u).abs()).log2() - lg(&u) + BALL)
} else {
Sens::Deriv(-(lg(x).abs() * LN2).max(0.28).log2() + 1.0)
}
}
ExprNode::Zeta(_) => {
if neg(x) {
return Sens::Numeric;
}
let low = if pos(x) && inside(lg(x), e) {
(f(x) - e.exp2()).max(0.875 * lg(x).exp2())
} else {
f(x) - e.exp2()
};
if low >= 3.0 {
return Sens::Deriv(1.0 - low);
}
let lu = lg(&diff(x, &int(1)));
deriv_if(inside(lu, e), lsum(-2.0 * (lu - shrink(e - lu)), 0.0))
}
ExprNode::DiracDelta(_) => Sens::Zero,
ExprNode::Apply(sid, _) => match arena.lib_fn(*sid) {
Some(fun) => lib_sens(fun, i, a, cc),
None => Sens::Numeric,
},
_ => Sens::Numeric,
}
}
fn lib_sens(fun: LibFn, i: usize, a: &Args<'_>, cc: &mut Consts) -> Sens {
let x = a.x[i];
let e = a.e[i];
let lv = lg(a.v);
match fun {
LibFn::Erfi => {
let hi = f(x).abs() + e.exp2();
Sens::Deriv(LOG2_TWO_OVER_SQRT_PI + hi * hi * LOG2E)
}
LibFn::ErfInv | LibFn::ErfcInv => {
let (lo, hi) = if fun == LibFn::ErfInv {
(sum(x, &int(1)), one_minus(x))
} else {
(x.clone(), diff(&int(2), x))
};
let w = f(a.v);
deriv_if(
inside(lg(&lo), e) && inside(lg(&hi), e),
-LOG2_TWO_OVER_SQRT_PI + w * w * LOG2E + 0.5,
)
}
LibFn::Shi => {
let ax = f(x).abs() + e.exp2();
Sens::Deriv(if ax < 1.0 {
0.25
} else {
ax * LOG2E - (2.0 * ax).log2()
})
}
LibFn::Chi => deriv_if(
inside(lg(x), e),
(f(x).abs() + e.exp2()) * LOG2E - lg(x) + shrink(e - lg(x)),
),
LibFn::FresnelS | LibFn::FresnelC => {
let lx = lg(x);
if lx > 1.0 && inside(lx, e) && 1.0 - lx < e {
Sens::Change(1.0 - lx)
} else {
Sens::Deriv(0.0)
}
}
LibFn::UpperGamma | LibFn::LowerGamma => incomplete_gamma(fun, i, a),
LibFn::ExpInt => {
let (nu, xv) = (a.x[0], a.x[1]);
if !pos(xv) || a.v.is_zero() {
return Sens::Numeric;
}
let c = f(nu).min(0.0).abs() + 1.0;
let mean_t = 1.0 + c * (-lg(xv)).exp2();
if i == 1 {
deriv_if(inside(lg(xv), a.e[1]), lv + mean_t.log2() + BALL)
} else {
Sens::Deriv(lv + mean_t.ln().max(f64::MIN_POSITIVE).log2())
}
}
LibFn::PolyLog => polylog_sens(i, a),
LibFn::DirichletEta => eta_sens(x),
LibFn::AiryAi | LibFn::AiryBi | LibFn::AiryAiPrime | LibFn::AiryBiPrime => {
let ax = f(x).abs() + e.exp2();
let prime = matches!(fun, LibFn::AiryAiPrime | LibFn::AiryBiPrime);
Sens::Deriv(if neg(x) {
if prime {
ax.log2()
} else {
(ax.sqrt().sqrt() + 1.0).log2()
}
} else {
let plus = if fun == LibFn::AiryAiPrime { 0.0 } else { 1.0 };
(ax.sqrt() + plus).log2() + lv + 0.2
})
}
LibFn::BesselJ | LibFn::BesselY | LibFn::BesselI | LibFn::BesselK => {
if i == 0 {
return bessel_order(fun, a);
}
bessel_x(fun, a, cc)
}
LibFn::EllipticK | LibFn::EllipticE => {
let u = one_minus(x);
if !inside(lg(&u), e) {
return Sens::Unbounded;
}
if lg(x) < -1.0 {
return Sens::Deriv(0.0);
}
let half_pi = std::f64::consts::FRAC_PI_2.log2();
let lu = lg(&u);
Sens::Deriv(if fun == LibFn::EllipticK {
lsum(half_pi + 0.5 * lu.max(0.0), lu + lv) - 1.0 - lg(x) - lu + BALL
} else {
lsum(lv, half_pi - 0.5 * lu.min(0.0)) - 1.0 - lg(x) + BALL
})
}
LibFn::EllipticF => elliptic_f_sens(i, a, cc),
LibFn::EllipticPi => elliptic_pi_sens(i, a),
LibFn::BetaInc | LibFn::BetaIncRegularized => {
betainc_sens(fun == LibFn::BetaIncRegularized, i, a)
}
LibFn::Legendre
| LibFn::ChebyshevT
| LibFn::ChebyshevU
| LibFn::Hermite
| LibFn::Laguerre
| LibFn::Gegenbauer
| LibFn::Jacobi
| LibFn::AssocLaguerre => {
if i == 0 {
discrete(e)
} else {
Sens::Numeric
}
}
LibFn::AssocLegendre => {
if i < 2 {
discrete(e)
} else {
Sens::Numeric
}
}
LibFn::Harmonic => polygamma_deriv(0.0, &sum(x, &int(1)), e),
LibFn::RisingFactorial | LibFn::FallingFactorial => {
if a.v.is_zero() {
return Sens::Numeric;
}
let (xv, nv) = (a.x[0], a.x[1]);
let one = int(1);
let (upper, lower) = if fun == LibFn::RisingFactorial {
(sum(xv, nv), xv.clone())
} else {
(sum(xv, &one), sum(&diff(xv, nv), &one))
};
let d = if i == 0 {
psi_diff(&upper, &lower, nv, e)
} else if fun == LibFn::RisingFactorial {
psi_abs(&upper, e)
} else {
psi_abs(&lower, e)
};
d.map_or(Sens::Unbounded, |d| Sens::Deriv(lv + d))
}
_ => Sens::Numeric,
}
}
fn bessel_order(fun: LibFn, a: &Args<'_>) -> Sens {
let (nu, x) = (a.x[0], a.x[1]);
if neg(nu) || !pos(x) || a.v.is_zero() {
return Sens::Numeric;
}
let (nf, xf) = (f(nu), f(x));
let lv = lg(a.v);
let log_terms =
|| ((xf / 2.0).ln().abs() + EULER_GAMMA_UP + (nf + 1.0 + xf / 2.0).ln()).log2() + 0.2;
match fun {
LibFn::BesselK => Sens::Deriv(if nu.is_zero() {
f64::NEG_INFINITY
} else {
lv + nf.log2() - lg(x) + 0.2
}),
LibFn::BesselI => Sens::Deriv(lv + log_terms()),
LibFn::BesselJ if xf <= 16.0 => {
let (lg_g, err) = ln_gamma_f64(&sum(nu, &int(1)));
Sens::Deriv(nf * (lg(x) - 1.0) + xf * LOG2E - (lg_g - err) * LOG2E + log_terms())
}
_ => Sens::Numeric,
}
}
fn polylog_sens(i: usize, a: &Args<'_>) -> Sens {
let (s, z) = (a.x[0], a.x[1]);
let e = a.e[1];
let sf = f(s);
if i == 0 {
let w = one_minus(&z.abs());
if !pos(&w) || !inside(lg(&w), a.e[0].max(e)) {
return Sens::Numeric;
}
let m = (2.0 - sf).ceil().max(0.0);
return Sens::Deriv(lg(z) + log2_factorial(m) - (m + 1.0) * (lg(&w) - shrink(e - lg(&w))));
}
let u = one_minus(z);
let exact_int = s.is_int() && a.e[0] == f64::NEG_INFINITY;
if exact_int && sf == 1.0 {
return deriv_if(inside(lg(&u), e), -lg(&u) + BALL);
}
if exact_int && sf == 2.0 && (!pos(z) || inside(lg(&u), e)) {
if lg(z) < -1.0 {
return Sens::Deriv(1.0);
}
let ln_u = (lg(&u) * LN2).abs() + shrink(e - lg(&u)) * LN2;
return Sens::Deriv(ln_u.max(f64::MIN_POSITIVE).log2() - lg(z) + 0.2);
}
if exact_int && sf >= 3.0 && lg(z) <= 0.0 {
return Sens::Deriv(1.0);
}
let w = one_minus(&z.abs());
if !pos(&w) || !inside(lg(&w), e) {
return Sens::Numeric;
}
let m = (1.0 - sf).ceil().max(0.0);
Sens::Deriv(log2_factorial(m) - (m + 1.0) * (lg(&w) - shrink(e - lg(&w))))
}
fn eta_sens(s: &BigFloat) -> Sens {
if neg(s) {
return Sens::Numeric;
}
let d = f(&diff(s, &int(1))).abs();
if d <= 0.25 {
return Sens::Deriv(0.0);
}
let sf = f(s);
let t = (1.0 - sf).exp2();
let bound = t * LN2 * (1.0 / d + 1.0) + (1.0 - t).abs() * (1.0 / (d * d) + 1.0);
Sens::Deriv(bound.log2() + 0.2)
}
fn elliptic_f_sens(i: usize, a: &Args<'_>, cc: &mut Consts) -> Sens {
let (phi, m) = (a.x[0], a.x[1]);
let e = a.e[i];
let rm = a.rm;
let small = lg(phi) < 0.6; let at_phi = (lg(phi) <= 20.0).then(|| {
let p = LP + 32;
let s = phi.sin(p, rm, cc);
one_minus(&m.mul(&s.mul(&s, p, rm), p, rm))
});
let d_min = if !pos(m) {
Some(int(1))
} else if small {
at_phi.clone()
} else {
Some(one_minus(m))
};
let Some(d_min) = d_min.filter(pos) else {
return Sens::Numeric;
};
if i == 0 {
let d = at_phi.filter(pos).unwrap_or(d_min);
return deriv_if(
lg(&d) > lg(m) + e + 3.0,
-0.5 * (lg(&d) - shrink(lg(m) + e - lg(&d))),
);
}
let lphi = lg(phi);
deriv_if(lg(&d_min) > e + 3.0, lphi - 1.0 - 1.5 * (lg(&d_min) - BALL))
}
fn elliptic_pi_sens(i: usize, a: &Args<'_>) -> Sens {
let (n, m) = (a.x[0], a.x[1]);
let (un, um) = (one_minus(n), one_minus(m));
if !pos(&un) || !pos(&um) {
return Sens::Numeric;
}
let (lun, lum) = (lg(&un), lg(&um));
let lc = lum.min(0.0);
let quarter_pi = std::f64::consts::FRAC_PI_4.log2();
if i == 0 {
let two_minus_n = (1.0 + f(&un)).log2();
return deriv_if(
inside(lun, a.e[0]),
quarter_pi + two_minus_n - 1.5 * (lun - shrink(a.e[0] - lun)) - 0.5 * lc,
);
}
deriv_if(
lc == 0.0 || inside(lum, a.e[1]),
quarter_pi - 0.5 * lun - 1.5 * (lc - BALL),
)
}
fn incomplete_gamma(fun: LibFn, i: usize, a: &Args<'_>) -> Sens {
let (s, x) = (a.x[0], a.x[1]);
let lv = lg(a.v);
let sf = f(s);
let lx = lg(x);
if i == 1 {
if !inside(lx, a.e[1]) {
return if sf >= 1.0 {
Sens::Numeric
} else {
Sens::Unbounded
};
}
let r = a.e[1].exp2();
let xf = f(x);
let lxb = if sf < 1.0 {
lx - shrink(a.e[1] - lx)
} else {
lx + swell(a.e[1] - lx)
};
return Sens::Deriv((sf - 1.0) * lxb - (xf - r) * LOG2E);
}
if a.v.is_zero() {
return Sens::Numeric;
}
let ln_x = lx * LN2;
if fun == LibFn::LowerGamma {
if !pos(s) || x.is_zero() {
return Sens::Numeric;
}
let bound = std::f64::consts::E * (ln_x.abs() + (-lg(s)).exp2());
return Sens::Deriv(lv + bound.log2());
}
if x.is_zero() {
return psi_abs(s, a.e[0]).map_or(Sens::Unbounded, |p| Sens::Deriv(lv + p));
}
let l_rho = sf * lx - f(x) * LOG2E - lv;
let l_mean = lsum(lg(s), l_rho);
let bound = ln_x.abs().max((l_mean * LN2).max(0.0)) + 1e-300;
Sens::Deriv(lv + bound.log2() + 0.1)
}
fn bessel_x(fun: LibFn, a: &Args<'_>, cc: &mut Consts) -> Sens {
let (nu, x) = (a.x[0], a.x[1]);
let int_order = nu.is_int();
let entire = matches!(fun, LibFn::BesselJ | LibFn::BesselI) && int_order;
if !entire && !inside(lg(x), a.e[1]) {
return Sens::Unbounded;
}
let lv = lg(a.v);
let lx = lg(x);
let nf = f(nu);
let usable = !neg(nu) || int_order || fun == LibFn::BesselK;
if usable && fun != LibFn::BesselY {
let an = nf.abs();
let ratio = if an == 0.0 {
f64::NEG_INFINITY
} else {
an.log2() - lx + shrink(a.e[1] - lx)
};
return Sens::Deriv(match fun {
LibFn::BesselK => lsum(0.0, (an + 1.0).log2() - lx + shrink(a.e[1] - lx)) + lv + 0.2,
LibFn::BesselI => lsum(0.0, ratio) + lv + 0.2,
_ => {
let (lg_g, err) = ln_gamma_f64(&sum(&int(2), &nu.abs()));
let small = (an + 1.0) * (lx + swell(a.e[1] - lx) - 1.0) - (lg_g - err) * LOG2E;
lsum(small.min(0.0), ratio + lv) + 0.2
}
});
}
let nu1 = diff(nu, &int(1));
let rm = a.rm;
let companion = match fun {
LibFn::BesselJ => super::arb_bessel_j(&nu1, x, LP, rm, cc),
LibFn::BesselY => super::arb_bessel_y(&nu1, x, LP, rm, cc),
LibFn::BesselI => super::arb_bessel_i(&nu1, x, LP, rm, cc),
_ => super::arb_bessel_k(&nu1, x, LP, rm, cc),
};
match companion {
Ok(c) => {
let ratio = if nu.is_zero() {
f64::NEG_INFINITY
} else {
lg(nu) - lg(x) + lg(a.v)
};
Sens::Deriv(lsum(lg(&c), ratio) + 1.0)
}
Err(_) => Sens::Numeric,
}
}
fn betainc_sens(regularized: bool, i: usize, a: &Args<'_>) -> Sens {
let (p, q, x1, x2) = (a.x[0], a.x[1], a.x[2], a.x[3]);
if !pos(p) || !pos(q) {
return Sens::Numeric;
}
let (ln_b_lo, ln_b_hi) = log2_beta_bounds(p, q);
let norm = if regularized { ln_b_lo } else { 0.0 };
if i >= 2 {
let t = a.x[i];
let e = a.e[i];
let u = one_minus(t);
let (pf, qf) = (f(p), f(q));
let (lt, lu) = (lg(t), lg(&u));
if !inside(lt, e) || !inside(lu, e) {
let near_zero = lt <= lu;
let (first, other, l_edge) = if near_zero {
(pf, qf, lt)
} else {
(qf, pf, lu)
};
let ls = lsum(l_edge, e + 3.0);
if ls >= -1.0 {
return Sens::Unbounded;
}
let rest = if other < 1.0 {
(other - 1.0) * (1.0 - ls.exp2()).log2()
} else {
0.0
};
return Sens::Change(first * ls - first.log2() + rest - norm + 1.0);
}
let variation =
((pf - 1.0).abs() * shrink(e - lt) + (qf - 1.0).abs() * shrink(e - lu)) * LN2;
return Sens::Deriv((pf - 1.0) * lt + (qf - 1.0) * lu - norm + variation * LOG2E);
}
let (lo, hi) = if x1.cmp(x2).is_some_and(|c| c > 0) {
(x2, x1)
} else {
(x1, x2)
};
let bound = if i == 0 {
beta_shape(p, q, lo, hi, a.e[0], a.v, regularized, ln_b_hi)
} else {
let (lo1, hi1) = (one_minus(hi), one_minus(lo));
beta_shape(q, p, &lo1, &hi1, a.e[1], a.v, regularized, ln_b_hi)
};
bound.map_or(Sens::Unbounded, |d| Sens::Deriv(lg(a.v) + d))
}
#[allow(clippy::too_many_arguments)]
fn beta_shape(
p: &BigFloat,
q: &BigFloat,
lo: &BigFloat,
hi: &BigFloat,
e: f64,
value: &BigFloat,
regularized: bool,
ln_b_hi: f64,
) -> Option<f64> {
let pq = sum(p, q);
let m = psi_diff(p, &pq, q, e)?;
let one = int(1);
let hi_one = hi.cmp(&one) == Some(0);
let mu = if pos(lo) {
(-lg(lo) * LN2).max(1e-300).log2()
} else if hi_one {
m
} else {
let qf = f(q);
let r = if qf > 1.0 {
(1.0 - qf) * lg(&one_minus(hi))
} else {
0.0
};
r + (-lg(hi) * LN2 + (-lg(p)).exp2()).log2()
};
let v = value.abs();
if !regularized {
let mut d = mu;
if lo.is_zero() && !hi_one {
d = d.min(m + ln_b_hi - lg(&v));
}
return Some(d);
}
if lo.is_zero() && hi_one {
return Some(f64::NEG_INFINITY); }
let mut d = lsum(mu, m);
if lo.is_zero() {
let one_minus_v = one_minus(&v);
d = d.min(m + lg(&one_minus_v) - lg(&v));
} else if hi_one {
d = d.min(m);
}
Some(d)
}
#[allow(clippy::too_many_arguments)]
fn numeric_change(
arena: &Arena,
id: ExprId,
child: ExprId,
e: ErrExp,
value: &Complex,
cache: &FxHashMap<ExprId, Complex>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Option<ErrExp> {
let v = cache.get(&child)?;
let floor = accuracy::mag(v).map_or(e, |m| (m - prec as i64 + 2) as f64);
let pe = e.max(floor).ceil();
if !pe.is_finite() || pe.abs() > 2e9 {
return None;
}
let mut delta = BigFloat::from_i32(1, LP);
delta.set_exponent(i32::try_from(pe as i64 + 1).ok()?);
let mut local: FxHashMap<ExprId, Complex> = FxHashMap::default();
for c in arena.node(id).children() {
local.insert(c, cache.get(&c)?.clone());
}
for moved in [sum(&v.0, &delta), diff(&v.0, &delta)] {
local.insert(child, (moved, v.1.clone()));
if let Ok(w) = super::eval_node(arena, id, &local, prec, rm, cc) {
if [&w.0, &w.1].iter().any(|p| p.is_nan() || p.is_inf()) {
continue;
}
let hp = prec + 64;
let d = c_sub(&w, value, hp, rm);
return Some(if accuracy::mag(&d).is_none() {
EXACT
} else {
accuracy::lg_abs(&d) - (pe - e) + 1.0
});
}
}
None
}
#[allow(clippy::too_many_arguments)]
pub(super) fn special_error(
arena: &Arena,
id: ExprId,
value: &Complex,
cache: &FxHashMap<ExprId, Complex>,
errs: &FxHashMap<ExprId, accuracy::Bound>,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> ErrExp {
let node = arena.node(id);
let kids = node.children();
let mut x = Vec::with_capacity(kids.len());
let mut e = Vec::with_capacity(kids.len());
let mut joint = Vec::with_capacity(kids.len());
for c in &kids {
let (Some(v), Some(b)) = (cache.get(c), errs.get(c)) else {
return UNKNOWN;
};
if b.is_unknown() {
return UNKNOWN;
}
x.push(&v.0);
joint.push(b.joint());
e.push(radius(b.joint()));
}
if joint.iter().all(|&j| accuracy::is_exact(j)) {
return EXACT;
}
let args = Args {
x,
e,
v: &value.0,
rm,
};
let mut contributions: Vec<ErrExp> = Vec::new();
let mut seen: Vec<ExprId> = Vec::new();
for (i, &c) in kids.iter().enumerate() {
if accuracy::is_exact(joint[i]) || seen.contains(&c) {
continue;
}
seen.push(c);
let mut total = f64::NEG_INFINITY;
let mut change = f64::NEG_INFINITY;
let mut numeric = false;
for (j, _) in kids.iter().enumerate().filter(|&(_, &k)| k == c) {
match analytic(arena, node, j, &args, cc) {
Sens::Zero => {}
Sens::Deriv(d) if d.is_nan() => numeric = true,
Sens::Deriv(d) => total = lsum(total, d),
Sens::Change(d) if d.is_nan() => numeric = true,
Sens::Change(d) => change = lsum(change, d),
Sens::Numeric => numeric = true,
Sens::Unbounded => return UNKNOWN,
}
}
if numeric {
match numeric_change(arena, id, c, joint[i], value, cache, prec, rm, cc) {
Some(k) => contributions.push(k),
None => return UNKNOWN,
}
continue;
}
let l = lsum(total + args.e[i], change);
if l == f64::NEG_INFINITY {
continue;
}
if !l.is_finite() || l > 1e15 {
return UNKNOWN;
}
contributions.push(l);
}
let total = contributions.into_iter().fold(EXACT, accuracy::lsum);
accuracy::shift(total, 1.0)
}