pub mod boundary;
mod poly;
mod search;
mod term;
pub use boundary::{boundary_status_2d, boundary_status_md, BoundaryStatus2d, BoundaryStatusMd};
pub use search::{
telescope2d_search, telescope_md_search, Telescoping2dOpts, Telescoping2dResult,
TelescopingMdOpts, TelescopingMdResult,
};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Telescoping2dError {
NotProperHypergeometric(String),
SearchExhausted(String),
InvalidInput(String),
}
impl fmt::Display for Telescoping2dError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Telescoping2dError::NotProperHypergeometric(s) => {
write!(f, "telescoping2d: not a proper hypergeometric term: {s}")
}
Telescoping2dError::SearchExhausted(s) => {
write!(f, "telescoping2d: search exhausted: {s}")
}
Telescoping2dError::InvalidInput(s) => {
write!(f, "telescoping2d: invalid input: {s}")
}
}
}
}
impl std::error::Error for Telescoping2dError {}
impl crate::errors::AlkahestError for Telescoping2dError {
fn code(&self) -> &'static str {
match self {
Telescoping2dError::NotProperHypergeometric(_) => "E-HOLO-040",
Telescoping2dError::SearchExhausted(_) => "E-HOLO-041",
Telescoping2dError::InvalidInput(_) => "E-HOLO-042",
}
}
fn remediation(&self) -> Option<&'static str> {
Some(match self {
Telescoping2dError::NotProperHypergeometric(_) => {
"rewrite the term as R(n,j,k)*z1**j*z2**k*w**n*prod(gamma(a*n+b*j+c*k+d)**e) \
with integer a, b, c; supported function heads are gamma, factorial, \
binomial, pochhammer"
}
Telescoping2dError::SearchExhausted(_) => {
"raise max_order, max_a_degree and/or max_cert_degree in Telescoping2dOpts; if \
the term genuinely has no such double-sum certificate within reach — or needs \
a certificate denominator this module's fixed-denominator ansatz cannot \
represent — this method does not apply"
}
Telescoping2dError::InvalidInput(_) => {
"n and every bound index must be pairwise distinct symbols, and at least one \
bound index must be supplied"
}
})
}
}
pub fn telescope2d(
term: crate::kernel::ExprId,
n: crate::kernel::ExprId,
j: crate::kernel::ExprId,
k: crate::kernel::ExprId,
pool: &crate::kernel::ExprPool,
) -> Result<Telescoping2dResult, Telescoping2dError> {
telescope2d_search(term, n, j, k, pool, &Telescoping2dOpts::default())
}
pub fn telescope_md(
term: crate::kernel::ExprId,
n: crate::kernel::ExprId,
indices: &[crate::kernel::ExprId],
pool: &crate::kernel::ExprPool,
) -> Result<TelescopingMdResult, Telescoping2dError> {
search::telescope_md_search(term, n, indices, pool, &TelescopingMdOpts::default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::AlkahestError as _;
use crate::kernel::{Domain, ExprId, ExprPool};
use rug::ops::Pow as _;
use rug::Integer;
fn njk(pool: &ExprPool) -> (ExprId, ExprId, ExprId) {
(
pool.symbol("n", Domain::Real),
pool.symbol("j", Domain::Real),
pool.symbol("k", Domain::Real),
)
}
fn binom_i(top: i64, bot: i64) -> Integer {
if bot < 0 || bot > top {
return Integer::from(0);
}
let mut acc = Integer::from(1);
for t in 0..bot {
acc *= Integer::from(top - t);
acc /= Integer::from(t + 1);
}
acc
}
fn coeff_at_n(pool: &ExprPool, coeff: ExprId, n: ExprId, ni: i64) -> rug::Rational {
let env = std::collections::HashMap::from([(n, ni as f64)]);
let v = crate::eval_f64(coeff, pool, &env).expect("a_i(n) evaluates");
assert!(
(v - v.round()).abs() < 1e-6,
"expected an integer coefficient, got {v}"
);
rug::Rational::from(v.round() as i64)
}
#[test]
fn separable_product_matches_known_recurrence() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let f = pool.mul(vec![
pool.func("binomial", vec![n, j]),
pool.func("binomial", vec![n, k]),
]);
let result = telescope2d(f, n, j, k, &pool).expect("certificate for C(n,j)*C(n,k)");
let s = |ni: i64| -> rug::Rational {
let mut acc = Integer::from(0);
for jj in 0..=40 {
for kk in 0..=40 {
acc += binom_i(ni, jj) * binom_i(ni, kk);
}
}
rug::Rational::from(acc)
};
for ni in 0..=6 {
assert_eq!(s(ni), rug::Rational::from(Integer::from(4).pow(ni as u32)));
}
assert_annihilates(&result.coeffs, n, &pool, &s, 0, 5);
}
#[test]
fn non_separable_double_binomial_matches_known_closed_form() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let f = pool.mul(vec![
pool.func("binomial", vec![n, j]),
pool.func("binomial", vec![j, k]),
]);
let result = telescope2d(f, n, j, k, &pool).expect("certificate for C(n,j)*C(j,k)");
let s = |ni: i64| -> rug::Rational {
let mut acc = Integer::from(0);
for jj in 0..=40 {
for kk in 0..=40 {
acc += binom_i(ni, jj) * binom_i(jj, kk);
}
}
rug::Rational::from(acc)
};
for ni in 0..=6 {
assert_eq!(s(ni), rug::Rational::from(Integer::from(3).pow(ni as u32)));
}
assert_annihilates(&result.coeffs, n, &pool, &s, 0, 5);
let lo = pool.integer(0_i32);
let hi = pool.integer(40_i32);
let status = boundary_status_2d(&result, f, n, j, k, (lo, hi), (lo, hi), &pool);
assert_eq!(
status.tag(),
"unknown",
"a constant rectangle cannot soundly certify Vanishes when the true support \
grows with n; got {status:?}"
);
}
#[test]
fn non_separable_fixed_support_boundary_vanishes() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let two_n = pool.pow(pool.integer(2_i32), n);
let f = pool.mul(vec![
two_n,
pool.func("binomial", vec![pool.integer(10_i32), j]),
pool.func("binomial", vec![j, k]),
]);
let result = telescope2d(f, n, j, k, &pool).expect("certificate for 2^n*C(10,j)*C(j,k)");
let s = |ni: i64| -> rug::Rational {
let mut acc = Integer::from(0);
for jj in 0..=10 {
for kk in 0..=10 {
acc += binom_i(10, jj) * binom_i(jj, kk);
}
}
rug::Rational::from(acc * Integer::from(2).pow(ni.max(0) as u32))
};
for ni in 0..=6 {
assert_eq!(
s(ni),
rug::Rational::from(Integer::from(2).pow(ni as u32) * Integer::from(3).pow(10))
);
}
assert_annihilates(&result.coeffs, n, &pool, &s, 0, 5);
let lo = pool.integer(0_i32);
let hi = pool.integer(15_i32);
let status = boundary_status_2d(&result, f, n, j, k, (lo, hi), (lo, hi), &pool);
assert_eq!(
status.tag(),
"vanishes",
"expected the natural (n-independent) boundary to vanish, got {status:?}"
);
}
fn assert_annihilates(
coeffs: &[ExprId],
n: ExprId,
pool: &ExprPool,
s: &dyn Fn(i64) -> rug::Rational,
lo: i64,
hi: i64,
) {
for ni in lo..=hi {
let mut total = rug::Rational::from(0);
for (i, &c) in coeffs.iter().enumerate() {
let ai = coeff_at_n(pool, c, n, ni + i as i64);
total += ai * s(ni + i as i64);
}
assert_eq!(
total,
rug::Rational::from(0),
"recurrence must annihilate S(n) exactly at n = {ni}"
);
}
}
#[test]
fn refuses_non_hypergeometric_input() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let bad = pool.func("sin", vec![pool.mul(vec![n, j, k])]);
let err = telescope2d(bad, n, j, k, &pool).expect_err("not hypergeometric");
assert!(matches!(
err,
Telescoping2dError::NotProperHypergeometric(_)
));
assert_eq!(crate::errors::AlkahestError::code(&err), "E-HOLO-040");
}
fn multinom_i(n: i64, x: i64, y: i64, z: i64) -> Integer {
if x < 0 || y < 0 || z < 0 || x + y + z > n {
return Integer::from(0);
}
binom_i(n, x) * binom_i(n - x, y) * binom_i(n - x - y, z)
}
#[test]
fn multinomial_matches_known_closed_form() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let z = pool.symbol("z", Domain::Real);
let rest = pool.add(vec![
n,
pool.mul(vec![x, pool.integer(-1_i32)]),
pool.mul(vec![y, pool.integer(-1_i32)]),
pool.mul(vec![z, pool.integer(-1_i32)]),
]);
let inv_fact = |e: ExprId| pool.pow(pool.func("factorial", vec![e]), pool.integer(-1_i32));
let f = pool.mul(vec![
pool.func("factorial", vec![n]),
inv_fact(x),
inv_fact(y),
inv_fact(z),
inv_fact(rest),
]);
let opts = TelescopingMdOpts {
max_order: 1,
max_a_degree: 1,
max_cert_degree: 2,
};
let result = search::telescope_md_search(f, n, &[x, y, z], &pool, &opts)
.expect("certificate for the 4-category multinomial coefficient");
assert_eq!(result.certs.len(), 3);
let s = |ni: i64| -> rug::Rational {
let mut acc = Integer::from(0);
for xx in 0..=15 {
for yy in 0..=15 {
for zz in 0..=15 {
acc += multinom_i(ni, xx, yy, zz);
}
}
}
rug::Rational::from(acc)
};
for ni in 0..=6 {
assert_eq!(s(ni), rug::Rational::from(Integer::from(4).pow(ni as u32)));
}
assert_annihilates(&result.coeffs, n, &pool, &s, 0, 4);
let lo = pool.integer(0_i32);
let hi = pool.integer(15_i32);
let status = boundary_status_md(
&result,
f,
n,
&[x, y, z],
&[(lo, hi), (lo, hi), (lo, hi)],
&pool,
);
assert_eq!(
status.tag(),
"unknown",
"a constant box cannot soundly certify Vanishes when the true support grows with \
n; got {status:?}"
);
}
#[test]
fn multinomial_fixed_support_boundary_vanishes() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let z = pool.symbol("z", Domain::Real);
let four_n = pool.pow(pool.integer(4_i32), n);
let ten = pool.integer(10_i32);
let rest = pool.add(vec![
ten,
pool.mul(vec![x, pool.integer(-1_i32)]),
pool.mul(vec![y, pool.integer(-1_i32)]),
pool.mul(vec![z, pool.integer(-1_i32)]),
]);
let inv_fact = |e: ExprId| pool.pow(pool.func("factorial", vec![e]), pool.integer(-1_i32));
let f = pool.mul(vec![
four_n,
pool.func("factorial", vec![ten]),
inv_fact(x),
inv_fact(y),
inv_fact(z),
inv_fact(rest),
]);
let opts = TelescopingMdOpts {
max_order: 1,
max_a_degree: 1,
max_cert_degree: 2,
};
let result = search::telescope_md_search(f, n, &[x, y, z], &pool, &opts)
.expect("certificate for 4^n*10!/(x!y!z!(10-x-y-z)!)");
let inner: Integer = {
let mut acc = Integer::from(0);
for xx in 0..=10 {
for yy in 0..=10 {
for zz in 0..=10 {
acc += multinom_i(10, xx, yy, zz);
}
}
}
acc
};
assert_eq!(inner, Integer::from(4).pow(10));
let s = |ni: i64| -> rug::Rational {
rug::Rational::from(inner.clone() * Integer::from(4).pow(ni.max(0) as u32))
};
for ni in 0..=6 {
assert_eq!(
s(ni),
rug::Rational::from(Integer::from(4).pow(ni as u32) * Integer::from(4).pow(10))
);
}
assert_annihilates(&result.coeffs, n, &pool, &s, 0, 4);
let lo = pool.integer(0_i32);
let hi = pool.integer(15_i32);
let status = boundary_status_md(
&result,
f,
n,
&[x, y, z],
&[(lo, hi), (lo, hi), (lo, hi)],
&pool,
);
assert_eq!(
status.tag(),
"vanishes",
"expected the natural (n-independent) boundary to vanish, got {status:?}"
);
}
#[test]
fn telescope_md_single_index_smoke_test() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let k = pool.symbol("k", Domain::Real);
let f = pool.func("binomial", vec![n, k]);
let result = telescope_md(f, n, &[k], &pool).expect("certificate for C(n,k)");
assert_eq!(result.certs.len(), 1);
}
#[test]
fn a_memory_ceiling_refuses_instead_of_aborting_the_process() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let f = pool.mul(vec![
pool.func("binomial", vec![n, x]),
pool.func("binomial", vec![x, y]),
]);
let _guard = crate::budget::enter_with_memory(crate::budget::Budget::new(), Some(1024));
let err = search::telescope_md_search(f, n, &[x, y], &pool, &TelescopingMdOpts::default())
.expect_err("1 KiB of exact-rational memory cannot cover this search");
assert!(
matches!(err, Telescoping2dError::SearchExhausted(_)),
"{err:?}"
);
let trip = crate::budget::take_trip().expect("the memory trip must be recorded");
assert_eq!(trip.code(), "E-BUDGET-004");
}
#[test]
fn a_generous_memory_ceiling_still_returns_the_certificate() {
let pool = ExprPool::new();
let (n, k, _) = njk(&pool);
let f = pool.func("binomial", vec![n, k]);
let _guard = crate::budget::enter_with_memory(crate::budget::Budget::new(), Some(1 << 40));
let res = telescope_md(f, n, &[k], &pool).expect("a 1 TiB ceiling cannot refuse this");
assert!(res.order >= 1);
}
#[test]
fn chained_product_at_original_bounds_refuses_fast_via_resource_ceiling() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let z = pool.symbol("z", Domain::Real);
let f = pool.mul(vec![
pool.func("binomial", vec![n, x]),
pool.func("binomial", vec![x, y]),
pool.func("binomial", vec![y, z]),
]);
let opts = TelescopingMdOpts {
max_order: 2,
max_a_degree: 2,
max_cert_degree: 3,
};
let ceilings = search::Ceilings {
max_ansatz_unknowns: 80,
large_probe_threshold: 40,
max_cumulative_large_probe_unknowns: 100,
};
let (result, stats) =
search::telescope_md_search_instrumented(f, n, &[x, y, z], &pool, &opts, &ceilings);
let err = result
.expect_err("this combination must be refused via the resource ceiling, not solved");
assert!(
stats.large_attempted >= 1,
"this input must still reach at least one large probe, or the ceiling under test is \
never exercised; got {stats:?}"
);
assert!(
stats.large_attempted <= 2,
"expected a bounded refusal (roughly one expensive elimination, not several); got \
{stats:?}"
);
assert!(
stats.large_unknowns_spent <= ceilings.max_cumulative_large_probe_unknowns,
"large-probe work spent must stay inside max_cumulative_large_probe_unknowns ({}); \
got {stats:?}",
ceilings.max_cumulative_large_probe_unknowns
);
assert!(
stats.skipped_per_probe_ceiling > 0,
"expected the per-probe ceiling to have refused at least one probe; got {stats:?}"
);
assert!(
stats.skipped_cumulative_ceiling > 0,
"expected the cumulative ceiling to have refused at least one repeat of the \
expensive probe; got {stats:?}"
);
assert!(matches!(err, Telescoping2dError::SearchExhausted(_)));
let Telescoping2dError::SearchExhausted(msg) = &err else {
unreachable!()
};
assert!(
msg.contains("MAX_ANSATZ_UNKNOWNS"),
"expected the SearchExhausted message to name the resource ceiling as the reason, \
got: {msg}"
);
}
#[test]
fn production_ceilings_classify_the_chained_product_probe_ladder() {
let c = search::Ceilings::PRODUCTION;
let m = 3_usize;
let (order, a_degree) = (1_usize, 0_usize);
let a_count = (order + 1) * (a_degree + 1);
let total = |cert_degree: usize| a_count + m * (cert_degree + 1).pow(m as u32 + 1);
assert_eq!([total(0), total(1), total(2), total(3)], [5, 50, 245, 770]);
for d in [0, 1] {
assert!(
total(d) < c.large_probe_threshold,
"cert_degree {d} ({} unknowns) should be below LARGE_PROBE_THRESHOLD {}",
total(d),
c.large_probe_threshold
);
}
assert!(total(2) >= c.large_probe_threshold);
assert!(total(2) <= c.max_ansatz_unknowns);
assert!(
total(2) <= c.max_cumulative_large_probe_unknowns,
"the cumulative budget must admit one 245-unknown probe"
);
assert!(
2 * total(2) > c.max_cumulative_large_probe_unknowns,
"the cumulative budget must refuse a *second* 245-unknown probe"
);
assert!(
total(3) > c.max_ansatz_unknowns,
"the 770-unknown probe must be refused outright"
);
}
}