use super::hyperexp::HyperExpTerm;
use super::rde::{rescale, solve_probe, verify, RdeSetup};
use super::{AzOpts, DiffTelescopingError};
use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::holonomic::hyperterm::{ratk_to_expr, ratuni_to_expr};
use crate::holonomic::qfield::{clear_denominators, rn_is_zero, rn_poly, RatK};
use crate::kernel::{ExprId, ExprPool};
use crate::matrix::normal_form::RatUniPoly;
#[derive(Debug, Clone)]
pub struct AzResult {
pub order: usize,
pub coeffs: Vec<ExprId>,
pub certificate: ExprId,
pub probes: usize,
}
pub fn integrand_antiderivative(result: &AzResult, term: ExprId, pool: &ExprPool) -> ExprId {
crate::simplify::simplify(pool.mul(vec![result.certificate, term]), pool).value
}
pub fn almkvist_zeilberger(
term: ExprId,
n: ExprId,
x: ExprId,
pool: &ExprPool,
opts: &AzOpts,
) -> Result<DerivedExpr<AzResult>, DiffTelescopingError> {
if n == x {
return Err(DiffTelescopingError::InvalidInput(
"the outer index n and the integration variable x must be distinct symbols".into(),
));
}
let f = HyperExpTerm::parse(term, n, x, pool)?;
let theta = f.theta()?;
let mut ratios: Vec<RatK> = Vec::with_capacity(opts.max_order + 1);
let mut ratio_failure: Option<DiffTelescopingError> = None;
let mut probes = 0usize;
for order in 0..=opts.max_order {
while ratios.len() <= order {
match f.ratio_n(ratios.len() as i64) {
Ok(r) => ratios.push(r),
Err(e) => {
ratio_failure = Some(e);
break;
}
}
}
if ratios.len() <= order {
break;
}
let Some(setup) = RdeSetup::new(&theta, ratios.clone()) else {
continue;
};
for kappa in 0..=opts.max_den_power {
for d in 0..=opts.max_degree {
probes += 1;
let Some(cand) = solve_probe(&setup, kappa, d) else {
continue;
};
let a_int: Vec<RatUniPoly> = clear_denominators(&cand.a);
if a_int.iter().all(|p| p.is_zero()) {
continue;
}
let scale = rn_poly(a_int[order].clone());
if rn_is_zero(&scale) {
continue;
}
let (a_scaled, r_final) = rescale(&cand.a, &cand.r, &scale);
if !verify(&setup, &a_scaled, &r_final) {
continue;
}
let coeffs: Vec<ExprId> =
a_int.iter().map(|p| ratuni_to_expr(pool, n, p)).collect();
let certificate = ratk_to_expr(pool, n, x, &r_final);
let mut log = DerivationLog::new();
log.push(RewriteStep::simple(
"almkvist_zeilberger_certificate",
term,
certificate,
));
return Ok(DerivedExpr::with_log(
AzResult {
order,
coeffs,
certificate,
probes,
},
log,
));
}
}
}
if let Some(e) = ratio_failure {
return Err(e);
}
Err(DiffTelescopingError::SearchExhausted(format!(
"no verified relation of order <= {} with certificate numerator degree <= {} and \
denominator power <= {} was found for {} ({probes} probes)",
opts.max_order,
opts.max_degree,
opts.max_den_power,
pool.display(term)
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
use std::collections::HashMap;
fn nx(pool: &ExprPool) -> (ExprId, ExprId) {
(
pool.symbol("n", Domain::Real),
pool.symbol("x", Domain::Real),
)
}
fn assert_ratio(r: &AzResult, pool: &ExprPool, n: ExprId, i: usize, want: fn(f64) -> f64) {
for ni in [2.0_f64, 5.0, 9.5] {
let env = HashMap::from([(n, ni)]);
let aj = crate::eval_f64(r.coeffs[r.order], pool, &env).expect("a_J evaluates");
let ai = crate::eval_f64(r.coeffs[i], pool, &env).expect("a_i evaluates");
assert!(aj.abs() > 1e-12, "leading coefficient must not vanish");
let got = ai / aj;
let expect = want(ni);
assert!(
(got - expect).abs() < 1e-9,
"a_{i}/a_{J} at n={ni}: got {got}, want {expect}",
J = r.order
);
}
}
#[test]
fn gamma_moments() {
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 out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 1, "expected the order-1 Gamma recurrence");
assert_ratio(r, &pool, n, 0, |v| -(v + 1.0));
}
#[test]
fn beta_integral() {
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, pool.rational(1, 2)),
]);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 1);
assert_ratio(r, &pool, n, 0, |v| -(v + 1.0) / (v + 2.5));
}
#[test]
fn beta_integral_with_an_integer_exponent_collapses_to_order_zero() {
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, pool.integer(3_i32)),
]);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
assert_eq!(out.value.order, 0);
}
#[test]
fn central_beta_integral() {
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 out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 1);
assert_ratio(r, &pool, n, 0, |v| -(v + 1.0) / (2.0 * (2.0 * v + 3.0)));
}
#[test]
fn gaussian_even_moments() {
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 out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 1);
assert_ratio(r, &pool, n, 0, |v| -(2.0 * v + 1.0) / 2.0);
}
#[test]
fn half_gaussian_moments_are_order_two() {
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), pool.pow(x, pool.integer(2_i32))])],
),
]);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 2, "f(n+2) = ((n+1)/2)·f(n) is an order-2 relation");
assert_ratio(r, &pool, n, 1, |_| 0.0);
assert_ratio(r, &pool, n, 0, |v| -(v + 1.0) / 2.0);
}
#[test]
fn bessel_contour_representation() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let arg = pool.add(vec![
x,
pool.mul(vec![
pool.integer(-1_i32),
pool.pow(x, pool.integer(-1_i32)),
]),
]);
let f = pool.mul(vec![
pool.func("exp", vec![arg]),
pool.pow(
x,
pool.add(vec![
pool.mul(vec![pool.integer(-1_i32), n]),
pool.integer(-1_i32),
]),
),
]);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 2, "Bessel's is a three-term recurrence");
assert_ratio(r, &pool, n, 1, |v| -(v + 1.0));
assert_ratio(r, &pool, n, 0, |_| 1.0);
}
#[test]
fn legendre_schlafli_representation() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let x2m1 = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(-1_i32)]);
let xm3 = pool.add(vec![x, pool.integer(-3_i32)]);
let f = pool.mul(vec![
pool.pow(x2m1, n),
pool.pow(pool.integer(2_i32), pool.mul(vec![pool.integer(-1_i32), n])),
pool.pow(
xm3,
pool.add(vec![
pool.mul(vec![pool.integer(-1_i32), n]),
pool.integer(-1_i32),
]),
),
]);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let r = &out.value;
assert_eq!(r.order, 2);
assert_ratio(r, &pool, n, 1, |v| -3.0 * (2.0 * v + 3.0) / (v + 2.0));
assert_ratio(r, &pool, n, 0, |v| (v + 1.0) / (v + 2.0));
}
#[test]
fn order_zero_is_reachable() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![
x,
pool.func(
"exp",
vec![pool.mul(vec![pool.integer(-1_i32), pool.pow(x, pool.integer(2_i32))])],
),
]);
let out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
assert_eq!(out.value.order, 0);
}
#[test]
fn out_of_class_integrand_is_refused() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![pool.pow(x, n), pool.func("log", vec![x])]);
let err = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default())
.expect_err("log is out of class");
assert!(matches!(err, DiffTelescopingError::NotHyperexponential(_)));
}
#[test]
fn non_hypergeometric_in_n_is_refused_specifically() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![n, x])]),
pool.pow(x, pool.integer(-1_i32)),
]);
let err = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default())
.expect_err("exp(n*x)/x has no relation in this family");
assert!(
matches!(err, DiffTelescopingError::NotHypergeometricInN(_)),
"expected the n-side refusal, got {err:?}"
);
}
#[test]
fn identical_symbols_are_refused() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let err = almkvist_zeilberger(x, x, x, &pool, &AzOpts::default())
.expect_err("n and x must differ");
assert!(matches!(err, DiffTelescopingError::InvalidInput(_)));
}
#[test]
fn bounds_too_small_refuse_rather_than_guess() {
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 opts = AzOpts {
max_order: 0,
max_degree: 4,
max_den_power: 1,
};
let err = almkvist_zeilberger(f, n, x, &pool, &opts).expect_err("bounds exhausted");
assert!(matches!(err, DiffTelescopingError::SearchExhausted(_)));
}
#[test]
fn antiderivative_is_available() {
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 out = almkvist_zeilberger(f, n, x, &pool, &AzOpts::default()).expect("certificate");
let g = integrand_antiderivative(&out.value, f, &pool);
let s = format!("{}", pool.display(g));
assert!(s.contains("exp"), "G must still carry the integrand: {s}");
}
}