use super::poly::{Axis, PolyM, RatM, AXIS_N};
use super::term::ProperTermM;
use super::Telescoping2dError;
use crate::kernel::{ExprId, ExprPool};
use rug::{Integer, Rational};
pub(crate) const MAX_ANSATZ_UNKNOWNS: usize = 400;
pub(crate) const LARGE_PROBE_THRESHOLD: usize = 150;
pub(crate) const MAX_CUMULATIVE_LARGE_PROBE_UNKNOWNS: usize = 300;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Ceilings {
pub max_ansatz_unknowns: usize,
pub large_probe_threshold: usize,
pub max_cumulative_large_probe_unknowns: usize,
}
impl Ceilings {
pub(crate) const PRODUCTION: Ceilings = Ceilings {
max_ansatz_unknowns: MAX_ANSATZ_UNKNOWNS,
large_probe_threshold: LARGE_PROBE_THRESHOLD,
max_cumulative_large_probe_unknowns: MAX_CUMULATIVE_LARGE_PROBE_UNKNOWNS,
};
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct SearchStats {
#[cfg_attr(not(test), allow(dead_code))]
pub attempted: usize,
#[cfg_attr(not(test), allow(dead_code))]
pub large_attempted: usize,
pub large_unknowns_spent: usize,
pub skipped_per_probe_ceiling: usize,
pub skipped_cumulative_ceiling: usize,
}
impl SearchStats {
fn any_ceiling_fired(&self) -> bool {
self.skipped_per_probe_ceiling > 0 || self.skipped_cumulative_ceiling > 0
}
}
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy)]
pub struct Telescoping2dOpts {
pub max_order: usize,
pub max_a_degree: usize,
pub max_cert_degree: usize,
}
impl Default for Telescoping2dOpts {
fn default() -> Self {
Telescoping2dOpts {
max_order: 2,
max_a_degree: 2,
max_cert_degree: 3,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TelescopingMdOpts {
pub max_order: usize,
pub max_a_degree: usize,
pub max_cert_degree: usize,
}
impl Default for TelescopingMdOpts {
fn default() -> Self {
TelescopingMdOpts {
max_order: 2,
max_a_degree: 2,
max_cert_degree: 2,
}
}
}
impl From<Telescoping2dOpts> for TelescopingMdOpts {
fn from(o: Telescoping2dOpts) -> Self {
TelescopingMdOpts {
max_order: o.max_order,
max_a_degree: o.max_a_degree,
max_cert_degree: o.max_cert_degree,
}
}
}
#[derive(Debug, Clone)]
pub struct Telescoping2dResult {
pub order: usize,
pub coeffs: Vec<ExprId>,
pub cert1: ExprId,
pub cert2: ExprId,
}
#[derive(Debug, Clone)]
pub struct TelescopingMdResult {
pub order: usize,
pub coeffs: Vec<ExprId>,
pub certs: Vec<ExprId>,
}
struct CandidateMd {
order: usize,
a: Vec<PolyM>, certs: Vec<RatM>, }
pub fn telescope2d_search(
term: ExprId,
n: ExprId,
j: ExprId,
k: ExprId,
pool: &ExprPool,
opts: &Telescoping2dOpts,
) -> Result<Telescoping2dResult, Telescoping2dError> {
let md_opts: TelescopingMdOpts = (*opts).into();
let r = telescope_md_search(term, n, &[j, k], pool, &md_opts)?;
debug_assert_eq!(r.certs.len(), 2);
Ok(Telescoping2dResult {
order: r.order,
coeffs: r.coeffs,
cert1: r.certs[0],
cert2: r.certs[1],
})
}
fn trip_to_error(trip: crate::budget::BudgetTrip) -> Telescoping2dError {
use crate::errors::AlkahestError;
crate::budget::record_trip(trip);
Telescoping2dError::SearchExhausted(format!(
"the multi-index telescoping search was stopped before it finished, and no certificate \
was found up to that point — this is NOT a statement that none exists: {} [{}]",
trip,
trip.code()
))
}
fn checkpoint() -> Result<(), Telescoping2dError> {
crate::budget::check_all().map_err(trip_to_error)
}
pub fn telescope_md_search(
term: ExprId,
n: ExprId,
indices: &[ExprId],
pool: &ExprPool,
opts: &TelescopingMdOpts,
) -> Result<TelescopingMdResult, Telescoping2dError> {
telescope_md_search_impl(
term,
n,
indices,
pool,
opts,
&Ceilings::PRODUCTION,
&mut SearchStats::default(),
)
}
#[cfg(test)]
pub(crate) fn telescope_md_search_instrumented(
term: ExprId,
n: ExprId,
indices: &[ExprId],
pool: &ExprPool,
opts: &TelescopingMdOpts,
ceilings: &Ceilings,
) -> (Result<TelescopingMdResult, Telescoping2dError>, SearchStats) {
let mut stats = SearchStats::default();
let result = telescope_md_search_impl(term, n, indices, pool, opts, ceilings, &mut stats);
(result, stats)
}
fn telescope_md_search_impl(
term: ExprId,
n: ExprId,
indices: &[ExprId],
pool: &ExprPool,
opts: &TelescopingMdOpts,
ceilings: &Ceilings,
stats: &mut SearchStats,
) -> Result<TelescopingMdResult, Telescoping2dError> {
let m = indices.len();
if m == 0 {
return Err(Telescoping2dError::InvalidInput(
"at least one bound index is required".into(),
));
}
if indices.contains(&n) || has_duplicate(indices) {
return Err(Telescoping2dError::InvalidInput(
"n and every bound index must be pairwise distinct symbols".into(),
));
}
if opts.max_order == 0 {
return Err(Telescoping2dError::InvalidInput(
"max_order must be at least 1".into(),
));
}
let num_axes = m + 1;
crate::budget::clear_trip();
let f = ProperTermM::parse(term, n, indices, pool)?;
let mut rhos: Vec<RatM> = Vec::with_capacity(m);
for t in 0..m {
let r = f.ratio_axis(t + 1, 1)?;
if r.den.is_zero() {
return Err(Telescoping2dError::NotProperHypergeometric(
"shift ratio has a zero denominator".into(),
));
}
rhos.push(r);
}
for order in 1..=opts.max_order {
let mut nn = Vec::with_capacity(order + 1);
let mut dn = Vec::with_capacity(order + 1);
for i in 0..=order as i64 {
let r = f.ratio_axis(AXIS_N, i)?;
if r.den.is_zero() {
return Err(Telescoping2dError::NotProperHypergeometric(
"shift ratio in n has a zero denominator".into(),
));
}
nn.push(r.num);
dn.push(r.den);
}
for a_degree in 0..=opts.max_a_degree {
for cert_degree in 0..=opts.max_cert_degree {
let box_len = cert_degree + 1;
let Some(cert_box_count) = box_len.checked_pow(num_axes as u32) else {
stats.skipped_per_probe_ceiling += 1;
continue;
};
let a_count = (order + 1) * (a_degree + 1);
let total = a_count.saturating_add(m.saturating_mul(cert_box_count));
if total > ceilings.max_ansatz_unknowns {
stats.skipped_per_probe_ceiling += 1;
continue;
}
if total >= ceilings.large_probe_threshold {
if stats.large_unknowns_spent.saturating_add(total)
> ceilings.max_cumulative_large_probe_unknowns
{
stats.skipped_cumulative_ceiling += 1;
continue;
}
stats.large_unknowns_spent += total;
stats.large_attempted += 1;
}
checkpoint()?;
stats.attempted += 1;
if let Some(cand) =
solve_ansatz_md(order, m, a_degree, cert_degree, &nn, &dn, &rhos, num_axes)?
{
if verify_certificate_md(&f, &cand) {
return Ok(finish_md(cand, n, indices, pool));
}
}
}
}
}
let budget_note = if stats.any_ceiling_fired() {
format!(
" (at least one (order, a_degree, cert_degree) combination within these bounds was \
skipped without being attempted, refused by this module's resource ceilings \
(MAX_ANSATZ_UNKNOWNS = {} unknowns for any single probe; \
MAX_CUMULATIVE_LARGE_PROBE_UNKNOWNS = {} total \
across every probe at or above {} unknowns in one search call) \
rather than attempted and left to run arbitrarily long — raising max_cert_degree or \
the bound-index count further will only make this worse, not better)",
ceilings.max_ansatz_unknowns,
ceilings.max_cumulative_large_probe_unknowns,
ceilings.large_probe_threshold,
)
} else {
String::new()
};
Err(Telescoping2dError::SearchExhausted(format!(
"no verified {}-index certificate of order <= {} was found for {} \
within a_degree <= {} and certificate degree <= {}{budget_note}",
m,
opts.max_order,
pool.display(term),
opts.max_a_degree,
opts.max_cert_degree
)))
}
fn has_duplicate(xs: &[ExprId]) -> bool {
for i in 0..xs.len() {
for j in (i + 1)..xs.len() {
if xs[i] == xs[j] {
return true;
}
}
}
false
}
fn flatten(exps: &[usize], box_len: usize) -> usize {
exps.iter().fold(0, |acc, &x| acc * box_len + x)
}
fn unflatten(mut combo: usize, num_axes: usize, box_len: usize) -> Vec<usize> {
let mut exps = vec![0usize; num_axes];
for idx in (0..num_axes).rev() {
exps[idx] = combo % box_len;
combo /= box_len;
}
exps
}
fn monomial_m(exps: &[usize], c: Rational, num_axes: usize) -> PolyM {
let mut p = PolyM::constant(c, num_axes);
for (axis, &e) in exps.iter().enumerate() {
if e > 0 {
p = p.mul(&PolyM::var(axis, num_axes).pow_u32(e as u32));
}
}
p
}
#[allow(clippy::too_many_arguments)]
fn solve_ansatz_md(
order: usize,
m: usize,
a_degree: usize,
cert_degree: usize,
nn: &[PolyM],
dn: &[PolyM],
rhos: &[RatM],
num_axes: usize,
) -> Result<Option<CandidateMd>, Telescoping2dError> {
let ds: Vec<&PolyM> = rhos.iter().map(|r| &r.den).collect();
let ns_: Vec<&PolyM> = rhos.iter().map(|r| &r.num).collect();
let mut dnfull = PolyM::one(num_axes);
for d in dn {
dnfull = dnfull.mul(d);
}
let mut dnfull_excl: Vec<PolyM> = Vec::with_capacity(order + 1);
for i in 0..=order {
let mut p = PolyM::one(num_axes);
for (idx, d) in dn.iter().enumerate() {
if idx != i {
p = p.mul(d);
}
}
dnfull_excl.push(p);
}
let mut e: Vec<PolyM> = Vec::with_capacity(m);
let mut es: Vec<PolyM> = Vec::with_capacity(m);
for (t, d) in ds.iter().enumerate().take(m) {
let et = d.mul(&dnfull);
let ets = et.shift(t + 1, 1);
e.push(et);
es.push(ets);
}
let block: Vec<PolyM> = (0..m).map(|t| e[t].mul(&es[t]).mul(ds[t])).collect();
let mut mn: Vec<PolyM> = Vec::with_capacity(order + 1);
for excl in &dnfull_excl {
let mut p = ds[0].mul(excl).mul(&es[0]).mul(ds[0]);
for blk in block.iter().skip(1) {
p = p.mul(blk);
}
mn.push(p);
}
let mut mt: Vec<PolyM> = Vec::with_capacity(m);
for t in 0..m {
let mut p = PolyM::one(num_axes);
for (tp, blk) in block.iter().enumerate() {
if tp != t {
p = p.mul(blk);
}
}
mt.push(p);
}
let na = a_degree + 1;
let box_len = cert_degree + 1;
let cert_box_count = box_len.pow(num_axes as u32);
let a_count = (order + 1) * na;
let total = a_count + m * cert_box_count;
let idx_a = |i: usize, p: usize| i * na + p;
let idx_cert = |t: usize, exps: &[usize]| a_count + t * cert_box_count + flatten(exps, box_len);
let mut rows: BTreeMap<Vec<u32>, Vec<Rational>> = BTreeMap::new();
let mut add_contribution = |mono: &BTreeMap<Vec<u32>, Rational>, col: usize| {
for (exp, c) in mono {
let row = rows
.entry(exp.clone())
.or_insert_with(|| vec![Rational::from(0); total]);
row[col] += c.clone();
}
};
for i in 0..=order {
for p in 0..=a_degree {
let basis = PolyM::var(AXIS_N, num_axes)
.pow_u32(p as u32)
.mul(&nn[i])
.mul(&mn[i]);
add_contribution(&basis.terms, idx_a(i, p));
}
}
let mut plain_pow: Vec<Vec<PolyM>> = Vec::with_capacity(m);
let mut shift_pow: Vec<Vec<PolyM>> = Vec::with_capacity(m);
for t in 0..m {
let axis = t + 1;
let xv = PolyM::var(axis, num_axes);
let xv1 = xv.add(&PolyM::one(num_axes));
let mut pp = Vec::with_capacity(box_len);
let mut sp = Vec::with_capacity(box_len);
let mut accp = PolyM::one(num_axes);
let mut accs = PolyM::one(num_axes);
for _ in 0..box_len {
pp.push(accp.clone());
sp.push(accs.clone());
accp = accp.mul(&xv);
accs = accs.mul(&xv1);
}
plain_pow.push(pp);
shift_pow.push(sp);
}
let n_pow: Vec<PolyM> = {
let nv = PolyM::var(AXIS_N, num_axes);
let mut acc = PolyM::one(num_axes);
let mut v = Vec::with_capacity(box_len);
for _ in 0..box_len {
v.push(acc.clone());
acc = acc.mul(&nv);
}
v
};
let total_combos = box_len.pow(num_axes as u32);
for combo in 0..total_combos {
checkpoint()?;
let exps = unflatten(combo, num_axes, box_len);
let p = exps[0];
let np = n_pow[p].clone();
for t in 0..m {
let mut mono_shift = np.clone();
let mut mono_plain = np.clone();
for tp in 0..m {
let q = exps[tp + 1];
mono_plain = mono_plain.mul(&plain_pow[tp][q]);
mono_shift = mono_shift.mul(if tp == t {
&shift_pow[tp][q]
} else {
&plain_pow[tp][q]
});
}
let basis = mono_shift
.mul(ns_[t])
.mul(&e[t])
.sub(&mono_plain.mul(&es[t]).mul(ds[t]))
.mul(&mt[t])
.neg();
add_contribution(&basis.terms, idx_cert(t, &exps));
}
}
let matrix: Vec<Vec<Rational>> = rows.into_values().collect();
let basis_vecs = rational_nullspace(matrix, total)?;
if basis_vecs.is_empty() {
return Ok(None);
}
for vec in &basis_vecs {
let top_nonzero = (0..na).any(|p| vec[idx_a(order, p)] != 0);
if !top_nonzero {
continue;
}
let mut scaled = vec.clone();
primitive_scale_rationals(&mut scaled);
let mut a = Vec::with_capacity(order + 1);
for i in 0..=order {
let mut poly = PolyM::zero();
for pw in 0..=a_degree {
let c = scaled[idx_a(i, pw)].clone();
if c != 0 {
poly = poly.add(&PolyM::var(AXIS_N, num_axes).pow_u32(pw as u32).scale(&c));
}
}
a.push(poly);
}
let mut certs_num: Vec<PolyM> = vec![PolyM::zero(); m];
for combo in 0..total_combos {
let exps = unflatten(combo, num_axes, box_len);
for (t, cn) in certs_num.iter_mut().enumerate() {
let c = scaled[idx_cert(t, &exps)].clone();
if c != 0 {
*cn = cn.add(&monomial_m(&exps, c, num_axes));
}
}
}
let certs: Vec<RatM> = (0..m)
.map(|t| RatM {
num: certs_num[t].clone(),
den: e[t].clone(),
})
.collect();
return Ok(Some(CandidateMd { order, a, certs }));
}
Ok(None)
}
fn primitive_scale_rationals(v: &mut [Rational]) {
let mut den_lcm = Integer::from(1);
for c in v.iter() {
den_lcm = den_lcm.lcm(c.denom());
}
for c in v.iter_mut() {
*c *= den_lcm.clone();
}
let mut content = Integer::from(0);
for c in v.iter() {
content = content.gcd(c.numer());
}
if content != 0 && content != 1 {
let inv = Rational::from((1, content.clone()));
for c in v.iter_mut() {
*c *= inv.clone();
}
}
let sign_ref = v.iter().rev().find(|c| **c != 0).cloned();
if let Some(s) = sign_ref {
if s < 0 {
for c in v.iter_mut() {
*c *= Rational::from(-1);
}
}
}
}
#[allow(clippy::needless_range_loop)]
fn rational_nullspace(
mut mat: Vec<Vec<Rational>>,
ncols: usize,
) -> Result<Vec<Vec<Rational>>, Telescoping2dError> {
let nrows = mat.len();
let mut pivot_cols: Vec<usize> = Vec::new();
let mut row = 0;
for col in 0..ncols {
if row >= nrows {
break;
}
checkpoint()?;
let Some(pr) = (row..nrows).find(|&r| mat[r][col] != 0) else {
continue;
};
mat.swap(row, pr);
let inv = Rational::from(1) / mat[row][col].clone();
for c in col..ncols {
mat[row][c] *= inv.clone();
}
for r in 0..nrows {
if r == row {
continue;
}
let factor = mat[r][col].clone();
if factor == 0 {
continue;
}
checkpoint()?;
for c in col..ncols {
let sub = factor.clone() * mat[row][c].clone();
mat[r][c] -= sub;
}
}
pivot_cols.push(col);
row += 1;
}
let is_pivot: Vec<bool> = {
let mut v = vec![false; ncols];
for &c in &pivot_cols {
v[c] = true;
}
v
};
let mut basis = Vec::new();
for free_col in 0..ncols {
if is_pivot[free_col] {
continue;
}
let mut v = vec![Rational::from(0); ncols];
v[free_col] = Rational::from(1);
for (r, &pc) in pivot_cols.iter().enumerate() {
v[pc] = -mat[r][free_col].clone();
}
basis.push(v);
}
Ok(basis)
}
fn verify_certificate_md(f: &ProperTermM, cand: &CandidateMd) -> bool {
let num_axes = f.m + 1;
let mut lhs = RatM::from_rational(Rational::from(0), num_axes);
for (i, ai) in cand.a.iter().enumerate() {
let Ok(ratio) = f.ratio_axis(AXIS_N, i as i64) else {
return false;
};
lhs = lhs.add(&RatM::from_poly(ai.clone(), num_axes).mul(&ratio));
}
let mut rhs = RatM::from_rational(Rational::from(0), num_axes);
for (t, cert_t) in cand.certs.iter().enumerate() {
let axis: Axis = t + 1;
let Ok(rho_t) = f.ratio_axis(axis, 1) else {
return false;
};
let g_delta = cert_t.shift(axis, 1).mul(&rho_t).sub(cert_t);
rhs = rhs.add(&g_delta);
}
lhs.eq_rat(&rhs)
}
fn finish_md(
cand: CandidateMd,
n: ExprId,
indices: &[ExprId],
pool: &ExprPool,
) -> TelescopingMdResult {
let coeffs = cand.a.iter().map(|p| p.to_expr(pool, n, indices)).collect();
let certs = cand
.certs
.iter()
.map(|c| c.to_expr(pool, n, indices))
.collect();
TelescopingMdResult {
order: cand.order,
coeffs,
certs,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
fn njk(pool: &ExprPool) -> (ExprId, ExprId, ExprId) {
(
pool.symbol("n", Domain::Real),
pool.symbol("j", Domain::Real),
pool.symbol("k", Domain::Real),
)
}
fn binom(pool: &ExprPool, top: ExprId, bot: ExprId) -> ExprId {
pool.func("binomial", vec![top, bot])
}
#[test]
fn finds_a_certificate_for_double_binomial_product() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let f = pool.mul(vec![binom(&pool, n, j), binom(&pool, j, k)]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts)
.expect("a double-sum certificate should be found for C(n,j)*C(j,k)");
assert!(result.order >= 1);
assert_eq!(result.coeffs.len(), result.order + 1);
}
#[test]
fn refuses_coincident_indices() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let opts = Telescoping2dOpts::default();
let err =
telescope2d_search(n, n, n, n, &pool, &opts).expect_err("n, j, k must be distinct");
assert!(matches!(err, Telescoping2dError::InvalidInput(_)));
}
#[test]
fn md_refuses_zero_indices() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let opts = TelescopingMdOpts::default();
let err = telescope_md_search(n, n, &[], &pool, &opts)
.expect_err("at least one bound index required");
assert!(matches!(err, Telescoping2dError::InvalidInput(_)));
}
#[test]
fn md_single_index_matches_classical_binomial_sum() {
let pool = ExprPool::new();
let n = pool.symbol("n", Domain::Real);
let k = pool.symbol("k", Domain::Real);
let f = binom(&pool, n, k);
let opts = TelescopingMdOpts::default();
let result = telescope_md_search(f, n, &[k], &pool, &opts)
.expect("a certificate should be found for C(n,k)");
assert_eq!(result.certs.len(), 1);
assert!(result.order >= 1);
}
}
#[cfg(kani)]
mod verification {
use super::{flatten, unflatten};
#[kani::proof]
#[kani::unwind(4)]
fn flatten_in_block_small() {
let num_axes: usize = kani::any_where(|n: &usize| *n <= 3);
let box_len: usize = kani::any_where(|b: &usize| *b >= 1 && *b < 1 << 8);
let Some(count) = box_len.checked_pow(num_axes as u32) else {
return;
};
let exps: [usize; 3] = [
kani::any_where(|e: &usize| *e < box_len),
kani::any_where(|e: &usize| *e < box_len),
kani::any_where(|e: &usize| *e < box_len),
];
assert!(flatten(&exps[..num_axes], box_len) < count);
}
#[kani::proof]
#[kani::unwind(4)]
fn flatten_roundtrip_small() {
let num_axes: usize = kani::any_where(|n: &usize| *n <= 3);
let box_len: usize = kani::any_where(|b: &usize| *b >= 1 && *b < 16);
let exps: [usize; 3] = [
kani::any_where(|e: &usize| *e < box_len),
kani::any_where(|e: &usize| *e < box_len),
kani::any_where(|e: &usize| *e < box_len),
];
let back = unflatten(flatten(&exps[..num_axes], box_len), num_axes, box_len);
assert_eq!(back.len(), num_axes);
for i in 0..num_axes {
assert_eq!(back[i], exps[i]);
}
}
}