use astro_float::{BigFloat, Consts, RoundingMode};
use num_bigint::BigInt;
use num_rational::Ratio;
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::debug;
use super::accuracy::{self, Bound, ErrExp};
use super::{
ErrMap, eval_node_with_error, evaluate_tree, exponent_range_error, is_condition, root_cause,
};
use crate::base::arena::Arena;
use crate::base::bigcomplex::{Complex, c_add, c_sub, c_zero};
use crate::base::errors::SymplexError;
use crate::base::node::ExprId;
use crate::base::walk;
const TARGET_GUARD: usize = 48;
const EVAL_GUARD: usize = 16;
const NODE_GUARD: usize = 24;
const SUM_GUARD: usize = 16;
const MIN_LEVEL: u32 = 4;
const MAX_LEVEL: u32 = 12;
const T_MIN_HALVES: u64 = 5;
const T_MAX_HALVES: u64 = 24;
const MAX_EVALS: usize = 30_000;
const MAX_PIECES: usize = 64;
const MAX_DEPTH: u32 = 8;
const FLOOR_SHARE: f64 = 6.0;
const NODE_SHARE: f64 = 18.0;
const MAX_GL_DEGREE: u32 = 7;
const MIN_GL_DEGREE: u32 = 4;
const GL_CACHE: usize = 8;
const MAX_PRECISIONS: usize = 24;
const TIME_LIMIT: std::time::Duration = std::time::Duration::from_secs(20);
struct Integrand<'a> {
arena: &'a Arena,
root: ExprId,
var: ExprId,
dependent: Vec<ExprId>,
frontier: Vec<(ExprId, Vec<ExprId>)>,
scratch: FxHashMap<usize, (FxHashMap<ExprId, Complex>, ErrMap)>,
}
impl<'a> Integrand<'a> {
fn new(arena: &'a Arena, root: ExprId, var: ExprId) -> Self {
let post = walk::post_order_ids(arena, root);
let mut dep: FxHashSet<ExprId> = FxHashSet::default();
for &id in &post {
if id == var || arena.node(id).children().iter().any(|c| dep.contains(c)) {
dep.insert(id);
}
}
let mut dependent = Vec::new();
let mut frontier = Vec::new();
let mut seen: FxHashSet<ExprId> = FxHashSet::default();
if !dep.contains(&root) {
frontier.push((root, post.clone()));
}
for &id in &post {
if !dep.contains(&id) {
continue;
}
let node = arena.node(id);
if id != var && (id == root || !is_condition(node)) {
dependent.push(id);
}
let mut stack: Vec<ExprId> = node.children().to_vec();
while let Some(c) = stack.pop() {
if dep.contains(&c) {
continue;
}
let child = arena.node(c);
if is_condition(child) {
stack.extend(child.children());
continue;
}
if seen.insert(c) {
frontier.push((c, walk::post_order_ids(arena, c)));
}
}
}
Integrand {
arena,
root,
var,
dependent,
frontier,
scratch: FxHashMap::default(),
}
}
fn eval(
&mut self,
x: &BigFloat,
x_err: ErrExp,
prec: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Result<(Complex, Bound), SymplexError> {
if !self.scratch.contains_key(&prec) {
if self.scratch.len() >= MAX_PRECISIONS {
self.scratch.clear();
}
let mut cache: FxHashMap<ExprId, Complex> = FxHashMap::default();
let mut errs = ErrMap::default();
for (c, order) in &self.frontier {
if let Ok((v, b)) = evaluate_tree(self.arena, *c, order, None, false, prec, rm, cc)
{
cache.insert(*c, v);
errs.insert(*c, b);
}
}
self.scratch.insert(prec, (cache, errs));
}
let Some((cache, errs)) = self.scratch.get_mut(&prec) else {
return Err(internal("integrand cache"));
};
cache.insert(self.var, (x.clone(), BigFloat::new(prec)));
errs.insert(self.var, Bound::real(x_err));
let mut failed: FxHashMap<ExprId, SymplexError> = FxHashMap::default();
for &id in &self.dependent {
let evaluated = eval_node_with_error(self.arena, id, cache, errs, prec, rm, cc)
.and_then(
|(v, b)| match exponent_range_error(self.arena, id, &v, cache, errs) {
Some(e) => Err(e),
None => Ok((v, b)),
},
);
match evaluated {
Ok((v, b)) => {
cache.insert(id, v);
errs.insert(id, b);
}
Err(e) => {
cache.remove(&id);
errs.remove(&id);
let e = root_cause(self.arena.node(id), e, &failed);
if id == self.root {
return Err(e);
}
failed.insert(id, e);
}
}
}
let value = cache
.get(&self.root)
.cloned()
.ok_or_else(|| SymplexError::Unevaluable {
reason: "the integrand has no value at a quadrature node".into(),
})?;
let bound = errs.get(&self.root).copied().unwrap_or(Bound::UNKNOWN);
Ok((value, bound))
}
}
fn internal(what: &str) -> SymplexError {
SymplexError::ComputationFailed {
operation: "evalf",
reason: format!("quadrature: missing {what}"),
}
}
#[derive(Clone, Debug)]
pub(super) enum End {
Rational(Ratio<BigInt>),
Float(BigFloat),
Expr(ExprId),
Value(BigFloat, ErrExp),
}
impl End {
fn same(&self, other: &End) -> bool {
match (self, other) {
(End::Rational(a), End::Rational(b)) => a == b,
(End::Expr(a), End::Expr(b)) => a == b,
(End::Float(a), End::Float(b)) => a.cmp(b) == Some(0),
_ => false,
}
}
}
#[derive(Clone, Debug)]
pub(super) enum Piece {
Finite(End, End),
Upper(End),
Lower(End),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Side {
Lo,
Hi,
}
struct TsNode {
delta: BigFloat,
w: BigFloat,
rel: ErrExp,
}
struct Placed {
x: BigFloat,
x_err: ErrExp,
w: BigFloat,
w_rel: ErrExp,
need: usize,
which: Option<usize>,
}
struct Term {
value: Complex,
err: ErrExp,
real: bool,
}
struct PieceOut {
value: Complex,
err: ErrExp,
l1: ErrExp,
converged: bool,
discretisation: bool,
real: bool,
digits: f64,
}
pub(super) struct RunOut {
pub(super) value: Complex,
pub(super) err: ErrExp,
pub(super) real: bool,
pub(super) converged: bool,
pub(super) discretisation: bool,
}
fn lg(x: &BigFloat) -> f64 {
accuracy::part_lg(x)
}
fn ulp2(x: &BigFloat, p: usize) -> ErrExp {
accuracy::part_mag(x).map_or(accuracy::EXACT, |m| (m - p as i64 + 1) as f64)
}
fn dyadic(j: u64, k: u32) -> BigFloat {
let mut t = BigFloat::from_u64(j, 64);
if let Some(e) = t.exponent() {
t.set_exponent(e - k as i32);
}
t
}
fn scale2(x: &BigFloat, s: i32) -> BigFloat {
let mut y = x.clone();
if !y.is_zero()
&& let Some(e) = y.exponent()
{
y.set_exponent(e + s);
}
y
}
fn small(n: u64) -> BigFloat {
BigFloat::from_u64(n, 64)
}
fn quantize(p: usize) -> usize {
p.div_ceil(32) * 32
}
pub(super) struct Quad<'a> {
f: Integrand<'a>,
variants: Vec<(End, bool, Integrand<'a>)>,
prec: usize,
pe: usize,
p_cap: usize,
rm: RoundingMode,
evals_left: usize,
evals_total: usize,
deadline: std::time::Instant,
ends: FxHashMap<(ExprId, usize), (BigFloat, ErrExp)>,
nodes: FxHashMap<(u32, u64), std::rc::Rc<TsNode>>,
abs_floor: ErrExp,
}
impl<'a> Quad<'a> {
pub(super) fn new(
arena: &'a Arena,
body: ExprId,
var: ExprId,
variants: Vec<(End, bool, ExprId)>,
prec: usize,
rm: RoundingMode,
) -> Self {
let pe = prec + EVAL_GUARD;
let max = arena.config.max_evalf_precision as usize;
Quad {
f: Integrand::new(arena, body, var),
variants: variants
.into_iter()
.map(|(e, plus, b)| (e, plus, Integrand::new(arena, b, var)))
.collect(),
prec,
pe,
p_cap: (12 * pe + 1024).min(max.max(pe)),
rm,
evals_left: MAX_EVALS,
evals_total: MAX_EVALS,
deadline: std::time::Instant::now() + TIME_LIMIT,
ends: FxHashMap::default(),
nodes: FxHashMap::default(),
abs_floor: accuracy::EXACT,
}
}
pub(super) fn lighten(&mut self) {
self.evals_left = MAX_EVALS / 4;
self.evals_total = MAX_EVALS / 4;
self.deadline = std::time::Instant::now() + TIME_LIMIT / 8;
}
pub(super) fn end_value(
&mut self,
e: &End,
p: usize,
cc: &mut Consts,
) -> Result<(BigFloat, ErrExp), SymplexError> {
match e {
End::Rational(q) => {
let v = crate::base::numeric::ratio_to_bigfloat(q, p, self.rm);
let err = accuracy::rational_part(q, &v, p);
Ok((v, err))
}
End::Float(v) => Ok((v.clone(), accuracy::EXACT)),
End::Value(v, err) => Ok((v.clone(), *err)),
End::Expr(id) => {
if let Some(hit) = self.ends.get(&(*id, p)) {
return Ok(hit.clone());
}
let order = walk::post_order_ids(self.f.arena, *id);
let (v, b) = evaluate_tree(self.f.arena, *id, &order, None, true, p, self.rm, cc)?;
if !accuracy::exact_zero(&v.1, b.im) || b.is_unknown() {
return Err(SymplexError::Unevaluable {
reason: "integration bounds of a definite integral must be real".into(),
});
}
let out = (v.0, b.re);
self.ends.insert((*id, p), out.clone());
Ok(out)
}
}
}
fn variant(&self, end: &End, plus: bool) -> Option<usize> {
self.variants
.iter()
.position(|(e, p, _)| *p == plus && e.same(end))
}
fn ts_node(&mut self, j: u64, k: u32, pw: usize, cc: &mut Consts) -> std::rc::Rc<TsNode> {
if let Some(n) = self.nodes.get(&(k, j)) {
return n.clone();
}
let n = std::rc::Rc::new(self.compute_ts_node(j, k, pw, cc));
self.nodes.insert((k, j), n.clone());
n
}
fn compute_ts_node(&self, j: u64, k: u32, pw: usize, cc: &mut Consts) -> TsNode {
let rm = self.rm;
let t = dyadic(j, k);
let pi = cc.pi(pw, rm);
let s = t.sinh(pw, rm, cc);
let c = t.cosh(pw, rm, cc);
let arg = pi.mul(&s, pw, rm); let q = arg.neg().exp(pw, rm, cc); let opq = small(1).add(&q, pw, rm);
let delta = scale2(&q, 1).div(&opq, pw, rm); let w = scale2(&pi.mul(&c, pw, rm).mul(&q, pw, rm), 1).div(&opq.mul(&opq, pw, rm), pw, rm);
let a = (j as f64 * (-(k as f64)).exp2()).sinh() * std::f64::consts::PI;
let rel = (1.0 + a).log2() + 4.0 - pw as f64;
TsNode { delta, w, rel }
}
#[allow(clippy::too_many_arguments)]
fn place(
&mut self,
piece: &Piece,
half: &(BigFloat, ErrExp),
side: Option<Side>,
node: Option<&TsNode>,
k: u32,
p: usize,
cc: &mut Consts,
) -> Result<Placed, SymplexError> {
let rm = self.rm;
let pw = self.pe + NODE_GUARD;
let step = -(k as i32);
let (c, c_err) = half;
let c_rel = if c.is_zero() {
accuracy::EXACT
} else {
accuracy::shift(*c_err, -lg(c))
};
let need_for = |end: &BigFloat, dist: &BigFloat, pe: usize| -> usize {
match (accuracy::part_mag(end), accuracy::part_mag(dist)) {
(Some(me), Some(md)) if me > md => pe + (me - md) as usize + 8,
_ => pe,
}
};
let pe = self.pe;
let one = small(1);
let two = small(2);
let (Some(node), Some(side)) = (node, side) else {
let pi_half = scale2(&cc.pi(pw, rm), -1);
return Ok(match piece {
Piece::Finite(a, _) => {
let (av, ae) = self.end_value(a, p, cc)?;
let x = av.add(c, p, rm);
Placed {
x_err: accuracy::lsum(accuracy::lsum(ae, *c_err), ulp2(&x, p)),
x,
w: scale2(&c.mul(&pi_half, pw, rm), step),
w_rel: accuracy::lsum(c_rel, 2.0 - pw as f64),
need: p,
which: None,
}
}
Piece::Upper(a) => {
let (av, ae) = self.end_value(a, p, cc)?;
let x = av.add(&one, p, rm);
Placed {
x_err: accuracy::lsum(ae, ulp2(&x, p)),
x,
w: scale2(&pi_half, step + 1),
w_rel: 2.0 - pw as f64,
need: p,
which: None,
}
}
Piece::Lower(b) => {
let (bv, be) = self.end_value(b, p, cc)?;
let x = bv.sub(&one, p, rm);
Placed {
x_err: accuracy::lsum(be, ulp2(&x, p)),
x,
w: scale2(&pi_half, step + 1),
w_rel: 2.0 - pw as f64,
need: p,
which: None,
}
}
});
};
let d = &node.delta;
let base_rel = node.rel + 3.0;
Ok(match (piece, side) {
(Piece::Finite(a, _), Side::Lo) | (Piece::Finite(_, a), Side::Hi) => {
let dist = c.mul(d, pw, rm);
if let Some(i) = self.variant(a, side == Side::Lo) {
return Ok(Placed {
x_err: ulp2(&dist, pw),
x: dist,
w: scale2(&c.mul(&node.w, pw, rm), step),
w_rel: accuracy::lsum(c_rel, base_rel),
need: p,
which: Some(i),
});
}
let (av, ae) = self.end_value(a, p, cc)?;
let x = if side == Side::Lo {
av.add(&dist, p, rm)
} else {
av.sub(&dist, p, rm)
};
Placed {
x_err: accuracy::lsum(ae, ulp2(&x, p)),
need: need_for(&av, &dist, pe),
x,
w: scale2(&c.mul(&node.w, pw, rm), step),
w_rel: accuracy::lsum(c_rel, base_rel),
which: None,
}
}
(Piece::Upper(a), Side::Lo) | (Piece::Lower(a), Side::Hi) => {
let tmd = two.sub(d, pw, rm);
let dist = d.div(&tmd, pw, rm);
let factor = scale2(&one.div(&tmd.mul(&tmd, pw, rm), pw, rm), 1);
if let Some(i) = self.variant(a, side == Side::Lo) {
return Ok(Placed {
x_err: ulp2(&dist, pw),
x: dist,
w: scale2(&node.w.mul(&factor, pw, rm), step),
w_rel: base_rel + 1.0,
need: p,
which: Some(i),
});
}
let (av, ae) = self.end_value(a, p, cc)?;
let x = if side == Side::Lo {
av.add(&dist, p, rm)
} else {
av.sub(&dist, p, rm)
};
Placed {
x_err: accuracy::lsum(ae, ulp2(&x, p)),
need: need_for(&av, &dist, pe),
x,
w: scale2(&node.w.mul(&factor, pw, rm), step),
w_rel: base_rel + 1.0,
which: None,
}
}
(Piece::Upper(a), Side::Hi) | (Piece::Lower(a), Side::Lo) => {
let far = scale2(&one.div(d, pw, rm), 1);
let (av, ae) = self.end_value(a, p, cc)?;
let x = if side == Side::Hi {
av.sub(&one, p, rm).add(&far, p, rm)
} else {
av.add(&one, p, rm).sub(&far, p, rm)
};
let factor = scale2(&one.div(&d.mul(d, pw, rm), pw, rm), 1);
Placed {
x_err: accuracy::lsum(ae, ulp2(&x, p)),
need: p,
x,
w: scale2(&node.w.mul(&factor, pw, rm), step),
w_rel: base_rel + 1.0,
which: None,
}
}
})
}
#[allow(clippy::too_many_arguments)]
fn term(
&mut self,
piece: &Piece,
half: &(BigFloat, ErrExp),
side: Option<Side>,
node: Option<&TsNode>,
k: u32,
budget: ErrExp,
cc: &mut Consts,
) -> Result<Term, SymplexError> {
let rm = self.rm;
let psum = self.pe + SUM_GUARD;
let mut p = self.pe;
loop {
if self.evals_left == 0 {
return Err(not_converged("the evaluation budget was exhausted"));
}
if std::time::Instant::now() > self.deadline {
return Err(not_converged("the time limit was reached"));
}
self.evals_left -= 1;
let placed = self.place(piece, half, side, node, k, p, cc)?;
let f = match placed.which.and_then(|i| self.variants.get_mut(i)) {
Some((_, _, g)) => g,
None => &mut self.f,
};
let evaluated = f.eval(&placed.x, placed.x_err, p, rm, cc);
let next = match evaluated {
Ok((fv, fb))
if !fb.is_unknown()
&& !(fv.0.is_nan() || fv.0.is_inf() || fv.1.is_nan() || fv.1.is_inf()) =>
{
let value = (fv.0.mul(&placed.w, psum, rm), fv.1.mul(&placed.w, psum, rm));
let size = accuracy::lg_abs(&value);
let err = accuracy::lsum(
accuracy::lsum(
accuracy::shift(fb.joint(), lg(&placed.w)),
accuracy::shift(placed.w_rel, size),
),
accuracy::shift(1.0 - psum as f64, size),
);
let target = budget.max(size - (self.pe as f64 - 8.0));
let real = accuracy::exactly_real(&fv, fb);
if err <= target || p >= self.p_cap {
return Ok(Term { value, err, real });
}
let deficit = (err - target).clamp(0.0, 1e6).ceil() as usize;
placed.need.max(p + deficit + 16)
}
Ok(_) | Err(_) if p >= self.p_cap => {
let why = match evaluated {
Err(e) => format!(": {e}"),
Ok(_) => String::new(),
};
return Err(not_converged(&format!(
"the integrand has no error bound at a node (x ≈ {}) even at {} bits{why}",
super::bigfloat_to_f64_rounded(&placed.x, rm)
.map_or_else(|_| "?".to_string(), |v| format!("{v:e}")),
self.p_cap
)));
}
_ => placed.need.max(2 * p),
};
p = quantize(next).max(p + 32).min(self.p_cap);
}
}
fn piece(&mut self, piece: &Piece, cc: &mut Consts) -> Result<PieceOut, SymplexError> {
let rm = self.rm;
let psum = self.pe + SUM_GUARD;
let pw = self.pe + NODE_GUARD;
let half = match piece {
Piece::Finite(a, b) => {
let (av, ae) = self.end_value(a, pw, cc)?;
let (bv, be) = self.end_value(b, pw, cc)?;
let c = scale2(&bv.sub(&av, pw, rm), -1);
let err = accuracy::lsum(accuracy::lsum(ae, be), ulp2(&c, pw));
(c, err)
}
_ => (small(1), accuracy::EXACT),
};
let mut sum = c_zero(psum);
let mut l1 = accuracy::EXACT; let mut rerr = accuracy::EXACT; let mut real = true;
let mut additions = 0usize;
let centre = self.term(piece, &half, None, None, 1, accuracy::EXACT, cc)?;
real &= centre.real;
l1 = accuracy::lsum(
l1,
accuracy::lsum(accuracy::lg_abs(¢re.value), centre.err),
);
rerr = accuracy::lsum(rerr, centre.err);
sum = c_add(&sum, ¢re.value, psum, rm);
additions += 1;
let mut sizes: [Vec<ErrExp>; 2] = [Vec::new(), Vec::new()];
for (si, side) in [Side::Lo, Side::Hi].into_iter().enumerate() {
let mut quiet = 0;
let mut j = 1u64;
loop {
if j > T_MAX_HALVES {
return Err(not_converged(NOT_DECAYING));
}
let node = self.ts_node(j, 1, pw, cc);
let budget = accuracy::shift(l1, -((self.pe as f64) + 8.0));
let t = self.term(piece, &half, Some(side), Some(node.as_ref()), 1, budget, cc)?;
real &= t.real;
let size = accuracy::lsum(accuracy::lg_abs(&t.value), t.err);
l1 = accuracy::lsum(l1, size);
rerr = accuracy::lsum(rerr, t.err);
sum = c_add(&sum, &t.value, psum, rm);
additions += 1;
sizes[si].push(size);
let threshold = accuracy::shift(l1, -((self.pe as f64) + 8.0));
if size <= threshold || accuracy::is_exact(size) {
quiet += 1;
} else {
quiet = 0;
}
if quiet >= 2 && j >= T_MIN_HALVES {
break;
}
j += 1;
}
}
let threshold = accuracy::shift(l1, -((self.pe as f64) + 8.0));
let mut cut = [0u64; 2];
let mut tail = accuracy::EXACT;
for (si, s) in sizes.iter().enumerate() {
let mut first = s.len();
while first > 0 && (s[first - 1] <= threshold || accuracy::is_exact(s[first - 1])) {
first -= 1;
}
cut[si] = (first as u64 + 1).min(s.len() as u64);
for &v in &s[first.min(s.len())..] {
tail = accuracy::lsum(tail, v);
}
}
let ends = cut;
let mut prev = sum.clone();
let mut d_hist: Vec<ErrExp> = Vec::new();
let mut out_err = accuracy::UNKNOWN;
let mut converged = false;
let mut discretisation = true;
let mut digits = 0.0f64;
for k in 2..=MAX_LEVEL {
let new_nodes: u64 = ends.iter().map(|&e| e << (k - 2)).sum();
if new_nodes as usize > self.evals_left {
debug!(
k,
"quadrature: the next level exceeds the evaluation budget"
);
break;
}
let budget = accuracy::shift(
accuracy::shift(l1, -1.0),
-((self.prec - TARGET_GUARD.min(self.prec)) as f64) - NODE_SHARE,
)
.max(accuracy::shift(self.abs_floor, -NODE_SHARE));
let mut level = c_zero(psum);
let mut level_l1 = accuracy::EXACT;
let mut level_err = accuracy::EXACT;
for (si, side) in [Side::Lo, Side::Hi].into_iter().enumerate() {
let top = ends[si] << (k - 1);
let mut j = 1u64;
while j <= top {
let node = self.ts_node(j, k, pw, cc);
let t =
self.term(piece, &half, Some(side), Some(node.as_ref()), k, budget, cc)?;
real &= t.real;
level_l1 =
accuracy::lsum(level_l1, accuracy::lsum(accuracy::lg_abs(&t.value), t.err));
level_err = accuracy::lsum(level_err, t.err);
level = c_add(&level, &t.value, psum, rm);
additions += 1;
j += 2;
}
}
let halved = (scale2(&prev.0, -1), scale2(&prev.1, -1));
sum = c_add(&halved, &level, psum, rm);
additions += 1;
l1 = accuracy::lsum(accuracy::shift(l1, -1.0), level_l1);
rerr = accuracy::lsum(accuracy::shift(rerr, -1.0), level_err);
let diff = accuracy::lg_abs(&c_sub(&sum, &prev, psum, rm));
let rounding = accuracy::shift(l1, (additions as f64).log2() + 1.0 - psum as f64);
let r = accuracy::lsum(rerr, rounding);
let e = accuracy::lsum(accuracy::lsum(diff, r), tail);
out_err = e;
prev = sum.clone();
let rel_tol = accuracy::shift(l1, -((self.prec - TARGET_GUARD.min(self.prec)) as f64))
.max(self.abs_floor);
let tol = rel_tol.max(accuracy::shift(r, 1.0));
debug!(k, diff, tol, r, tail, "quadrature: level done");
if k >= MIN_LEVEL && diff <= tol && tail <= tol - 2.0 {
converged = true;
digits = digits.max(l1 - diff);
break;
}
if r > rel_tol && diff <= tol {
discretisation = false;
}
d_hist.push(diff - l1);
digits = digits.max(l1 - diff);
if self.hopeless(k, &d_hist, rel_tol - l1, &ends) {
break;
}
}
Ok(PieceOut {
value: sum,
err: out_err,
l1,
converged,
discretisation,
real,
digits,
})
}
fn hopeless(&self, k: u32, rel: &[ErrExp], target: ErrExp, ends: &[u64; 2]) -> bool {
let Some(&last) = rel.last() else {
return false;
};
let next_cost: u64 = ends.iter().map(|&e| e << k.saturating_sub(1)).sum();
if next_cost as usize > self.evals_left {
return true;
}
if k < 6 {
return false;
}
let digits = -last;
let needed = -target;
if digits >= needed {
return false;
}
if digits < 8.0 {
return true;
}
let levels = (needed / digits).log2().ceil().max(1.0) as u32;
if levels > MAX_LEVEL.saturating_sub(k) {
return true;
}
if rel.len() >= 2 {
let before = -rel[rel.len() - 2];
if before > 0.0 && digits < 1.3 * before {
return true;
}
}
false
}
fn gl_nodes(&self, m: u32, pw: usize) -> GlNodes {
let hit = GL_NODES.with(|c| {
c.borrow()
.iter()
.find(|e| e.0 == (m, pw))
.map(|e| e.1.clone())
});
if let Some(n) = hit {
return n;
}
let nodes = std::rc::Rc::new(gauss_legendre(m, pw, self.rm));
GL_NODES.with(|c| {
let mut c = c.borrow_mut();
if c.len() >= GL_CACHE {
c.remove(0);
}
c.push(((m, pw), nodes.clone()));
});
nodes
}
fn gl_piece(&mut self, piece: &Piece, cc: &mut Consts) -> Result<PieceOut, SymplexError> {
let rm = self.rm;
let pe = self.pe;
let psum = pe + SUM_GUARD;
let pw = pe + NODE_GUARD;
let one = small(1);
let (centre, c, end_err, upper) = match piece {
Piece::Finite(a, b) => {
let (av, ae) = self.end_value(a, pw, cc)?;
let (bv, be) = self.end_value(b, pw, cc)?;
let c = scale2(&bv.sub(&av, pw, rm), -1);
let mid = scale2(&av.add(&bv, pw, rm), -1);
let err = accuracy::lsum(accuracy::lsum(ae, be), ulp2(&mid, pw));
(mid, c, err, None)
}
Piece::Upper(a) => {
let (av, ae) = self.end_value(a, pw, cc)?;
(av.sub(&one, pw, rm), one.clone(), ae, Some(true))
}
Piece::Lower(b) => {
let (bv, be) = self.end_value(b, pw, cc)?;
(bv.add(&one, pw, rm), one.clone(), be, Some(false))
}
};
let rel_tol_bits = (self.prec - TARGET_GUARD.min(self.prec)) as f64;
let mut prev: Option<Complex> = None;
let mut out = PieceOut {
value: c_zero(psum),
err: accuracy::UNKNOWN,
l1: accuracy::EXACT,
converged: false,
discretisation: true,
real: true,
digits: 0.0,
};
let mut rel_hist: Vec<ErrExp> = Vec::new();
for m in 2..=MAX_GL_DEGREE {
let nodes = self.gl_nodes(m, pw);
let (ref list, node_rel) = *nodes;
if 2 * list.len() > self.evals_left {
break;
}
let mut sum = c_zero(psum);
let mut l1 = accuracy::EXACT;
let mut rerr = accuracy::EXACT;
let mut real = true;
for (r, w) in list.iter() {
for sign in [false, true] {
if self.evals_left == 0 {
return Err(not_converged("the evaluation budget was exhausted"));
}
self.evals_left -= 1;
let s = if sign { r.neg() } else { r.clone() };
let (x, weight) = match upper {
None => (centre.add(&c.mul(&s, pw, rm), pe, rm), c.mul(w, pw, rm)),
Some(up) => {
let sp1 = s.add(&one, pw, rm);
let u = scale2(&one.div(&sp1, pw, rm), 1);
let x = if up {
centre.add(&u, pe, rm)
} else {
centre.sub(&u, pe, rm)
};
(x, scale2(&w.div(&sp1.mul(&sp1, pw, rm), pw, rm), 1))
}
};
let x_err = accuracy::lsum(end_err, ulp2(&x, pe));
let (fv, fb) = self.f.eval(&x, x_err, pe, rm, cc)?;
if fb.is_unknown()
|| fv.0.is_nan()
|| fv.0.is_inf()
|| fv.1.is_nan()
|| fv.1.is_inf()
{
return Err(not_converged("the integrand has no error bound at a node"));
}
let value = (fv.0.mul(&weight, psum, rm), fv.1.mul(&weight, psum, rm));
let size = accuracy::lg_abs(&value);
let err = accuracy::lsum(
accuracy::lsum(
accuracy::shift(fb.joint(), lg(&weight)),
accuracy::shift(node_rel + 2.0, size),
),
accuracy::shift(1.0 - psum as f64, size),
);
real &= accuracy::exactly_real(&fv, fb);
l1 = accuracy::lsum(l1, accuracy::lsum(size, err));
rerr = accuracy::lsum(rerr, err);
sum = c_add(&sum, &value, psum, rm);
}
}
let rounding =
accuracy::shift(l1, ((2 * list.len()) as f64).log2() + 1.0 - psum as f64);
let r = accuracy::lsum(rerr, rounding);
if let Some(p) = &prev {
let diff = accuracy::lg_abs(&c_sub(&sum, p, psum, rm));
let rel_tol = accuracy::shift(l1, -rel_tol_bits).max(self.abs_floor);
let tol = rel_tol.max(accuracy::shift(r, 1.0));
debug!(m, diff, tol, r, "quadrature: Gauss–Legendre degree done");
out.err = accuracy::lsum(diff, r);
out.converged = m >= MIN_GL_DEGREE && diff <= tol;
out.discretisation = r <= rel_tol || diff > tol;
out.digits = out.digits.max(l1 - diff);
rel_hist.push(diff - l1);
}
out.value = sum.clone();
out.l1 = l1;
out.real = real;
if out.converged {
break;
}
if m >= 6 && rel_hist.last().is_some_and(|&v| v > -2.0) {
break;
}
prev = Some(sum);
}
Ok(out)
}
pub(super) fn run(
&mut self,
pieces: Vec<Piece>,
cc: &mut Consts,
) -> Result<RunOut, SymplexError> {
let rm = self.rm;
let psum = self.pe + SUM_GUARD;
let mut total = c_zero(psum);
let mut err = accuracy::EXACT;
let mut l1 = accuracy::EXACT;
let mut real = true;
let mut converged = true;
let mut discretisation = true;
let mut count = pieces.len();
let mut queue: Vec<(Piece, u32, bool)> =
pieces.into_iter().rev().map(|p| (p, 0, false)).collect();
let gl_only = GAUSS_LEGENDRE.with(std::cell::Cell::get);
while let Some((piece, depth, gl_first)) = queue.pop() {
let finite = match &piece {
Piece::Finite(a, b) => Some((a.clone(), b.clone())),
_ => None,
};
let mut best: Option<PieceOut> = None;
let mut ts_digits = f64::INFINITY;
let order: &[bool] = if gl_only {
&[true]
} else if gl_first {
&[true, false]
} else {
&[false, true]
};
for &gl in order {
let out = match (gl, gl_only) {
(true, true) => self.gl_piece(&piece, cc)?,
(true, false) => match self.gl_piece(&piece, cc) {
Ok(out) if finite.is_some() => out,
_ => continue,
},
(false, _) => self.piece(&piece, cc)?,
};
if !gl {
ts_digits = out.digits;
}
let done = out.converged || !out.discretisation;
if best
.as_ref()
.is_none_or(|b| out.converged || out.err < b.err)
{
if gl && out.converged {
debug!(depth, "quadrature: Gauss–Legendre converged");
}
best = Some(out);
}
if done {
break;
}
}
let Some(out) = best else {
return Err(internal("piece result"));
};
let bisect = !out.converged
&& out.discretisation
&& depth < MAX_DEPTH
&& count < MAX_PIECES
&& self.evals_left > self.evals_total / 8;
if bisect {
let osc = ts_digits < 8.0;
count += 1;
match &piece {
Piece::Finite(a, b) => {
let m = self.midpoint(a, b, cc)?;
debug!(depth, "quadrature: bisecting a piece that did not converge");
queue.push((Piece::Finite(m.clone(), b.clone()), depth + 1, osc));
queue.push((Piece::Finite(a.clone(), m), depth + 1, osc));
}
Piece::Upper(a) => {
let m = self.step_out(a, true, cc)?;
debug!(
depth,
"quadrature: splitting an infinite piece that did not converge"
);
queue.push((Piece::Upper(m.clone()), depth + 1, false));
queue.push((Piece::Finite(a.clone(), m), depth + 1, osc));
}
Piece::Lower(b) => {
let m = self.step_out(b, false, cc)?;
debug!(
depth,
"quadrature: splitting an infinite piece that did not converge"
);
queue.push((Piece::Lower(m.clone()), depth + 1, false));
queue.push((Piece::Finite(m, b.clone()), depth + 1, osc));
}
}
continue;
}
total = c_add(&total, &out.value, psum, rm);
err = accuracy::lsum(err, out.err);
l1 = accuracy::lsum(l1, out.l1);
self.abs_floor = accuracy::shift(
l1,
-((self.prec - TARGET_GUARD.min(self.prec)) as f64) - FLOOR_SHARE,
);
real &= out.real;
converged &= out.converged;
discretisation &= out.converged || out.discretisation;
}
debug!(
pieces = count,
evaluations = self.evals_total.saturating_sub(self.evals_left),
nodes = self.nodes.len(),
"quadrature: done"
);
if count > 1 {
err = accuracy::lsum(
err,
accuracy::shift(l1, (count as f64).log2() + 1.0 - psum as f64),
);
}
Ok(RunOut {
value: total,
err,
real,
converged,
discretisation,
})
}
fn step_out(&mut self, e: &End, up: bool, cc: &mut Consts) -> Result<End, SymplexError> {
use num_traits::Signed;
if let End::Rational(q) = e {
let one = Ratio::from_integer(BigInt::from(1));
let step = if q.abs() > one { q.abs() } else { one };
return Ok(End::Rational(if up { q + step } else { q - step }));
}
let p = self.pe + NODE_GUARD;
let (v, _) = self.end_value(e, p, cc)?;
let one = small(1);
let step = if v.abs().cmp(&one).is_some_and(|c| c > 0) {
v.abs()
} else {
one
};
Ok(End::Float(if up {
v.add(&step, p, self.rm)
} else {
v.sub(&step, p, self.rm)
}))
}
fn midpoint(&mut self, a: &End, b: &End, cc: &mut Consts) -> Result<End, SymplexError> {
if let (End::Rational(x), End::Rational(y)) = (a, b) {
return Ok(End::Rational((x + y) / BigInt::from(2)));
}
let p = self.pe + NODE_GUARD;
let (av, _) = self.end_value(a, p, cc)?;
let (bv, _) = self.end_value(b, p, cc)?;
Ok(End::Float(scale2(&av.add(&bv, p, self.rm), -1)))
}
}
const NOT_DECAYING: &str = "the integrand does not decay towards an end of the interval (the \
integral may diverge, or oscillate without decaying)";
fn not_converged(why: &str) -> SymplexError {
SymplexError::ComputationFailed {
operation: "evalf",
reason: format!("quadrature of the definite integral did not converge: {why}"),
}
}
pub(super) fn pieces(lo: Option<End>, hi: Option<End>, points: Vec<End>) -> Vec<Piece> {
let mut cuts = points;
if lo.is_none() && hi.is_none() && cuts.is_empty() {
cuts.push(End::Rational(Ratio::from_integer(BigInt::from(0))));
}
let mut out = Vec::with_capacity(cuts.len() + 1);
let mut left = lo;
for c in cuts {
out.push(match left {
Some(a) => Piece::Finite(a, c.clone()),
None => Piece::Lower(c.clone()),
});
left = Some(c);
}
out.push(match (left, hi) {
(Some(a), Some(b)) => Piece::Finite(a, b),
(Some(a), None) => Piece::Upper(a),
(None, Some(b)) => Piece::Lower(b),
(None, None) => Piece::Upper(End::Rational(Ratio::from_integer(BigInt::from(0)))),
});
out
}
pub(super) fn same_end(a: &End, b: &End) -> bool {
a.same(b)
}
thread_local! {
static GAUSS_LEGENDRE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(super) struct RuleGuard {
previous: bool,
}
impl RuleGuard {
pub(super) fn set(gauss_legendre: bool) -> RuleGuard {
RuleGuard {
previous: GAUSS_LEGENDRE.with(|c| c.replace(gauss_legendre)),
}
}
}
impl Drop for RuleGuard {
fn drop(&mut self) {
GAUSS_LEGENDRE.with(|c| c.set(self.previous));
}
}
type GlNodes = std::rc::Rc<(Vec<(BigFloat, BigFloat)>, ErrExp)>;
thread_local! {
static GL_NODES: std::cell::RefCell<Vec<((u32, usize), GlNodes)>> = const { std::cell::RefCell::new(Vec::new()) };
}
fn legendre_f64(n: usize, r: f64) -> (f64, f64) {
let (mut t1, mut t2) = (1.0, 0.0);
for j in 1..=n {
let jf = j as f64;
let t3 = ((2.0 * jf - 1.0) * r * t1 - (jf - 1.0) * t2) / jf;
t2 = t1;
t1 = t3;
}
(t1, t2)
}
fn gauss_legendre(m: u32, pw: usize, rm: RoundingMode) -> (Vec<(BigFloat, BigFloat)>, ErrExp) {
let n: usize = 3 << m.saturating_sub(1);
let nb = small(n as u64);
let one = small(1);
let mut out = Vec::with_capacity(n / 2);
for j in 1..=n / 2 {
let mut r = (std::f64::consts::PI * (j as f64 - 0.25) / (n as f64 + 0.5)).cos();
for _ in 0..10 {
let (t1, t2) = legendre_f64(n, r);
let dp = n as f64 * (r * t1 - t2) / (r * r - 1.0);
let a = t1 / dp;
r -= a;
if a.abs() < 1e-15 {
break;
}
}
let mut rb = BigFloat::from_f64(r, pw);
let mut bits = 40usize;
let t4 = loop {
let (mut t1, mut t2) = (one.clone(), BigFloat::new(pw));
for jj in 1..=n as u64 {
let a = rb.mul(&t1, pw, rm).mul(&small(2 * jj - 1), pw, rm);
let b = t2.mul(&small(jj - 1), pw, rm);
let t3 = a.sub(&b, pw, rm).div(&small(jj), pw, rm);
t2 = t1;
t1 = t3;
}
let den = rb.mul(&rb, pw, rm).sub(&one, pw, rm);
let t4 = nb
.mul(&rb.mul(&t1, pw, rm).sub(&t2, pw, rm), pw, rm)
.div(&den, pw, rm);
rb = rb.sub(&t1.div(&t4, pw, rm), pw, rm);
bits *= 2;
if bits >= pw + 16 {
break t4;
}
};
let om = one.sub(&rb.mul(&rb, pw, rm), pw, rm);
let w = scale2(&one, 1).div(&om.mul(&t4.mul(&t4, pw, rm), pw, rm), pw, rm);
out.push((rb, w));
}
(out, (n as f64).log2() + 6.0 - pw as f64)
}
pub(super) type MemoKey = [usize; 6];
struct Failure {
key: MemoKey,
prec: usize,
outcome: Result<(Complex, ErrExp, bool), SymplexError>,
}
thread_local! {
static FAILURES: std::cell::RefCell<Vec<Failure>> = const { std::cell::RefCell::new(Vec::new()) };
}
const MEMO_CAP: usize = 16;
pub(super) fn memo_key(
arena: &Arena,
body: ExprId,
var: ExprId,
lo: ExprId,
hi: ExprId,
) -> MemoKey {
[
std::ptr::from_ref(arena) as usize,
arena.node_count(),
body.0 as usize,
var.0 as usize,
lo.0 as usize,
hi.0 as usize,
]
}
pub(super) fn recall(
key: &MemoKey,
prec: usize,
) -> Option<Result<(Complex, ErrExp, bool), SymplexError>> {
FAILURES.with(|f| {
f.borrow()
.iter()
.find(|e| e.key == *key && e.prec <= prec)
.map(|e| match &e.outcome {
Ok(v) => Ok(v.clone()),
Err(err) => Err(super::duplicate_error(err)),
})
})
}
pub(super) fn remember(
key: MemoKey,
prec: usize,
outcome: Result<(Complex, ErrExp, bool), SymplexError>,
) {
FAILURES.with(|f| {
let mut f = f.borrow_mut();
f.retain(|e| e.key != key);
if f.len() >= MEMO_CAP {
f.remove(0);
}
f.push(Failure { key, prec, outcome });
});
}
pub(super) fn is_node_failure(e: &SymplexError) -> bool {
matches!(e, SymplexError::ComputationFailed { reason, .. }
if reason.contains(NOT_DECAYING) || reason.contains("budget") || reason.contains("time limit"))
}
#[cfg(test)]
mod tests {
use super::*;
fn to_f64(x: &BigFloat) -> f64 {
super::super::bigfloat_to_f64_rounded(x, RoundingMode::ToEven).unwrap_or(f64::NAN)
}
#[test]
fn gauss_legendre_nodes_are_exact_to_degree_2n_minus_1() {
let rm = RoundingMode::ToEven;
let p = 200;
let (nodes, rel) = gauss_legendre(3, p, rm);
assert_eq!(nodes.len(), 6);
assert!(rel < -180.0);
let mut sum_w = BigFloat::new(p);
let mut moment = BigFloat::new(p);
for (r, w) in &nodes {
sum_w = sum_w.add(&scale2(w, 1), p, rm);
moment = moment.add(&scale2(&w.mul(&r.powi(22, p, rm), p, rm), 1), p, rm);
}
let two = small(2);
let exact = two.div(&small(23), p, rm);
assert!(lg(&sum_w.sub(&two, p, rm)) < -180.0, "{}", to_f64(&sum_w));
assert!(
lg(&moment.sub(&exact, p, rm)) < -180.0,
"{}",
to_f64(&moment)
);
}
#[test]
fn dyadic_steps_are_exact() {
assert_eq!(to_f64(&dyadic(3, 2)), 0.75);
assert_eq!(to_f64(&scale2(&small(5), -3)), 0.625);
assert_eq!(to_f64(&scale2(&BigFloat::new(64), 7)), 0.0);
assert_eq!(quantize(129), 160);
}
#[test]
fn pieces_split_the_range() {
let zero = || End::Rational(Ratio::from_integer(BigInt::from(0)));
let p = pieces(None, None, Vec::new());
assert!(matches!(
&p[..],
[
Piece::Lower(End::Rational(_)),
Piece::Upper(End::Rational(_))
]
));
let p = pieces(
Some(zero()),
None,
vec![End::Rational(Ratio::from_integer(BigInt::from(1)))],
);
assert!(matches!(&p[..], [Piece::Finite(..), Piece::Upper(_)]));
assert!(same_end(&zero(), &zero()));
}
}