use super::ball::CBall;
use super::wp::{RM, Wp};
use crate::base::bigfloat;
use astro_float::{BigFloat, Consts};
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::budget::{BudgetKind, Work};
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::base::walk;
use num_traits::{One, Zero};
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;
fn walk_reach(j: u64) -> f64 {
std::f64::consts::PI * (j as f64 / 2.0).sinh() * std::f64::consts::LOG2_E
}
const MAX_EVALS: u64 = 30_000;
const MAX_PIECES: usize = 64;
const MAX_DEPTH: u32 = 8;
const MAX_PERIODS_PER_PIECE: f64 = 256.0;
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;
pub(super) 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> {
pub(super) 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(),
}
}
pub(super) fn eval(
&mut self,
x: &BigFloat,
x_err: ErrExp,
w: &mut Wp<'_>,
) -> Result<CBall, SymplexError> {
let prec = w.bits();
self.eval_frontier(w);
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, w).and_then(
|CBall { mid: v, err: 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(CBall::new(value, bound))
}
pub(super) fn constant_scale(&mut self, w: &mut Wp<'_>) -> f64 {
self.eval_frontier(w);
let Some((cache, _)) = self.scratch.get(&w.bits()) else {
return f64::INFINITY;
};
let mut scale = 0.0f64;
for (c, _) in &self.frontier {
let Some(v) = cache.get(c) else {
return f64::INFINITY;
};
for part in [&v.0, &v.1] {
if !part.is_zero() {
scale = scale.max(accuracy::part_lg(part).abs());
}
}
}
scale
}
pub(super) fn crossover_reach(&mut self, w: &mut Wp<'_>) -> f64 {
let twice = 2.0 * self.constant_scale(w);
twice.max(self.close_reach(w))
}
pub(super) fn close_reach(&mut self, w: &mut Wp<'_>) -> f64 {
self.close_points(w)
.into_iter()
.fold(f64::NEG_INFINITY, f64::max)
}
pub(super) fn close_points(&mut self, w: &mut Wp<'_>) -> Vec<f64> {
let pairs = self.close_orders();
if pairs.is_empty() {
return Vec::new();
}
let twice = 2.0 * self.constant_scale(w);
let two = small(2);
let _ = self.eval(&two, accuracy::EXACT, w);
let ln2 = std::f64::consts::LN_2;
let size = |cache: &FxHashMap<ExprId, Complex>, m: &Size| -> Option<f64> {
let lg = |id: &ExprId| {
cache
.get(id)
.map(accuracy::lg_abs)
.filter(|l| l.is_finite())
};
match m {
Size::Of(id) => lg(id),
Size::LogOf(b, e) => {
let l = (lg(b)? * ln2 * e).abs().log2();
l.is_finite().then_some(l)
}
}
};
let mut points = Vec::new();
let cache = self.scratch.get(&w.bits()).map(|s| &s.0);
for p in &pairs {
let sizes = cache.and_then(|c| Some((size(c, &p.top)?, size(c, &p.other)?)));
let slack = if p.logarithm { 0.6 } else { 0.0 };
let lead = sizes.map_or(twice, |(top, other)| other - top + slack);
if p.logarithm {
let mut l = (lead.max(0.0) + 64.0) / p.gap;
for _ in 0..30 {
l = ((lead + (l.max(2.0) * ln2).log2()) / p.gap).max(1.0);
}
points.push(l + 1.0);
points.push((lead - p.gap.log2()) / p.gap + 1.0);
} else {
let r = lead / p.gap + 1.0;
points.push(r);
if let Some(ratio) = p.ratio {
points.push(r + ratio.log2() / p.gap);
}
}
}
points.retain(|r| r.is_finite());
points
}
fn close_orders(&self) -> Vec<Close> {
use num_traits::Signed;
let one = Q::one();
let depends: FxHashSet<ExprId> = self.dependent.iter().copied().collect();
let mut orders: FxHashMap<ExprId, Option<Q>> = FxHashMap::default();
orders.insert(self.var, Some(Q::one()));
let mut pairs: Vec<Close> = Vec::new();
let mut note = |terms: &[(Size, Q, bool)]| {
let Some(top) = terms.iter().map(|t| &t.1).max().cloned() else {
return;
};
for (lead, _, _) in terms.iter().filter(|t| t.1 == top) {
for (other, p, logarithm) in terms {
let gap = &top - p;
if gap.is_positive() && gap < one && pairs.len() < MAX_PAIRS {
pairs.push(Close {
top: lead.clone(),
other: other.clone(),
gap: crate::base::numeric::ratio_to_f64(&gap).unwrap_or(1.0),
logarithm: *logarithm,
ratio: (p.is_positive() && top.is_positive())
.then(|| crate::base::numeric::ratio_to_f64(&(p / &top)))
.flatten(),
});
}
}
}
};
for &id in &self.dependent {
let order_of = |c: &ExprId| -> Option<Q> {
match orders.get(c) {
Some(o) => o.clone(),
None if depends.contains(c) => None,
None => Some(Q::zero()),
}
};
let dependent = |c: &ExprId| *c == self.var || depends.contains(c);
let order = match self.arena.node(id) {
ExprNode::Add(children) => {
let terms: Option<Vec<(Size, Q, bool)>> = children
.iter()
.map(|c| {
order_of(c)
.map(|q| (Size::Of(*c), q.clone(), dependent(c) && q.is_zero()))
})
.collect();
terms.and_then(|terms| {
note(&terms);
terms.into_iter().map(|t| t.1).max()
})
}
ExprNode::Mul(children) => {
let logs: Option<Vec<(Size, Q, bool)>> = children
.iter()
.filter(|c| dependent(c))
.filter_map(|c| match self.arena.node(*c) {
ExprNode::Exp(u) => Some(order_of(u).map(|q| {
(
Size::Of(*u),
if q.is_positive() { q } else { Q::zero() },
false,
)
})),
ExprNode::Pow(b, e) => {
let e = crate::base::numeric::ratio_to_f64(self.arena.as_num(*e)?)?;
Some(order_of(c).map(|_| (Size::LogOf(*b, e), Q::zero(), true)))
}
_ if *c == self.var => {
Some(Some((Size::LogOf(self.var, 1.0), Q::zero(), true)))
}
_ => None,
})
.collect();
if let Some(logs) = logs
&& logs.iter().any(|t| t.1.is_positive())
{
note(&logs);
}
children
.iter()
.map(order_of)
.try_fold(Q::zero(), |s, q| q.map(|q| s + q))
}
ExprNode::Pow(b, e) => match self.arena.as_num(*e) {
Some(e) => order_of(b).map(|q| q * e),
None => None,
},
ExprNode::Exp(u) => order_of(u).filter(|q| !q.is_positive()).map(|_| Q::zero()),
ExprNode::Neg(u) | ExprNode::Abs(u) => order_of(u),
ExprNode::Ln(u) => order_of(u).map(|_| Q::zero()),
ExprNode::Sin(_)
| ExprNode::Cos(_)
| ExprNode::Atan(_)
| ExprNode::Tanh(_)
| ExprNode::Erf(_) => Some(Q::zero()),
_ => None,
};
orders.insert(id, order);
}
pairs
}
fn frequency(&self) -> f64 {
let mut top = 0.0f64;
for &id in &self.dependent {
if let ExprNode::Sin(a) | ExprNode::Cos(a) | ExprNode::Tan(a) = self.arena.node(id)
&& let Some(c) = linear_coefficient(self.arena, *a, self.var)
{
top = top.max(c);
}
}
top
}
fn eval_frontier(&mut self, w: &mut Wp<'_>) {
let prec = w.bits();
if self.scratch.contains_key(&prec) {
return;
}
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(CBall { mid: v, err: b }) =
evaluate_tree(self.arena, *c, order, None, false, w)
{
cache.insert(*c, v);
errs.insert(*c, b);
}
}
self.scratch.insert(prec, (cache, errs));
}
}
const MAX_PAIRS: usize = 64;
#[derive(Clone, Debug)]
enum Size {
Of(ExprId),
LogOf(ExprId, f64),
}
#[derive(Clone, Debug)]
struct Close {
top: Size,
other: Size,
gap: f64,
logarithm: bool,
ratio: Option<f64>,
}
fn linear_coefficient(arena: &Arena, e: ExprId, var: ExprId) -> Option<f64> {
let term = |t: ExprId| -> Option<f64> {
if t == var {
return Some(1.0);
}
let ExprNode::Mul(children) = arena.node(t) else {
return None;
};
let mut coefficient = 1.0f64;
let mut found = false;
for &c in children.iter() {
if c == var && !found {
found = true;
} else {
coefficient *= crate::base::numeric::ratio_to_f64(arena.as_num(c)?)?.abs();
}
}
found.then_some(coefficient)
};
if let ExprNode::Add(children) = arena.node(e) {
let mut dependent = children.iter().filter(|&&c| walk::contains(arena, c, var));
let (Some(&t), None) = (dependent.next(), dependent.next()) else {
return None;
};
return term(t);
}
term(e)
}
pub(crate) fn nonzero_on(
arena: &Arena,
body: ExprId,
var: ExprId,
lo: &Ratio<BigInt>,
hi: &Ratio<BigInt>,
) -> bool {
const PREC: usize = 128;
if lo > hi {
return false;
}
let Ok(mut cc) = Consts::new() else {
return false;
};
let two = Ratio::from_integer(BigInt::from(2));
let centre = (lo + hi) / &two;
let Some(half) = crate::base::numeric::ratio_to_f64(&((hi - lo) / &two)) else {
return false;
};
if !half.is_finite() {
return false;
}
let x = crate::base::numeric::ratio_to_bigfloat(¢re, PREC, RM);
let rounding = ulp2(&x, PREC);
let width = if half > 0.0 {
half.log2() + 1e-9
} else {
accuracy::EXACT
};
let x_err = accuracy::lsum(width, rounding);
let mut f = Integrand::new(arena, body, var);
let Ok(CBall { mid: v, err: b }) = f.eval(&x, x_err, &mut Wp::new(PREC, &mut cc)) else {
return false;
};
if b.is_unknown() || v.0.is_nan() || v.1.is_nan() || v.0.is_inf() || v.1.is_inf() {
return false;
}
let clear = |part: &BigFloat, e: ErrExp| {
accuracy::part_mag(part).is_some_and(|m| (m - 1) as f64 > e + 1.0)
};
clear(&v.0, b.re) || clear(&v.1, b.im)
}
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 {
pub(super) fn exact(&self, pa: &mut Arena) -> Option<ExprId> {
match self {
End::Rational(q) => Some(pa.num_ratio(q.clone())),
End::Expr(id) => Some(*id),
End::Float(_) | End::Value(..) => None,
}
}
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,
seen: bool,
}
struct PieceOut {
value: Complex,
err: ErrExp,
l1: ErrExp,
converged: bool,
discretisation: bool,
real: bool,
digits: f64,
seen: bool,
}
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 underflowed(v: &Complex, b: Bound) -> bool {
v.0.is_zero() && v.1.is_zero() && accuracy::is_underflow(b.joint())
}
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
}
pub(super) 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
}
pub(super) 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,
work: Work,
ends: FxHashMap<(ExprId, usize), (BigFloat, ErrExp)>,
nodes: FxHashMap<(u32, u64), std::rc::Rc<TsNode>>,
abs_floor: ErrExp,
reach: Option<f64>,
frequency: Option<f64>,
}
impl<'a> Quad<'a> {
pub(super) fn new(
arena: &'a Arena,
body: ExprId,
var: ExprId,
variants: Vec<(End, bool, ExprId)>,
prec: usize,
) -> 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)),
work: Work::new(BudgetKind::QuadratureEvaluations, MAX_EVALS),
ends: FxHashMap::default(),
nodes: FxHashMap::default(),
abs_floor: accuracy::EXACT,
reach: None,
frequency: None,
}
}
fn reach(&mut self, cc: &mut Consts) -> f64 {
if let Some(s) = self.reach {
return s;
}
let s = self.f.crossover_reach(&mut Wp::new(self.pe, cc));
self.reach = Some(s);
s
}
pub(super) fn lighten(&mut self) {
self.work = Work::new(BudgetKind::QuadratureEvaluations, MAX_EVALS / 4);
}
pub(super) fn end_value(
&mut self,
e: &End,
w: &mut Wp<'_>,
) -> Result<(BigFloat, ErrExp), SymplexError> {
let p = w.bits();
match e {
End::Rational(q) => {
let v = crate::base::numeric::ratio_to_bigfloat(q, p, 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 CBall { mid: v, err: b } =
evaluate_tree(self.f.arena, *id, &order, None, true, w)?;
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, w: &mut Wp<'_>) -> 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, w));
self.nodes.insert((k, j), n.clone());
n
}
fn compute_ts_node(&self, j: u64, k: u32, wpc: &mut Wp<'_>) -> TsNode {
let pw = wpc.bits();
let t = dyadic(j, k);
let pi = wpc.cc.pi(pw, RM);
let s = t.sinh(pw, RM, wpc.cc);
let c = t.cosh(pw, RM, wpc.cc);
let arg = pi.mul(&s, pw, RM); let q = arg.neg().exp(pw, RM, wpc.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,
wpc: &mut Wp<'_>,
) -> Result<Placed, SymplexError> {
let p = wpc.bits();
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(&wpc.cc.pi(pw, RM), -1);
return Ok(match piece {
Piece::Finite(a, _) => {
let (av, ae) = self.end_value(a, wpc)?;
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, wpc)?;
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, wpc)?;
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, wpc)?;
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, wpc)?;
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, wpc)?;
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 psum = self.pe + SUM_GUARD;
let mut p = self.pe;
loop {
self.work.charge(1, "evalf")?;
let placed = self.place(piece, half, side, node, k, &mut Wp::new(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, &mut Wp::new(p, cc));
let next = match evaluated {
Ok(CBall { mid: fv, err: 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 {
let seen = !underflowed(&fv, fb);
return Ok(Term {
value,
err,
real,
seen,
});
}
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::f64_of(&placed.x)
.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, QuadError> {
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, &mut Wp::new(pw, cc))?;
let (bv, be) = self.end_value(b, &mut Wp::new(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;
let mut seen = centre.seen;
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 reach_needed = match (piece, side) {
(Piece::Upper(_), Side::Hi) | (Piece::Lower(_), Side::Lo) => self.reach(cc),
_ => f64::NEG_INFINITY,
};
if reach_needed > walk_reach(T_MAX_HALVES) {
debug!(
reach_needed,
"quadrature: the terms cross over beyond the nodes"
);
return Err(not_converged(&format!(
"the integrand's terms cross over near |x| = 2^{reach_needed:.0}, beyond the \
reach of the quadrature's nodes"
))
.into());
}
let mut quiet = 0;
let mut j = 1u64;
loop {
if j > T_MAX_HALVES {
debug!(
?side,
sizes = ?sizes[si],
l1,
"quadrature: the walk found no negligible terms"
);
return Err(QuadError::NotDecaying);
}
let node = self.ts_node(j, 1, &mut Wp::new(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;
seen |= t.seen;
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;
}
let reach = 1.0 - lg(&node.delta);
if quiet >= 2 && (j >= T_MAX_HALVES || (j >= T_MIN_HALVES && reach >= reach_needed))
{
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 > self.work.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, &mut Wp::new(pw, cc));
let t =
self.term(piece, &half, Some(side), Some(node.as_ref()), k, budget, cc)?;
real &= t.real;
seen |= t.seen;
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,
seen,
})
}
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 > self.work.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));
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 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, &mut Wp::new(pw, cc))?;
let (bv, be) = self.end_value(b, &mut Wp::new(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, &mut Wp::new(pw, cc))?;
(av.sub(&one, pw, RM), one.clone(), ae, Some(true))
}
Piece::Lower(b) => {
let (bv, be) = self.end_value(b, &mut Wp::new(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,
seen: false,
};
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() as u64 > self.work.left() {
break;
}
let mut sum = c_zero(psum);
let mut l1 = accuracy::EXACT;
let mut rerr = accuracy::EXACT;
let mut real = true;
let mut seen = false;
for (r, w) in list.iter() {
for sign in [false, true] {
self.work.charge(1, "evalf")?;
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 CBall { mid: fv, err: fb } =
self.f.eval(&x, x_err, &mut Wp::new(pe, 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);
seen |= !underflowed(&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;
out.seen = seen;
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, QuadError> {
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 seen = false;
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) && out.seen;
if best.as_ref().is_none_or(|b| {
if out.seen == b.seen {
out.converged || out.err < b.err
} else {
out.seen
}
}) {
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").into());
};
let bisect = !out.converged
&& out.discretisation
&& depth < MAX_DEPTH
&& count < MAX_PIECES
&& self.work.left() > self.work.cap() / 8;
if bisect && let Some(periods) = self.unresolvable(&piece, depth, cc) {
debug!(
depth,
periods, "quadrature: a piece oscillates beyond resolution"
);
return Err(not_converged(&format!(
"the integrand oscillates about {periods:.3e} times over a piece of the range, \
more than the quadrature resolves"
))
.into());
}
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;
seen |= out.seen;
}
if !seen {
debug!("quadrature: every node underflowed");
return Err(not_converged(BLIND).into());
}
debug!(
pieces = count,
evaluations = self.work.spent(),
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 unresolvable(&mut self, piece: &Piece, depth: u32, cc: &mut Consts) -> Option<f64> {
let Piece::Finite(a, b) = piece else {
return None;
};
let frequency = *self.frequency.get_or_insert_with(|| self.f.frequency());
if frequency <= 0.0 {
return None;
}
let p = 64;
let (av, _) = self.end_value(a, &mut Wp::new(p, cc)).ok()?;
let (bv, _) = self.end_value(b, &mut Wp::new(p, cc)).ok()?;
let width = super::f64_of(&bv.sub(&av, p, RM)).ok()?.abs();
let periods = frequency * width / std::f64::consts::TAU;
let finest = periods / f64::from(MAX_DEPTH.saturating_sub(depth)).exp2();
let most = MAX_PERIODS_PER_PIECE * MAX_PIECES as f64;
(finest > MAX_PERIODS_PER_PIECE || periods > most).then_some(periods)
}
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, &mut Wp::new(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, RM)
} else {
v.sub(&step, p, 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, &mut Wp::new(p, cc))?;
let (bv, _) = self.end_value(b, &mut Wp::new(p, cc))?;
Ok(End::Float(scale2(&av.add(&bv, p, RM), -1)))
}
}
const BLIND: &str = "the integrand underflowed the exponent range at every node: its mass, \
if any, lies between them";
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) -> (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 = [u64; 5];
struct Failure {
key: MemoKey,
prec: usize,
outcome: Result<(Complex, ErrExp, bool), QuadError>,
}
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 {
[
arena.serial(),
u64::from(body.index()),
u64::from(var.index()),
u64::from(lo.index()),
u64::from(hi.index()),
]
}
pub(super) fn recall(
key: &MemoKey,
prec: usize,
) -> Option<Result<(Complex, ErrExp, bool), QuadError>> {
FAILURES.with(|f| {
f.borrow()
.iter()
.find(|e| e.key == *key && e.prec <= prec)
.map(|e| e.outcome.clone())
})
}
pub(super) fn remember(
key: MemoKey,
prec: usize,
outcome: Result<(Complex, ErrExp, bool), QuadError>,
) {
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) type ClosedForm = std::rc::Rc<Result<Option<(Arena, ExprId)>, SymplexError>>;
thread_local! {
static CLOSED_FORMS: std::cell::RefCell<Vec<(MemoKey, ClosedForm)>> =
const { std::cell::RefCell::new(Vec::new()) };
static SEEKING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
struct Seeking;
impl Drop for Seeking {
fn drop(&mut self) {
SEEKING.with(|s| s.set(false));
}
}
pub(super) fn closed_form(
key: MemoKey,
seek: impl FnOnce() -> Result<Option<(Arena, ExprId)>, SymplexError>,
) -> Option<ClosedForm> {
let known = recall_closed_form(&key);
if known.is_some() {
return known;
}
if SEEKING.with(std::cell::Cell::get) {
return None;
}
SEEKING.with(|s| s.set(true));
let found = {
let _seeking = Seeking;
std::rc::Rc::new(seek())
};
CLOSED_FORMS.with(|m| {
let mut m = m.borrow_mut();
if m.len() >= CLOSED_FORM_CAP {
m.remove(0);
}
m.push((key, found.clone()));
});
Some(found)
}
pub(super) fn recall_closed_form(key: &MemoKey) -> Option<ClosedForm> {
CLOSED_FORMS.with(|m| {
m.borrow()
.iter()
.find(|(k, _)| k == key)
.map(|(_, c)| c.clone())
})
}
const CLOSED_FORM_CAP: usize = 8;
#[derive(Clone, Debug)]
pub(super) enum QuadError {
NotDecaying,
Failed(SymplexError),
}
impl From<SymplexError> for QuadError {
fn from(e: SymplexError) -> Self {
QuadError::Failed(e)
}
}
impl From<QuadError> for SymplexError {
fn from(e: QuadError) -> Self {
match e {
QuadError::NotDecaying => not_converged(NOT_DECAYING),
QuadError::Failed(e) => e,
}
}
}
impl QuadError {
pub(super) fn is_node_failure(&self) -> bool {
matches!(
self,
QuadError::NotDecaying
| QuadError::Failed(
SymplexError::Divergent { .. }
| SymplexError::BudgetExhausted {
kind: BudgetKind::QuadratureEvaluations,
..
}
)
)
}
pub(super) fn may_diverge(&self) -> bool {
matches!(
self,
QuadError::NotDecaying
| QuadError::Failed(
SymplexError::ComputationFailed { .. } | SymplexError::BudgetExhausted { .. }
)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn to_f64(x: &BigFloat) -> f64 {
super::super::f64_of(x).unwrap_or(f64::NAN)
}
#[test]
fn gauss_legendre_nodes_are_exact_to_degree_2n_minus_1() {
let p = 200;
let (nodes, rel) = gauss_legendre(3, p);
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()));
}
}