use super::hyperexp::HyperExpTerm;
use super::search::AzResult;
use super::DiffTelescopingError;
use crate::holonomic::hyperterm::{as_ratk, rn_to_expr};
use crate::holonomic::qfield::{
rn_add, rn_int, rn_is_zero, rn_mul, rn_rat, rn_var, rn_zero, PolyK, RatK, Rn,
};
use crate::kernel::{ExprId, ExprPool};
use rug::Rational;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IntegrationLimit {
Finite(Rational),
NegInfinity,
PosInfinity,
}
impl IntegrationLimit {
pub fn at(v: i64) -> IntegrationLimit {
IntegrationLimit::Finite(Rational::from(v))
}
fn describe(&self) -> String {
match self {
IntegrationLimit::Finite(q) => q.to_string(),
IntegrationLimit::NegInfinity => "-oo".into(),
IntegrationLimit::PosInfinity => "+oo".into(),
}
}
}
#[derive(Debug, Clone)]
pub enum IntegralBoundaryStatus {
Vanishes {
conditions: Vec<ExprId>,
},
Nonzero {
rhs: ExprId,
conditions: Vec<ExprId>,
},
Unknown {
reason: String,
},
}
impl IntegralBoundaryStatus {
pub fn tag(&self) -> &'static str {
match self {
IntegralBoundaryStatus::Vanishes { .. } => "vanishes",
IntegralBoundaryStatus::Nonzero { .. } => "nonzero",
IntegralBoundaryStatus::Unknown { .. } => "unknown",
}
}
pub fn implies_integral_recurrence(&self) -> bool {
!matches!(self, IntegralBoundaryStatus::Unknown { .. })
}
pub fn conditions(&self) -> &[ExprId] {
match self {
IntegralBoundaryStatus::Vanishes { conditions }
| IntegralBoundaryStatus::Nonzero { conditions, .. } => conditions,
IntegralBoundaryStatus::Unknown { .. } => &[],
}
}
pub fn side_conditions(&self, range: &str, pool: &ExprPool) -> Vec<String> {
let render = |conds: &[ExprId]| -> Vec<String> {
conds
.iter()
.map(|c| format!("{} > 0", pool.display(*c)))
.collect()
};
match self {
IntegralBoundaryStatus::Vanishes { conditions } => {
let mut out = vec![format!(
"the boundary term [R*F] over {range} was proved to vanish in exact \
arithmetic, so the homogeneous recurrence sum_i a_i(n)*f(n+i) = 0 holds \
for the integral"
)];
out.extend(render(conditions));
out.push(
"the verdict is a statement about the n at which the integrand, the \
certificate and the integral are all defined; convergence of the \
integral itself is assumed, not proved here"
.into(),
);
out
}
IntegralBoundaryStatus::Nonzero { conditions, .. } => {
let mut out = vec![format!(
"the boundary term [R*F] over {range} was computed exactly and is not \
identically zero, so the recurrence for the integral is inhomogeneous: \
sum_i a_i(n)*f(n+i) = rhs(n)"
)];
out.extend(render(conditions));
out.push(
"the verdict is a statement about the n at which the integrand, the \
certificate and the integral are all defined; convergence of the \
integral itself is assumed, not proved here"
.into(),
);
out
}
IntegralBoundaryStatus::Unknown { reason } => vec![format!(
"no recurrence for the integral over {range} may be claimed: {reason}. The \
certificate remains a true identity about the integrand"
)],
}
}
}
pub fn integral_boundary_status(
result: &AzResult,
term: ExprId,
n: ExprId,
x: ExprId,
pool: &ExprPool,
lower: &IntegrationLimit,
upper: &IntegrationLimit,
) -> Result<IntegralBoundaryStatus, DiffTelescopingError> {
let f = HyperExpTerm::parse(term, n, x, pool)?;
let r = as_ratk(result.certificate, n, x, pool, 0).ok_or_else(|| {
DiffTelescopingError::InvalidInput(
"the certificate is not a rational function of the two symbols supplied; it must \
come from the same almkvist_zeilberger call as `term`"
.into(),
)
})?;
let g = HyperExpTerm {
rat: f.rat.mul(&r),
w: f.w.clone(),
eta: f.eta.clone(),
powers: f.powers.clone(),
};
if g.rat.is_zero() {
return Ok(IntegralBoundaryStatus::Vanishes { conditions: vec![] });
}
let hi = endpoint_limit(&g, upper, n, pool);
let lo = endpoint_limit(&g, lower, n, pool);
Ok(combine(hi, lo, lower, upper, pool))
}
enum EndpointLimit {
Zero { conditions: Vec<ExprId> },
Finite(ExprId),
Unknown(String),
}
fn endpoint_limit(
g: &HyperExpTerm,
limit: &IntegrationLimit,
n: ExprId,
pool: &ExprPool,
) -> EndpointLimit {
match limit {
IntegrationLimit::Finite(c) => finite_endpoint(g, c, n, pool),
IntegrationLimit::PosInfinity => infinite_endpoint(g, true, n, pool),
IntegrationLimit::NegInfinity => infinite_endpoint(g, false, n, pool),
}
}
fn finite_endpoint(g: &HyperExpTerm, c: &Rational, n: ExprId, pool: &ExprPool) -> EndpointLimit {
let Some(eta_val) = ratk_eval(&g.eta, c) else {
return EndpointLimit::Unknown(format!(
"the exponential argument has a pole at x = {c}, so exp(eta) has no two-sided limit \
there"
));
};
let Some((rat_ord, rat_val)) = ratk_order_and_value(&g.rat, c) else {
return EndpointLimit::Unknown(format!("the rational part of R*F degenerates at x = {c}"));
};
let mut nu = rn_int(rat_ord);
let mut any_power_singular = false;
let mut power_vals: Vec<(Rn, Rn)> = Vec::with_capacity(g.powers.len());
for p in &g.powers {
let Some((ord, val)) = ratk_order_and_value(&p.base, c) else {
return EndpointLimit::Unknown(format!("a power factor's base degenerates at x = {c}"));
};
let e = rn_add(
&rn_mul(&rn_int(p.alpha), &rn_var()),
&rn_rat(p.beta.clone()),
);
if ord != 0 {
any_power_singular = true;
nu = rn_add(&nu, &rn_mul(&e, &rn_int(ord)));
}
power_vals.push((e, val));
}
match positivity(&nu, n, pool) {
Positivity::Positive => EndpointLimit::Zero { conditions: vec![] },
Positivity::Conditional(cond) => EndpointLimit::Zero {
conditions: vec![cond],
},
Positivity::Zero => {
if any_power_singular || rat_ord != 0 {
return EndpointLimit::Unknown(format!(
"the order of R*F at x = {c} is zero only through cancellation between a \
vanishing rational part and a vanishing power factor; the limiting value \
is not determined by this analysis"
));
}
EndpointLimit::Finite(endpoint_value(g, &rat_val, &eta_val, &power_vals, n, pool))
}
Positivity::Negative => EndpointLimit::Unknown(format!(
"R*F is unbounded as x -> {c} (order {}), so the boundary term does not exist",
pool.display(rn_to_expr(pool, n, &nu))
)),
Positivity::Undecided => EndpointLimit::Unknown(format!(
"the order of R*F at x = {c} is {}, whose sign is not an affine condition on n",
pool.display(rn_to_expr(pool, n, &nu))
)),
}
}
fn infinite_endpoint(g: &HyperExpTerm, at_plus: bool, n: ExprId, pool: &ExprPool) -> EndpointLimit {
let where_ = if at_plus { "+oo" } else { "-oo" };
let eta_deg = ratk_degree(&g.eta);
if eta_deg >= 1 {
let Some(lead) = ratk_leading_rational(&g.eta) else {
return EndpointLimit::Unknown(format!(
"the exponential argument grows at {where_} with a leading coefficient that \
depends on n, so the direction of exp(eta) is not decided"
));
};
let sign_at_end = if at_plus || eta_deg % 2 == 0 {
lead.clone()
} else {
-lead.clone()
};
return if sign_at_end < 0 {
EndpointLimit::Zero { conditions: vec![] }
} else {
EndpointLimit::Unknown(format!(
"R*F is unbounded as x -> {where_}: the exponential argument tends to +oo"
))
};
}
let mut delta = rn_int(ratk_degree(&g.rat));
for p in &g.powers {
let e = rn_add(
&rn_mul(&rn_int(p.alpha), &rn_var()),
&rn_rat(p.beta.clone()),
);
delta = rn_add(&delta, &rn_mul(&e, &rn_int(ratk_degree(&p.base))));
}
let neg_delta = rn_mul(&delta, &rn_int(-1));
match positivity(&neg_delta, n, pool) {
Positivity::Positive => EndpointLimit::Zero { conditions: vec![] },
Positivity::Conditional(cond) => EndpointLimit::Zero {
conditions: vec![cond],
},
Positivity::Zero | Positivity::Negative => EndpointLimit::Unknown(format!(
"R*F does not tend to 0 as x -> {where_}: its algebraic degree there is {}",
pool.display(rn_to_expr(pool, n, &delta))
)),
Positivity::Undecided => EndpointLimit::Unknown(format!(
"the degree of R*F at {where_} is {}, whose sign is not an affine condition on n",
pool.display(rn_to_expr(pool, n, &delta))
)),
}
}
fn endpoint_value(
g: &HyperExpTerm,
rat_val: &Rn,
eta_val: &Rn,
power_vals: &[(Rn, Rn)],
n: ExprId,
pool: &ExprPool,
) -> ExprId {
let mut factors = vec![rn_to_expr(pool, n, rat_val)];
if g.w != 1 {
factors.push(pool.pow(rational_expr(pool, &g.w), n));
}
if !rn_is_zero(eta_val) {
factors.push(pool.func("exp", vec![rn_to_expr(pool, n, eta_val)]));
}
for (e, val) in power_vals {
if rn_is_zero(e) || val.eq(&crate::holonomic::qfield::rn_one()) {
continue;
}
factors.push(pool.pow(rn_to_expr(pool, n, val), rn_to_expr(pool, n, e)));
}
crate::simplify::simplify(pool.mul(factors), pool).value
}
fn combine(
hi: EndpointLimit,
lo: EndpointLimit,
lower: &IntegrationLimit,
upper: &IntegrationLimit,
pool: &ExprPool,
) -> IntegralBoundaryStatus {
let range = format!("x = {}..{}", lower.describe(), upper.describe());
match (hi, lo) {
(EndpointLimit::Unknown(r), _) => IntegralBoundaryStatus::Unknown {
reason: format!("at the upper limit of {range}: {r}"),
},
(_, EndpointLimit::Unknown(r)) => IntegralBoundaryStatus::Unknown {
reason: format!("at the lower limit of {range}: {r}"),
},
(EndpointLimit::Zero { conditions: mut a }, EndpointLimit::Zero { conditions: b }) => {
for c in b {
if !a.contains(&c) {
a.push(c);
}
}
IntegralBoundaryStatus::Vanishes { conditions: a }
}
(EndpointLimit::Finite(v), EndpointLimit::Zero { conditions }) => {
IntegralBoundaryStatus::Nonzero { rhs: v, conditions }
}
(EndpointLimit::Zero { conditions }, EndpointLimit::Finite(v)) => {
IntegralBoundaryStatus::Nonzero {
rhs: crate::simplify::simplify(pool.mul(vec![pool.integer(-1_i32), v]), pool).value,
conditions,
}
}
(EndpointLimit::Finite(u), EndpointLimit::Finite(l)) => {
if u == l {
IntegralBoundaryStatus::Vanishes { conditions: vec![] }
} else {
IntegralBoundaryStatus::Unknown {
reason: format!(
"both endpoints of {range} are finite and nonzero ({} and {}), and their \
difference could not be shown to be nonzero without leaving exact \
arithmetic",
pool.display(u),
pool.display(l)
),
}
}
}
}
}
enum Positivity {
Positive,
Zero,
Negative,
Conditional(ExprId),
Undecided,
}
fn positivity(v: &Rn, n: ExprId, pool: &ExprPool) -> Positivity {
if v.den.degree() != 0 {
return Positivity::Undecided;
}
match v.num.degree() {
d if d < 0 => Positivity::Zero,
0 => {
let Some(q) = rn_as_rational(v) else {
return Positivity::Undecided;
};
if q > 0 {
Positivity::Positive
} else if q == 0 {
Positivity::Zero
} else {
Positivity::Negative
}
}
1 => Positivity::Conditional(rn_to_expr(pool, n, v)),
_ => Positivity::Undecided,
}
}
fn rn_as_rational(v: &Rn) -> Option<Rational> {
if v.num.degree() > 0 || v.den.degree() > 0 {
return None;
}
let a = v
.num
.coeffs
.first()
.cloned()
.unwrap_or_else(|| Rational::from(0));
let b = v.den.coeffs.first().cloned()?;
if b == 0 {
return None;
}
Some(a / b)
}
fn rational_expr(pool: &ExprPool, q: &Rational) -> ExprId {
if *q.clone().denom() == 1 {
pool.integer(q.numer().clone())
} else {
pool.mul(vec![
pool.integer(q.numer().clone()),
pool.pow(pool.integer(q.denom().clone()), pool.integer(-1_i32)),
])
}
}
fn polyk_eval(p: &PolyK, c: &Rational) -> Rn {
let cc = rn_rat(c.clone());
let mut acc = rn_zero();
for coeff in p.coeffs.iter().rev() {
acc = rn_add(&rn_mul(&acc, &cc), coeff);
}
acc
}
fn polyk_deflate(p: &PolyK, c: &Rational) -> PolyK {
let d = p.degree();
if d <= 0 {
return PolyK::zero();
}
let d = d as usize;
let cc = rn_rat(c.clone());
let mut q = vec![rn_zero(); d];
q[d - 1] = p.coeff(d);
for i in (1..d).rev() {
q[i - 1] = rn_add(&p.coeff(i), &rn_mul(&cc, &q[i]));
}
PolyK::from_coeffs(q)
}
fn polyk_order_and_value(p: &PolyK, c: &Rational) -> Option<(i64, Rn)> {
if p.is_zero() {
return None;
}
let mut cur = p.clone();
let mut m = 0i64;
loop {
let val = polyk_eval(&cur, c);
if !rn_is_zero(&val) {
return Some((m, val));
}
if cur.degree() <= 0 {
return None;
}
cur = polyk_deflate(&cur, c);
m += 1;
}
}
fn ratk_order_and_value(r: &RatK, c: &Rational) -> Option<(i64, Rn)> {
let (mn, vn) = polyk_order_and_value(&r.num, c)?;
let (md, vd) = polyk_order_and_value(&r.den, c)?;
let v = crate::holonomic::qfield::rn_div(&vn, &vd)?;
Some((mn - md, v))
}
fn ratk_eval(r: &RatK, c: &Rational) -> Option<Rn> {
let den = polyk_eval(&r.den, c);
if rn_is_zero(&den) {
return None;
}
crate::holonomic::qfield::rn_div(&polyk_eval(&r.num, c), &den)
}
fn ratk_degree(r: &RatK) -> i64 {
(r.num.degree() as i64) - (r.den.degree() as i64)
}
fn ratk_leading_rational(r: &RatK) -> Option<Rational> {
let lead = crate::holonomic::qfield::rn_div(&r.num.leading_coeff(), &r.den.leading_coeff())?;
rn_as_rational(&lead)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holonomic::azeil::{almkvist_zeilberger, AzOpts};
use crate::kernel::Domain;
fn nx(pool: &ExprPool) -> (ExprId, ExprId) {
(
pool.symbol("n", Domain::Real),
pool.symbol("x", Domain::Real),
)
}
fn verdict(
f: ExprId,
n: ExprId,
x: ExprId,
pool: &ExprPool,
lo: IntegrationLimit,
hi: IntegrationLimit,
) -> (AzResult, IntegralBoundaryStatus) {
let out = almkvist_zeilberger(f, n, x, pool, &AzOpts::default()).expect("certificate");
let status = integral_boundary_status(&out.value, f, n, x, pool, &lo, &hi)
.expect("boundary analysis");
(out.value, status)
}
#[test]
fn gamma_boundary_vanishes() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![
pool.pow(x, n),
pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), x])]),
]);
let (r, status) = verdict(
f,
n,
x,
&pool,
IntegrationLimit::at(0),
IntegrationLimit::PosInfinity,
);
assert_eq!(r.order, 1);
assert_eq!(status.tag(), "vanishes");
assert!(status.implies_integral_recurrence());
assert_eq!(
status.conditions().len(),
1,
"x^(n+1) -> 0 at the origin only for n + 1 > 0"
);
let conds = status.side_conditions("x = 0..+oo", &pool);
assert!(
conds.iter().any(|c| c.contains("> 0")),
"the positivity condition must be reported: {conds:?}"
);
}
#[test]
fn gaussian_boundary_vanishes_unconditionally() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![
pool.pow(x, pool.mul(vec![pool.integer(2_i32), n])),
pool.func(
"exp",
vec![pool.mul(vec![pool.integer(-1_i32), pool.pow(x, pool.integer(2_i32))])],
),
]);
let (_, status) = verdict(
f,
n,
x,
&pool,
IntegrationLimit::NegInfinity,
IntegrationLimit::PosInfinity,
);
assert_eq!(status.tag(), "vanishes");
assert!(
status.conditions().is_empty(),
"exp(-x^2) needs no condition on n at either end"
);
}
#[test]
fn nonvanishing_boundary_is_reported_as_nonzero() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.pow(x, n);
let (r, status) = verdict(
f,
n,
x,
&pool,
IntegrationLimit::at(0),
IntegrationLimit::at(1),
);
assert_eq!(r.order, 0, "F = D_x(x/(n+1) * F)");
match &status {
IntegralBoundaryStatus::Nonzero { rhs, conditions } => {
for ni in [2.0_f64, 7.0] {
let env = std::collections::HashMap::from([(n, ni)]);
let got = crate::eval_f64(*rhs, &pool, &env).expect("rhs evaluates");
assert!(
(got - 1.0 / (ni + 1.0)).abs() < 1e-12,
"boundary term at n={ni}: got {got}, want {}",
1.0 / (ni + 1.0)
);
}
assert_eq!(conditions.len(), 1, "x^(n+1) -> 0 at 0 needs n + 1 > 0");
}
other => panic!("expected Nonzero, got {other:?}"),
}
assert!(status.implies_integral_recurrence());
}
#[test]
fn divergent_upper_limit_is_unknown() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![pool.pow(x, n), pool.func("exp", vec![x])]);
let (_, status) = verdict(
f,
n,
x,
&pool,
IntegrationLimit::at(0),
IntegrationLimit::PosInfinity,
);
assert_eq!(status.tag(), "unknown");
assert!(
!status.implies_integral_recurrence(),
"nothing may be claimed about a divergent integral"
);
}
#[test]
fn central_beta_boundary_vanishes() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let one_minus = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let f = pool.mul(vec![pool.pow(x, n), pool.pow(one_minus, n)]);
let (r, status) = verdict(
f,
n,
x,
&pool,
IntegrationLimit::at(0),
IntegrationLimit::at(1),
);
assert_eq!(r.order, 1);
assert_eq!(status.tag(), "vanishes", "got {status:?}");
assert!(
!status.conditions().is_empty(),
"the endpoints vanish only for n large enough"
);
}
#[test]
fn unknown_reads_as_unknown() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![pool.pow(x, n), pool.func("exp", vec![x])]);
let (_, status) = verdict(
f,
n,
x,
&pool,
IntegrationLimit::at(0),
IntegrationLimit::PosInfinity,
);
let sc = status.side_conditions("x = 0..+oo", &pool);
assert!(
sc.iter().any(|s| s.contains("may be claimed")),
"an Unknown verdict must state that nothing follows: {sc:?}"
);
}
#[test]
fn certificate_from_a_different_call_is_refused() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.pow(x, n);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let mut bogus = out.value.clone();
bogus.certificate = pool.func("exp", vec![x]);
let err = integral_boundary_status(
&bogus,
f,
n,
x,
&pool,
&IntegrationLimit::at(0),
&IntegrationLimit::at(1),
)
.expect_err("a non-rational certificate is not from this engine");
assert!(matches!(err, DiffTelescopingError::InvalidInput(_)));
}
}