use super::asymptotic::AsymptoticError;
use super::asymptotic_common::{
bernoulli_numbers, eval_over, gate_accept, rational_to_expr, verification_points,
AsymptoticReport, Hypothesis, Rigor, DEFAULT_SLACK,
};
use crate::diff::diff;
use crate::integrate::integrate;
use crate::jit::eval_interp;
use crate::kernel::{subs, ExprId, ExprPool};
use crate::simplify::simplify;
use rug::{Integer, Rational};
use std::collections::HashMap;
pub const MAX_CORRECTIONS: usize = 8;
const CHECK_POINTS: [f64; 4] = [64.0, 128.0, 256.0, 512.0];
const CONSTANT_FIT_POINT: f64 = 1024.0;
const CONSTANT_DRIFT_TOL: f64 = 1e-2;
pub fn euler_maclaurin(
f: ExprId,
k: ExprId,
a: i64,
n: ExprId,
corrections: usize,
pool: &ExprPool,
) -> Result<AsymptoticReport, AsymptoticError> {
if corrections > MAX_CORRECTIONS {
return Err(AsymptoticError::InvalidTermCount);
}
if k == n {
return Err(AsymptoticError::InvalidTermCount);
}
let mut derivation = Vec::new();
let mut terms: Vec<ExprId> = Vec::new();
let antiderivative = integrate(f, k, pool)
.map_err(|_| AsymptoticError::UnsupportedScale)?
.value;
let integral_at_n = simplify(subs_one(antiderivative, k, n, pool), pool).value;
derivation.push(format!(
"∫f dk = {}, evaluated at n",
pool.display(antiderivative)
));
terms.push(integral_at_n);
let half = pool.rational(Integer::from(1), Integer::from(2));
let f_at_n = subs_one(f, k, n, pool);
terms.push(simplify(pool.mul(vec![half, f_at_n]), pool).value);
derivation.push("boundary term f(n)/2".to_string());
let bern = bernoulli_numbers(2 * corrections + 1);
let mut deriv = f;
let mut deriv_order = 0usize;
let mut factorial = Integer::from(1);
for j in 1..=corrections {
let target = 2 * j - 1;
while deriv_order < target {
deriv = diff(deriv, k, pool).map_err(AsymptoticError::Diff)?.value;
deriv = simplify(deriv, pool).value;
deriv_order += 1;
}
for t in (2 * j - 1)..=(2 * j) {
factorial *= Integer::from(t as u32);
}
let b = bern[2 * j].clone();
if b == 0 {
continue;
}
let coeff = Rational::from((b.numer().clone(), b.denom().clone() * factorial.clone()));
let coeff_expr = rational_to_expr(&coeff, pool);
let d_at_n = subs_one(deriv, k, n, pool);
terms.push(simplify(pool.mul(vec![coeff_expr, d_at_n]), pool).value);
derivation.push(format!("Bernoulli correction j = {j} (B_{} term)", 2 * j));
}
let points: Vec<f64> = CHECK_POINTS.to_vec();
let mut fit_points = points.clone();
fit_points.push(CONSTANT_FIT_POINT);
let oracle_all = exact_sums(f, k, a, &fit_points, pool).ok_or(AsymptoticError::GateFailed)?;
let mut term_vals_all: Vec<Vec<f64>> = Vec::with_capacity(terms.len());
for &t in &terms {
term_vals_all.push(eval_over(t, n, &fit_points, pool).ok_or(AsymptoticError::GateFailed)?);
}
let m = fit_points.len() - 1;
let fit_at =
|j: usize| -> f64 { oracle_all[j] - term_vals_all.iter().map(|row| row[j]).sum::<f64>() };
let constant = fit_at(m);
let previous = fit_at(m - 1);
let drift = (constant - previous).abs();
let scale = constant.abs().max(previous.abs());
let constant_converged = drift <= CONSTANT_DRIFT_TOL * scale;
let oracle: Vec<f64> = oracle_all[..points.len()].to_vec();
let mut term_vals: Vec<Vec<f64>> = term_vals_all
.into_iter()
.map(|row| row[..points.len()].to_vec())
.collect();
let last = points.len() - 1;
let mut constant_slot: Option<usize> = None;
if constant_converged {
derivation.push(format!(
"additive constant fitted numerically at n = {}: {constant} \
(it moved by {drift:.3e} from the fit at n = {}, so it is a constant)",
fit_points[m],
fit_points[m - 1]
));
let constant_expr = float_to_expr(constant, pool);
constant_slot = Some(terms.len());
terms.push(constant_expr);
term_vals.push(vec![constant; points.len()]);
} else {
derivation.push(format!(
"no additive constant is claimed: the fit moved from {previous} at n = {} \
to {constant} at n = {}, so it is not a constant — most likely a term of \
the expansion that `corrections` was too small to produce",
fit_points[m - 1],
fit_points[m],
));
}
let mut order: Vec<usize> = (0..terms.len()).collect();
order.sort_by(|&i, &j| {
term_vals[j][last]
.abs()
.partial_cmp(&term_vals[i][last].abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
terms = order.iter().map(|&i| terms[i]).collect();
term_vals = order.iter().map(|&i| term_vals[i].clone()).collect();
let constant_position = constant_slot.and_then(|slot| order.iter().position(|&i| i == slot));
let accepted = gate_accept(&oracle, &term_vals, DEFAULT_SLACK);
if accepted == 0 {
return Err(AsymptoticError::GateFailed);
}
terms.truncate(accepted);
term_vals.truncate(accepted);
let verification = verification_points(&points, &oracle, &term_vals, accepted);
let mut hypotheses = vec![
Hypothesis::checked(
"the summand has a symbolic antiderivative and is finite at every check point",
),
Hypothesis::assumed(
"the summand is smooth on [a, ∞) and its high derivatives decay, so the \
Euler–Maclaurin remainder is asymptotically negligible",
),
];
if constant_position.is_some_and(|p| p < accepted) {
hypotheses.push(Hypothesis::assumed(
"the additive constant was fitted numerically from the exact sum, not derived; \
it was refit at a second, larger point and agreed",
));
} else {
hypotheses.push(Hypothesis::checked(
"no numerically fitted additive constant is part of this expansion",
));
}
Ok(AsymptoticReport {
method: "euler-maclaurin",
var: n,
terms,
rigor: Rigor::NumericallyConsistent,
hypotheses,
verification,
derivation,
})
}
fn subs_one(expr: ExprId, from: ExprId, to: ExprId, pool: &ExprPool) -> ExprId {
let mut m = HashMap::new();
m.insert(from, to);
subs(expr, &m, pool)
}
fn exact_sums(f: ExprId, k: ExprId, a: i64, points: &[f64], pool: &ExprPool) -> Option<Vec<f64>> {
let mut out = Vec::with_capacity(points.len());
for &p in points {
let upper = p as i64;
let mut acc = 0.0f64;
for i in a..=upper {
let mut env = HashMap::new();
env.insert(k, i as f64);
let v = eval_interp(f, &env, pool)?;
if !v.is_finite() {
return None;
}
acc += v;
}
if !acc.is_finite() {
return None;
}
out.push(acc);
}
Some(out)
}
fn float_to_expr(v: f64, pool: &ExprPool) -> ExprId {
match Rational::from_f64(v) {
Some(q) => {
let scale = Integer::from(10_000_000_000_000_i64);
let scaled = (q * Rational::from(scale.clone())).round();
let num = scaled.numer().clone();
rational_to_expr(&Rational::from((num, scale)), pool)
}
None => pool.integer(0_i32),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
fn setup() -> (ExprPool, ExprId, ExprId) {
let pool = ExprPool::new();
let k = pool.symbol("k", Domain::Real);
let n = pool.symbol("n", Domain::Real);
(pool, k, n)
}
#[test]
fn harmonic_numbers_recover_log_plus_gamma() {
let (pool, k, n) = setup();
let f = pool.pow(k, pool.integer(-1_i32));
let r = euler_maclaurin(f, k, 1, n, 2, &pool).expect("expansion");
assert_eq!(r.method, "euler-maclaurin");
assert!(
r.terms.len() >= 2,
"expected at least log n and the constant"
);
let leading = pool.display(r.leading().unwrap()).to_string();
assert!(
leading.contains("log") || leading.contains("ln"),
"leading term should be logarithmic, got {leading}"
);
let partial = r.partial_sum(&pool);
let mut env = std::collections::HashMap::new();
env.insert(n, 1000.0);
let approx = crate::jit::eval_interp(partial, &env, &pool).expect("evaluates");
let truth: f64 = (1..=1000).map(|i| 1.0 / i as f64).sum();
assert!(
(approx - truth).abs() < 1e-6,
"H_1000: expansion {approx} vs truth {truth}"
);
}
#[test]
fn constant_is_labelled_as_fitted_not_proved() {
let (pool, k, n) = setup();
let f = pool.pow(k, pool.integer(-1_i32));
let r = euler_maclaurin(f, k, 1, n, 2, &pool).expect("expansion");
assert_eq!(r.rigor, Rigor::NumericallyConsistent);
assert!(!r.all_hypotheses_checked());
assert!(
r.hypotheses
.iter()
.any(|h| h.statement.contains("fitted numerically")),
"the fitted constant must be declared"
);
assert!(!r.verification.is_empty());
assert!(r.max_relative_error().unwrap() < 1e-6);
}
#[test]
fn polynomial_summand_is_exact() {
let (pool, k, n) = setup();
let r = euler_maclaurin(k, k, 1, n, 1, &pool).expect("expansion");
let partial = r.partial_sum(&pool);
for ni in [10.0_f64, 50.0, 200.0] {
let mut env = std::collections::HashMap::new();
env.insert(n, ni);
let approx = crate::jit::eval_interp(partial, &env, &pool).expect("evaluates");
let truth = ni * (ni + 1.0) / 2.0;
assert!(
(approx - truth).abs() / truth < 1e-9,
"n = {ni}: {approx} vs {truth}"
);
}
}
#[test]
fn faulhaber_gets_no_spurious_constant() {
let (pool, k, n) = setup();
let f = pool.pow(k, pool.integer(9_i32));
let r = euler_maclaurin(f, k, 1, n, 2, &pool).expect("expansion");
for &t in &r.terms {
let mut env = std::collections::HashMap::new();
env.insert(n, 10.0);
let at_10 = crate::jit::eval_interp(t, &env, &pool).expect("evaluates");
env.insert(n, 20.0);
let at_20 = crate::jit::eval_interp(t, &env, &pool).expect("evaluates");
assert!(
(at_10 - at_20).abs() > 1e-9 * at_10.abs().max(1.0),
"constant term {} in Σ k⁹ (value {at_10} at both n = 10 and n = 20)",
pool.display(t)
);
}
let partial = r.partial_sum(&pool);
for ni in [1000.0_f64, 10_000.0] {
let mut env = std::collections::HashMap::new();
env.insert(n, ni);
let approx = crate::jit::eval_interp(partial, &env, &pool).expect("evaluates");
let truth = ni.powi(10) / 10.0 + ni.powi(9) / 2.0 + 0.75 * ni.powi(8)
- 0.7 * ni.powi(6)
+ 0.5 * ni.powi(4)
- 0.15 * ni * ni;
assert!(
(approx - truth).abs() / truth < 1e-9,
"n = {ni}: expansion {approx} vs Faulhaber {truth}"
);
}
}
#[test]
fn the_constant_is_refit_and_the_report_says_so() {
let (pool, k, n) = setup();
let harmonic = pool.pow(k, pool.integer(-1_i32));
let r = euler_maclaurin(harmonic, k, 1, n, 2, &pool).expect("expansion");
assert!(
r.derivation
.iter()
.any(|d| d.contains("so it is a constant")),
"γ must be accepted as a constant: {:?}",
r.derivation
);
let ninth = pool.pow(k, pool.integer(9_i32));
let r9 = euler_maclaurin(ninth, k, 1, n, 2, &pool).expect("expansion");
assert!(
r9.derivation
.iter()
.any(|d| d.contains("no additive constant is claimed")),
"the Σ k⁹ fit is not a constant and must be reported as such: {:?}",
r9.derivation
);
assert!(
r9.hypotheses
.iter()
.all(|h| !h.statement.contains("fitted numerically")),
"no fitted constant was emitted, so none may be claimed"
);
}
#[test]
fn refuses_when_the_summand_cannot_be_integrated() {
let (pool, k, n) = setup();
let neg_k2 = pool.mul(vec![pool.integer(-1_i32), k, k]);
let f = pool.func("exp", vec![neg_k2]);
let err = euler_maclaurin(f, k, 1, n, 1, &pool).expect_err("must refuse");
assert!(matches!(err, AsymptoticError::UnsupportedScale));
}
#[test]
fn refuses_absurd_correction_count() {
let (pool, k, n) = setup();
let f = pool.pow(k, pool.integer(-1_i32));
let err = euler_maclaurin(f, k, 1, n, MAX_CORRECTIONS + 1, &pool).expect_err("must refuse");
assert!(matches!(err, AsymptoticError::InvalidTermCount));
}
#[test]
fn refuses_coincident_index_and_variable() {
let (pool, k, _n) = setup();
let f = pool.pow(k, pool.integer(-1_i32));
assert!(euler_maclaurin(f, k, 1, k, 1, &pool).is_err());
}
}