use astro_float::{BigFloat, Consts, RoundingMode};
use tracing::debug;
use super::accuracy::{self, ErrExp};
use super::quad::{Integrand, scale2, small};
use super::{Settled, ZeroSearch, evalf_settled};
use crate::base::arena::Arena;
use crate::base::bigcomplex::c_sub;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
const PREC: usize = 128;
const EXPONENTS: [i32; 9] = [16, 24, 32, 64, 128, 256, 512, 1024, 2048];
const F64_END: i32 = 1024;
const F64_LAST: i32 = 1000;
const AGREEMENT: f64 = -20.0;
const NEGLIGIBLE: ErrExp = -1.0e5;
fn segment(e0: i32, g0: ErrExp, e1: i32, g1: ErrExp) -> (ErrExp, f64) {
let span = f64::from(e1 - e0) * std::f64::consts::LN_2;
match (accuracy::is_exact(g0), accuracy::is_exact(g1)) {
(true, true) => (accuracy::EXACT, f64::INFINITY),
(false, true) => (g0 + span.log2(), f64::INFINITY),
(true, false) => (g1 + span.log2(), -1.0),
(false, false) => {
let alpha = (g0 - g1) * std::f64::consts::LN_2 / span;
let mass = if alpha > 0.0 {
g0 + span.min(1.0 / alpha).log2()
} else {
g1 + span.log2()
};
(mass, alpha)
}
}
}
pub(super) fn f64_tail_trusted(
arena: &Arena,
body: ExprId,
var: ExprId,
upper: bool,
compiled: &dyn Fn(f64) -> f64,
allowed: f64,
cc: &mut Consts,
) -> bool {
if allowed.is_nan() || allowed < 0.0 {
return false;
}
let lg_allowed = allowed.log2() - 3.0;
let rm = RoundingMode::ToEven;
let mut f = Integrand::new(arena, body, var);
let mut sample = |e: i32| -> Option<(ErrExp, (BigFloat, BigFloat))> {
let x = scale2(&small(1), e);
let x = if upper { x } else { x.neg() };
let e_bits = usize::try_from(e).unwrap_or(0);
let mut found = None;
for prec in [PREC, 2 * e_bits + PREC, 4 * e_bits + PREC] {
let evaluated = f.eval(&x, accuracy::EXACT, prec, rm, cc);
match evaluated {
Ok((v, b))
if !(b.is_unknown()
|| v.0.is_nan()
|| v.1.is_nan()
|| v.0.is_inf()
|| v.1.is_inf()) =>
{
found = Some((v, b));
break;
}
other => debug!(e, upper, prec, ?other, "evalf: a far-tail sample failed"),
}
}
let (v, b) = found?;
let size = accuracy::lsum(accuracy::lg_abs(&v), b.joint());
let g = accuracy::shift(size, f64::from(e));
debug!(e, upper, g, "evalf: the far tail of an f64 quadrature");
Some((if g < NEGLIGIBLE { accuracy::EXACT } else { g }, v))
};
let mut samples: Vec<(i32, ErrExp, (BigFloat, BigFloat))> = Vec::new();
let mut mid = accuracy::EXACT;
let mut far = accuracy::EXACT;
for &e in &EXPONENTS {
let Some((g, v)) = sample(e) else {
return false;
};
if let Some(&(pe, pg, _)) = samples.last() {
let (mass, alpha) = segment(pe, pg, e, g);
if pe < F64_END {
mid = accuracy::lsum(mid, mass);
} else {
far = accuracy::lsum(far, mass);
}
let rest = if accuracy::is_exact(g) {
accuracy::EXACT
} else if alpha > 0.0 {
g - alpha.log2()
} else {
accuracy::UNKNOWN
};
if accuracy::lsum(accuracy::lsum(mid, far), rest) <= lg_allowed {
return true;
}
if e == EXPONENTS[EXPONENTS.len() - 1] {
far = accuracy::lsum(far, rest);
}
}
samples.push((e, g, v));
}
if far > lg_allowed {
return false;
}
let Some((g_last, v_last)) = sample(F64_LAST) else {
return false;
};
let span = f64::from(F64_LAST - EXPONENTS[0]) * std::f64::consts::LN_2;
let floor = lg_allowed - span.log2();
samples.retain(|s| s.0 < F64_END);
samples.push((F64_LAST, g_last, v_last));
for (e, g, v) in &samples {
if *g <= floor {
continue;
}
let t = f64::from(*e).exp2();
let c = compiled(if upper { t } else { -t });
if !c.is_finite() {
return false;
}
let d = c_sub(
v,
&(BigFloat::from_f64(c, 64), BigFloat::new(PREC)),
PREC,
rm,
);
let diff = accuracy::shift(accuracy::lg_abs(&d), f64::from(*e));
if diff > (g + AGREEMENT).max(floor) {
return false;
}
}
true
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Approach {
Above(ExprId),
Below(ExprId),
PosInf,
NegInf,
}
const MAX_NODES: usize = 80;
pub(super) fn divergence(
arena: &mut Arena,
body: ExprId,
var: ExprId,
approaches: &[Approach],
) -> Option<String> {
if walk::post_order_ids(arena, body).len() > MAX_NODES {
return None;
}
let u = crate::transforms::subs::fresh_symbol(arena, "__improper_u", &[body]);
let inf = arena.infinity();
let one = arena.one();
let ln_u = arena.ln(u);
let inv_u = arena.div(one, u);
for &approach in approaches {
let (fu, scale, shown) = match approach {
Approach::PosInf => (
crate::transforms::subs::subs(arena, body, var, u),
u,
"∞".to_string(),
),
Approach::NegInf => {
let neg = arena.neg(u);
(
crate::transforms::subs::subs(arena, body, var, neg),
u,
"−∞".to_string(),
)
}
Approach::Above(c) | Approach::Below(c) => {
let x = match approach {
Approach::Above(_) => arena.add(&[c, inv_u]),
_ => arena.sub(c, inv_u),
};
let side = if matches!(approach, Approach::Above(_)) {
"⁺"
} else {
"⁻"
};
(
crate::transforms::subs::subs(arena, body, var, x),
inv_u,
format!("{}{side}", arena.display(c)),
)
}
};
let first = arena.mul(&[fu, scale]);
let second = arena.mul(&[fu, scale, ln_u]);
for (h, test) in [
(first, "comparison with 1/x"),
(second, "comparison with 1/(x ln x)"),
] {
let h = crate::transforms::eval::eval(arena, h);
let Ok(l) = arena.limit_expr(h, u, inf) else {
continue;
};
if nonzero_limit(arena, l) {
return Some(format!(
"the integrand is not integrable as x → {shown} ({test}: the limit is {})",
arena.display(l)
));
}
}
}
None
}
fn nonzero_limit(arena: &Arena, l: ExprId) -> bool {
if l == arena.infinity() || l == arena.neg_infinity() {
return true;
}
if !walk::free_symbols(arena, l).is_empty() || walk::has_unevaluated(arena, l) {
return false;
}
let undecided = walk::post_order_ids(arena, l).iter().any(|&id| {
let node = arena.node(id);
node.is_set_node()
|| matches!(
node,
ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
| ExprNode::NaN
)
});
if undecided {
return false;
}
matches!(
evalf_settled(arena, l, 16, ZeroSearch::Number),
Ok((z, Settled::Certified)) if !(z.0.is_zero() && z.1.is_zero())
)
}