use std::collections::BTreeMap;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
use tracing::trace;
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use crate::calculus::summation::{self, TermShape};
use crate::poly::polybridge;
use crate::transforms::eval;
type Rat = Ratio<BigInt>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Convergence {
Converges,
Diverges,
Inconclusive,
}
impl Convergence {
fn to_option(self) -> Option<bool> {
match self {
Convergence::Converges => Some(true),
Convergence::Diverges => Some(false),
Convergence::Inconclusive => None,
}
}
}
pub(crate) fn is_convergent(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<bool> {
test_convergence(arena, body, var, false).to_option()
}
pub(crate) fn is_absolutely_convergent(
arena: &mut Arena,
body: ExprId,
var: ExprId,
) -> Option<bool> {
test_convergence(arena, body, var, true).to_option()
}
fn test_convergence(arena: &mut Arena, body: ExprId, var: ExprId, absolute: bool) -> Convergence {
if !matches!(arena.node(var), ExprNode::Symbol(_)) {
return Convergence::Inconclusive;
}
if !walk::contains(arena, body, var) {
let v = eval::eval(arena, body);
trace!("convergence: body is constant w.r.t. var");
return if arena.is_zero_structural(v) || arena.as_num(v).is_some_and(|r| r.is_zero()) {
Convergence::Converges
} else {
Convergence::Diverges
};
}
if let Some(c) = rational_test(arena, body, var) {
return c;
}
if let ExprNode::Add(ref terms) = arena.node(body).clone() {
let terms: Vec<ExprId> = terms.to_vec();
let mut convergent = 0usize;
let mut divergent = 0usize;
for &t in &terms {
match test_convergence(arena, t, var, absolute) {
Convergence::Converges => convergent += 1,
Convergence::Diverges => divergent += 1,
Convergence::Inconclusive => return Convergence::Inconclusive,
}
}
if divergent == 0 {
return Convergence::Converges;
}
if divergent == 1 && convergent == terms.len() - 1 {
return Convergence::Diverges;
}
return Convergence::Inconclusive;
}
if let Some(g) = growth_exponents(arena, body, var) {
return g.decide(absolute);
}
if let Some(rest) = strip_bounded_factors(arena, body, var)
&& test_convergence(arena, rest, var, true) == Convergence::Converges
{
return Convergence::Converges;
}
if let Some(c) = bertrand_test(arena, body, var) {
return c;
}
if let Some(c) = integral_test(arena, body, var) {
return c;
}
Convergence::Inconclusive
}
fn bertrand_test(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<Convergence> {
let factors: Vec<ExprId> = match arena.node(body) {
ExprNode::Mul(ch) => ch.to_vec(),
_ => vec![body],
};
let mut a = Rat::zero();
let mut b = Rat::zero();
let mut saw_log = false;
for f in factors {
if !walk::contains(arena, f, var) {
continue;
}
let (base, exp) = arena.as_base_exp(f);
let e = arena.as_num(exp).cloned()?;
match arena.node(base).clone() {
ExprNode::Ln(arg) => {
let (alpha, _) = linear_in(arena, arg, var)?;
if !alpha.is_positive() {
return None;
}
saw_log = true;
b += e;
}
_ => {
let (alpha, _) = linear_in(arena, base, var)?;
if !alpha.is_positive() {
return None;
}
a += e;
}
}
}
if !saw_log {
return None; }
let minus_one = -Rat::one();
trace!("convergence: Bertrand series with a = {a}, b = {b}");
Some(if a < minus_one || (a == minus_one && b < minus_one) {
Convergence::Converges
} else {
Convergence::Diverges
})
}
fn rational_test(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<Convergence> {
let combined = if matches!(arena.node(body), ExprNode::Add(_)) {
let t = polybridge::together(arena, body);
eval::eval(arena, t)
} else {
body
};
let (n, d) = polybridge::as_numer_denom(arena, combined);
let np = polybridge::expr_to_poly(arena, n, var)?;
let dp = polybridge::expr_to_poly(arena, d, var)?;
if dp.is_zero() {
return None;
}
if np.is_zero() {
return Some(Convergence::Converges);
}
let dn = np.degree()? as i64;
let dd = dp.degree()? as i64;
trace!("convergence: rational function, deg N = {dn}, deg D = {dd}");
Some(if dd - dn >= 2 {
Convergence::Converges
} else {
Convergence::Diverges
})
}
#[derive(Debug, Clone)]
pub(crate) struct Growth {
a: Rat,
b_rat: Rat,
b_logs: BTreeMap<u64, Rat>,
b_numeric: f64,
b_has_numeric: bool,
b_symbolic: bool,
c: Rat,
alternating: bool,
}
impl Growth {
fn b_sign(&self) -> Option<Option<bool>> {
if self.b_symbolic {
return None;
}
let logs_zero = self.b_logs.values().all(|q| q.is_zero());
if !self.b_has_numeric {
if logs_zero {
if self.b_rat.is_zero() {
return Some(None);
}
return Some(Some(self.b_rat.is_positive()));
}
let mut v = self.b_rat.to_f64().unwrap_or(0.0);
for (p, q) in &self.b_logs {
v += q.to_f64().unwrap_or(0.0) * (*p as f64).ln();
}
return Some(Some(v > 0.0));
}
let mut v = self.b_rat.to_f64().unwrap_or(0.0) + self.b_numeric;
for (p, q) in &self.b_logs {
v += q.to_f64().unwrap_or(0.0) * (*p as f64).ln();
}
if v.abs() < 1e-9 {
return None;
}
Some(Some(v > 0.0))
}
pub(crate) fn tends_to_zero(&self) -> Option<bool> {
if self.a.is_negative() {
return Some(true);
}
if self.a.is_positive() {
return Some(false);
}
match self.b_sign()? {
Some(false) => Some(true),
Some(true) => Some(false),
None => {
if self.c.is_negative() {
Some(true)
} else if self.c.is_positive() {
Some(false)
} else {
None
}
}
}
}
fn decide(&self, absolute: bool) -> Convergence {
if self.a.is_positive() {
return Convergence::Diverges;
}
if self.a.is_negative() {
return Convergence::Converges;
}
match self.b_sign() {
None => return Convergence::Inconclusive,
Some(Some(true)) => return Convergence::Diverges,
Some(Some(false)) => return Convergence::Converges,
Some(None) => {}
}
let minus_one = -Rat::one();
if self.alternating && !absolute {
if self.c.is_negative() {
Convergence::Converges
} else {
Convergence::Diverges
}
} else if self.c < minus_one {
Convergence::Converges
} else {
Convergence::Diverges
}
}
}
fn add_log_rational(logs: &mut BTreeMap<u64, Rat>, r: &Rat, q: &Rat) -> Option<()> {
if r.is_zero() {
return None;
}
let numer = r.numer().abs().to_u64()?;
let denom = r.denom().abs().to_u64()?;
for (n, sign) in [(numer, 1i64), (denom, -1i64)] {
for (p, e) in factor_small(n) {
let entry = logs.entry(p).or_insert_with(Rat::zero);
*entry += q * Ratio::from_integer(BigInt::from(sign * e as i64));
}
}
Some(())
}
fn factor_small(mut n: u64) -> Vec<(u64, u32)> {
let mut out = Vec::new();
if n < 2 {
return out;
}
let mut p = 2u64;
while p * p <= n {
let mut e = 0u32;
while n.is_multiple_of(p) {
n /= p;
e += 1;
}
if e > 0 {
out.push((p, e));
}
p += if p == 2 { 1 } else { 2 };
}
if n > 1 {
out.push((n, 1));
}
out
}
pub(crate) fn growth_exponents(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<Growth> {
let factors: Vec<ExprId> = match arena.node(body) {
ExprNode::Mul(ch) => ch.to_vec(),
_ => vec![body],
};
let mut pow_pows: Vec<(Rat, Rat, Rat, Rat)> = Vec::new();
let mut rest: Vec<ExprId> = Vec::new();
for f in factors {
let f = if let ExprNode::Pow(base, exp) = arena.node(f).clone()
&& let ExprNode::Pow(b2, e2) = arena.node(base).clone()
&& arena.as_num(exp).is_some_and(|q| q.is_integer())
{
let pq = arena.mul(&[e2, exp]);
let pq = eval::eval(arena, pq);
arena.pow(b2, pq)
} else {
f
};
if let ExprNode::Pow(base, exp) = arena.node(f).clone()
&& walk::contains(arena, base, var)
&& walk::contains(arena, exp, var)
{
let (alpha, beta) = linear_in(arena, base, var)?;
let (c, d) = linear_in(arena, exp, var)?;
if !alpha.is_positive() {
return None;
}
pow_pows.push((alpha, beta, c, d));
} else {
rest.push(f);
}
}
let rest_expr = match rest.len() {
0 => arena.one,
1 => rest[0],
_ => arena.mul(&rest),
};
let shape: TermShape = summation::term_shape(arena, rest_expr, var)?;
if !shape.binomials.is_empty() {
return None;
}
let mut g = Growth {
a: Rat::zero(),
b_rat: Rat::zero(),
b_logs: BTreeMap::new(),
b_numeric: 0.0,
b_has_numeric: false,
b_symbolic: false,
c: Rat::zero(),
alternating: shape.alternating,
};
for (_, p) in &shape.lin_pows {
g.c += p;
}
if !shape.numeric_base.is_one() {
if shape.numeric_base.is_negative() {
g.alternating = !g.alternating;
}
add_log_rational(&mut g.b_logs, &shape.numeric_base, &Rat::one())?;
}
for (base, a) in &shape.bases {
if *base == arena.e_const {
g.b_rat += a;
} else if let Some(v) = numeric_abs(arena, *base) {
if v <= 0.0 || !v.is_finite() {
return None;
}
g.b_numeric += a.to_f64()? * v.ln();
g.b_has_numeric = true;
} else {
g.b_symbolic = true;
}
}
let half = Rat::new(BigInt::one(), BigInt::from(2));
for (alpha, beta, e) in &shape.facts {
if !alpha.is_positive() {
return None;
}
let er = Ratio::from_integer(BigInt::from(*e));
g.a += &er * alpha;
g.b_rat -= &er * alpha;
add_log_rational(&mut g.b_logs, alpha, &(&er * alpha))?;
g.c += &er * (beta + &half);
}
for (alpha, _beta, c, d) in &pow_pows {
g.a += c;
add_log_rational(&mut g.b_logs, alpha, c)?;
g.c += d;
}
trace!(?g, "convergence: growth exponents");
Some(g)
}
fn numeric_abs(arena: &mut Arena, e: ExprId) -> Option<f64> {
if !walk::free_symbols(arena, e).is_empty() {
return None;
}
crate::transforms::evalf::eval_const_f64(arena, e).map(f64::abs)
}
fn linear_in(arena: &Arena, e: ExprId, var: ExprId) -> Option<(Rat, Rat)> {
let p = polybridge::expr_to_poly(arena, e, var)?;
match p.degree() {
Some(1) => Some((p.coeff(1), p.coeff(0))),
Some(0) => Some((Rat::zero(), p.coeff(0))),
_ => None,
}
}
fn strip_bounded_factors(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<ExprId> {
let ExprNode::Mul(ref factors) = arena.node(body).clone() else {
return None;
};
let factors: Vec<ExprId> = factors.to_vec();
let mut kept = Vec::new();
let mut stripped = false;
for f in factors {
let bounded = match arena.node(f).clone() {
ExprNode::Sin(_) | ExprNode::Cos(_) => true,
ExprNode::Pow(base, exp) => {
matches!(arena.node(base), ExprNode::Sin(_) | ExprNode::Cos(_))
&& arena.as_num(exp).is_some_and(|r| r.is_positive())
}
_ => false,
};
if bounded && walk::contains(arena, f, var) {
stripped = true;
} else {
kept.push(f);
}
}
if !stripped {
return None;
}
Some(match kept.len() {
0 => arena.one,
1 => kept[0],
_ => arena.mul(&kept),
})
}
fn is_log_exp(arena: &Arena, e: ExprId, var: ExprId) -> bool {
let mut stack = vec![e];
let mut has_log = false;
while let Some(id) = stack.pop() {
match arena.node(id) {
ExprNode::Num(_) => {}
ExprNode::Symbol(_) => {
if id != var {
return false;
}
}
ExprNode::Add(ch) | ExprNode::Mul(ch) => stack.extend(ch.iter().copied()),
ExprNode::Pow(b, x) => {
if arena.as_num(*x).is_none() {
return false;
}
stack.push(*b);
}
ExprNode::Ln(a) => {
has_log = true;
stack.push(*a);
}
ExprNode::Exp(a) => stack.push(*a),
ExprNode::Neg(a) => stack.push(*a),
_ => return false,
}
}
has_log
}
fn integral_test(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<Convergence> {
if !is_log_exp(arena, body, var) {
return None;
}
let anti = crate::transforms::integrate::integrate(arena, body, var);
if walk::has_unevaluated(arena, anti) {
return None;
}
let inf = arena.infinity;
let lim = crate::calculus::limit::limit(arena, anti, var, inf).ok()?;
if lim == arena.infinity || lim == arena.neg_infinity {
let f1 = value_at(arena, anti, var, 1_000)?;
let f2 = value_at(arena, anti, var, 1_000_000_000)?;
if f2.abs() > f1.abs() {
return Some(Convergence::Diverges);
}
return None;
}
if walk::has_unevaluated(arena, lim) || !walk::free_symbols(arena, lim).is_empty() {
return None;
}
let l = crate::transforms::evalf::eval_const_f64(arena, lim)?;
if !l.is_finite() {
return None;
}
let f1 = value_at(arena, anti, var, 1_000)?;
let f2 = value_at(arena, anti, var, 1_000_000_000)?;
let d1 = (f1 - l).abs();
let d2 = (f2 - l).abs();
if d2 <= d1 + 1e-12 && d2 < 0.5 * f64::max(1.0, l.abs()) {
trace!("convergence: integral test → converges (limit {l})");
Some(Convergence::Converges)
} else {
None
}
}
fn value_at(arena: &mut Arena, e: ExprId, var: ExprId, n: i64) -> Option<f64> {
let ne = arena.int(n);
let s = crate::transforms::subs::subs(arena, e, var, ne);
let v = crate::transforms::evalf::eval_const_f64(arena, s)?;
if v.is_finite() { Some(v) } else { None }
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() -> (Arena, ExprId) {
let mut arena = Arena::new();
let k = arena.symbol("k");
(arena, k)
}
#[test]
fn p_series_converges_p2() {
let (mut arena, k) = setup();
let neg2 = arena.int(-2);
let body = arena.pow(k, neg2);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
assert_eq!(is_absolutely_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn p_series_diverges_harmonic() {
let (mut arena, k) = setup();
let neg1 = arena.neg_one;
let body = arena.pow(k, neg1);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
}
#[test]
fn p_series_diverges_p_half() {
let (mut arena, k) = setup();
let neg_half = arena.rational(-1, 2);
let body = arena.pow(k, neg_half);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
}
#[test]
fn geometric_converges_half() {
let (mut arena, k) = setup();
let half = arena.rational(1, 2);
let body = arena.pow(half, k);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn geometric_diverges_two() {
let (mut arena, k) = setup();
let two = arena.int(2);
let body = arena.pow(two, k);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
}
#[test]
fn constant_zero_converges() {
let (mut arena, k) = setup();
let body = arena.zero;
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn constant_nonzero_diverges() {
let (mut arena, k) = setup();
let body = arena.int(5);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
}
#[test]
fn growing_terms_diverge() {
let (mut arena, k) = setup();
let two = arena.int(2);
let body = arena.pow(k, two);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
}
#[test]
fn sin_over_k_inconclusive_but_sin_over_k_squared_converges() {
let (mut arena, k) = setup();
let sin_k = arena.sin(k);
let neg1 = arena.neg_one;
let k_inv = arena.pow(k, neg1);
let body = arena.mul(&[sin_k, k_inv]);
assert_eq!(is_convergent(&mut arena, body, k), None);
let neg2 = arena.int(-2);
let k_inv2 = arena.pow(k, neg2);
let body = arena.mul(&[sin_k, k_inv2]);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn alternating_harmonic_conditionally_convergent() {
let (mut arena, k) = setup();
let m1 = arena.neg_one;
let sgn = arena.pow(m1, k);
let body = arena.div(sgn, k);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
assert_eq!(is_absolutely_convergent(&mut arena, body, k), Some(false));
assert_eq!(is_convergent(&mut arena, sgn, k), Some(false));
}
#[test]
fn factorial_ratio_tests() {
let (mut arena, k) = setup();
let kf = arena.factorial(k);
let kk = arena.pow(k, k);
let body = arena.div(kf, kk);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
let two = arena.int(2);
let tk = arena.pow(two, k);
let body = arena.div(tk, kf);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
let body = arena.div(kf, tk);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
let ten = arena.int(10);
let k10 = arena.pow(k, ten);
let body = arena.div(k10, tk);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
let two_k = arena.mul(&[two, k]);
let c2k = arena.binomial(two_k, k);
let four = arena.int(4);
let fk = arena.pow(four, k);
let body = arena.div(c2k, fk);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
let five = arena.int(5);
let fk5 = arena.pow(five, k);
let body = arena.div(c2k, fk5);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
let den = arena.mul(&[fk, k]);
let body = arena.div(c2k, den);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn rational_function_terms() {
let (mut arena, k) = setup();
let one = arena.one;
let k1 = arena.add(&[k, one]);
let body = arena.div(k, k1);
assert_eq!(is_convergent(&mut arena, body, k), Some(false));
let a = arena.div(one, k);
let b = arena.div(one, k1);
let body = arena.sub(a, b);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn integral_test_log_terms() {
let (mut arena, k) = setup();
let one = arena.one;
let lnk = arena.ln(k);
let two = arena.int(2);
let ln2 = arena.pow(lnk, two);
let den = arena.mul(&[k, ln2]);
let body = arena.div(one, den);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
}
#[test]
fn symbolic_parameter_decided_by_factorial() {
let (mut arena, k) = setup();
let x = arena.symbol("x");
let xk = arena.pow(x, k);
let kf = arena.factorial(k);
let body = arena.div(xk, kf);
assert_eq!(is_convergent(&mut arena, body, k), Some(true));
assert_eq!(is_convergent(&mut arena, xk, k), None);
}
#[test]
fn factor_small_primes() {
assert_eq!(factor_small(360), vec![(2, 3), (3, 2), (5, 1)]);
assert_eq!(factor_small(1), vec![]);
assert_eq!(factor_small(97), vec![(97, 1)]);
}
}