use super::hyperterm::{affine_parts, as_ratk, rn_to_expr, GammaFactor, ProperTerm};
use super::qfield::{
rn_add, rn_div, rn_eval, rn_int, rn_is_zero, rn_mul, rn_neg, rn_one, rn_rat, rn_var, rn_zero,
PolyK, RatK, Rn,
};
use super::zeilberger::ZeilbergerResult;
use crate::kernel::{ExprId, ExprPool};
use rug::{Integer, Rational};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoundaryStatus {
Vanishes,
Nonzero {
rhs: ExprId,
witness_n: i64,
},
Unknown {
reason: String,
},
}
impl BoundaryStatus {
pub fn tag(&self) -> &'static str {
match self {
BoundaryStatus::Vanishes => "vanishes",
BoundaryStatus::Nonzero { .. } => "nonzero",
BoundaryStatus::Unknown { .. } => "unknown",
}
}
pub fn implies_sum_recurrence(&self) -> bool {
!matches!(self, BoundaryStatus::Unknown { .. })
}
pub fn side_conditions(&self, range: &str) -> Vec<String> {
match self {
BoundaryStatus::Vanishes => vec![format!(
"the boundary difference for {range} was proved to vanish in exact arithmetic, \
so the homogeneous recurrence sum_i a_i(n)*S(n+i) = 0 holds for the sum; it is \
a statement about the integers n at which the summand, the certificate and the \
sum are all defined"
)],
BoundaryStatus::Nonzero { witness_n, .. } => vec![format!(
"the boundary difference for {range} does not vanish, so the recurrence for the \
sum is the inhomogeneous sum_i a_i(n)*S(n+i) = b(n) with b(n) returned \
explicitly (b({witness_n}) != 0 in exact arithmetic); the HOMOGENEOUS \
recurrence is FALSE for this sum"
)],
BoundaryStatus::Unknown { reason } => vec![format!(
"the boundary difference for {range} could not be decided ({reason}); the \
certificate proves the telescoping identity in k and NOTHING follows about \
sum_k F(n,k) until this is discharged independently"
)],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BoundaryVerdict {
pub status: BoundaryStatus,
pub valid_from: Option<i64>,
pub certificate_poles: Vec<ExprId>,
}
impl BoundaryVerdict {
pub fn tag(&self) -> &'static str {
self.status.tag()
}
pub fn implies_sum_recurrence(&self) -> bool {
self.status.implies_sum_recurrence()
}
pub fn side_conditions(&self, range: &str, pool: &ExprPool) -> Vec<String> {
let mut out = self.status.side_conditions(range);
if let Some(n_min) = self.valid_from {
out.push(format!(
"this verdict is claimed for integers n >= {n_min} only: the range {range} runs \
backwards below that, where a sum over it is 0 under one reading and a signed \
sum under the other, and the relation above is FALSE"
));
}
if !self.certificate_poles.is_empty() {
let list: Vec<String> = self
.certificate_poles
.iter()
.map(|&p| pool.display(p).to_string())
.collect();
out.push(format!(
"the telescoped G(n,k) = R(n,k)*F(n,k) was not shown to be finite at the interior \
point(s) k = {} of {range}, so the telescoping breaks inside the range and not \
at an endpoint",
list.join(", ")
));
}
out
}
fn unknown(reason: String) -> Self {
BoundaryVerdict {
status: BoundaryStatus::Unknown { reason },
valid_from: None,
certificate_poles: Vec::new(),
}
}
}
pub fn natural_limits(n: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
(pool.integer(0_i32), n)
}
pub fn boundary_status(
result: &ZeilbergerResult,
term: ExprId,
n: ExprId,
k: ExprId,
limits: Option<(ExprId, ExprId)>,
pool: &ExprPool,
) -> BoundaryStatus {
let verdict = boundary_verdict(result, term, n, k, limits, pool);
match verdict.valid_from {
Some(n_min) if n_min > 0 && verdict.status.implies_sum_recurrence() => {
BoundaryStatus::Unknown {
reason: format!(
"the declared summation range runs backwards for n < {n_min}, where the \
relation is false; the verdict holds from n = {n_min} on and \
`boundary_verdict` returns it with that domain attached"
),
}
}
_ => verdict.status,
}
}
pub fn boundary_verdict(
result: &ZeilbergerResult,
term: ExprId,
n: ExprId,
k: ExprId,
limits: Option<(ExprId, ExprId)>,
pool: &ExprPool,
) -> BoundaryVerdict {
let setup = match Setup::new(result, term, n, k, limits, pool) {
Err(reason) => return BoundaryVerdict::unknown(reason),
Ok(s) => s,
};
let valid_from = match range_domain(setup.lo, setup.hi) {
Err(reason) => return BoundaryVerdict::unknown(reason),
Ok(v) => v,
};
let poles = interior_poles(&setup);
if !poles.is_empty() {
let list: Vec<String> = poles.iter().map(|p| p.to_string()).collect();
return BoundaryVerdict {
status: BoundaryStatus::Unknown {
reason: format!(
"the telescoped G(n,k) = R(n,k)*F(n,k) was not shown to be finite at the \
interior point(s) k = {}, which are integers inside the declared range for \
infinitely many n; the telescoping breaks there and not at an endpoint",
list.join(", ")
),
},
valid_from,
certificate_poles: poles.iter().map(|p| p.to_expr(pool, n)).collect(),
};
}
if let Some(pt) = summand_pole_inside(&setup.f, setup.lo, setup.hi) {
return BoundaryVerdict {
status: BoundaryStatus::Unknown {
reason: format!(
"the summand is undefined at k = {pt}, which lies inside the summation \
range k = {}..{}: that term of S(n) is a division by zero, so the sum \
has no value and no recurrence for it follows from the certificate",
setup.lo, setup.hi
),
},
valid_from,
certificate_poles: vec![PolePoint {
s: Rational::from(pt.alpha),
t: Rational::from(pt.beta),
}
.to_expr(pool, n)],
};
}
let status = match collect_terms(&setup, result, n, k, pool) {
Err(reason) => BoundaryStatus::Unknown { reason },
Ok(terms) => decide(&terms, n, valid_from.unwrap_or(1).max(1), pool),
};
BoundaryVerdict {
status,
valid_from,
certificate_poles: Vec::new(),
}
}
struct Setup {
f: ProperTerm,
r: RatK,
lo: Point,
hi: Point,
}
impl Setup {
fn new(
result: &ZeilbergerResult,
term: ExprId,
n: ExprId,
k: ExprId,
limits: Option<(ExprId, ExprId)>,
pool: &ExprPool,
) -> Result<Setup, String> {
let Some((lo, hi)) = limits else {
return Err(
"the summation limits were not supplied, so there is no range over which \
to evaluate the boundary; pass (k_lo, k_hi) — the usual choice is \
k = 0..n"
.into(),
);
};
let f = ProperTerm::parse(term, n, k, pool).map_err(|_| {
"the summand did not re-parse as a proper hypergeometric term".to_string()
})?;
let r = as_ratk(result.certificate, n, k, pool, 0)
.ok_or("the certificate did not re-parse as an element of Q(n)(k)")?;
let lo = endpoint_point(lo, n, k, pool).map_err(|e| format!("lower limit k_lo: {e}"))?;
let hi = endpoint_point(hi, n, k, pool).map_err(|e| format!("upper limit k_hi: {e}"))?;
let order = result.order;
let extras = (lo.alpha.unsigned_abs() + hi.alpha.unsigned_abs()) * order as u64;
if extras > MAX_CORRECTION_TERMS {
return Err(format!(
"the summation limits move with n fast enough to need {extras} correction terms, \
past the supported limit of {MAX_CORRECTION_TERMS}"
));
}
Ok(Setup { f, r, lo, hi })
}
}
fn collect_terms(
setup: &Setup,
result: &ZeilbergerResult,
n: ExprId,
k: ExprId,
pool: &ExprPool,
) -> Result<Vec<HypTerm>, String> {
let (f, r, lo_pt, hi_pt) = (&setup.f, &setup.r, setup.lo, setup.hi);
let order = result.order;
let mut terms: Vec<HypTerm> = Vec::new();
let at_hi = hi_pt.offset(1);
push_value(&mut terms, value_at(r, f, 0, at_hi), &rn_one())
.map_err(|e| format!("G(n, k_hi+1) could not be evaluated: {e}"))?;
push_value(&mut terms, value_at(r, f, 0, lo_pt), &rn_neg(&rn_one()))
.map_err(|e| format!("G(n, k_lo) could not be evaluated: {e}"))?;
let one = RatK::one();
for i in 0..=order {
let a_i = coeff_as_rn(result.coeffs[i], n, k, pool)
.ok_or_else(|| format!("recurrence coefficient a_{i}(n) is not an element of Q(n)"))?;
if rn_is_zero(&a_i) {
continue;
}
let i64_i = i as i64;
for (t, sign) in signed_window(1, hi_pt.alpha * i64_i) {
let weight = scale_sign(&a_i, sign);
let at = hi_pt.offset(t);
push_value(&mut terms, value_at(&one, f, i64_i, at), &weight)
.map_err(|e| format!("the upper range-shift correction failed: {e}"))?;
}
for (t, sign) in signed_window(lo_pt.alpha * i64_i, -1) {
let weight = scale_sign(&a_i, sign);
let at = lo_pt.offset(t);
push_value(&mut terms, value_at(&one, f, i64_i, at), &weight)
.map_err(|e| format!("the lower range-shift correction failed: {e}"))?;
}
}
Ok(terms)
}
fn decide(terms: &[HypTerm], n: ExprId, witness_from: i64, pool: &ExprPool) -> BoundaryStatus {
let mut classes: Vec<(Rational, Vec<GammaN>, Rn)> = Vec::new();
for t in terms {
let Some((coeff, base, sig)) = t.canonical() else {
return BoundaryStatus::Unknown {
reason: "a boundary term could not be put in canonical hypergeometric form".into(),
};
};
match classes.iter_mut().find(|(b, g, _)| *b == base && *g == sig) {
Some((_, _, acc)) => *acc = rn_add(acc, &coeff),
None => classes.push((base, sig, coeff)),
}
}
if classes.iter().all(|(_, _, c)| rn_is_zero(c)) {
return BoundaryStatus::Vanishes;
}
match nonzero_witness(terms, witness_from) {
Some(witness_n) => BoundaryStatus::Nonzero {
rhs: classes_expr(&classes, n, pool),
witness_n,
},
None => BoundaryStatus::Unknown {
reason: "the boundary difference is an explicit closed form that neither cancelled \
structurally nor evaluated to anything nonzero at a sampled integer n; it \
may well vanish, but that was not proved"
.into(),
},
}
}
const MAX_CORRECTION_TERMS: u64 = 256;
#[derive(Debug, Clone, Copy)]
struct Point {
alpha: i64,
beta: i64,
}
impl Point {
fn offset(self, t: i64) -> Point {
Point {
alpha: self.alpha,
beta: self.beta + t,
}
}
}
fn endpoint_point(e: ExprId, n: ExprId, k: ExprId, pool: &ExprPool) -> Result<Point, String> {
let Some((a, b, c)) = affine_parts(e, n, k, pool) else {
return Err(format!(
"`{}` is not an integer-affine function of n",
pool.display(e)
));
};
if b != 0 {
return Err(format!(
"`{}` depends on the summation index k",
pool.display(e)
));
}
if *c.clone().denom() != 1 {
return Err(format!(
"`{}` has a non-integer constant part",
pool.display(e)
));
}
let beta = c
.numer()
.to_i64()
.ok_or_else(|| "the constant part is too large".to_string())?;
Ok(Point { alpha: a, beta })
}
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.alpha, self.beta) {
(0, b) => write!(f, "{b}"),
(1, 0) => write!(f, "n"),
(a, 0) => write!(f, "{a}*n"),
(1, b) if b > 0 => write!(f, "n + {b}"),
(1, b) => write!(f, "n - {}", -b),
(a, b) if b > 0 => write!(f, "{a}*n + {b}"),
(a, b) => write!(f, "{a}*n - {}", -b),
}
}
}
fn ge_eventually(a: Point, b: Point) -> bool {
a.alpha > b.alpha || (a.alpha == b.alpha && a.beta >= b.beta)
}
const MAX_INTERIOR_POLE_OFFSET: i64 = 64;
fn summand_pole_inside(f: &ProperTerm, lo: Point, hi: Point) -> Option<Point> {
let (first, last) = if ge_eventually(hi, lo) {
(lo, hi)
} else {
(hi.offset(1), lo.offset(-1))
};
if first.alpha > last.alpha {
return None;
}
let den_can_vanish = f.rat.den.degree() >= 1;
let gamma_can_pole = |alpha: i64| {
f.gammas
.iter()
.any(|g| g.e > 0 && g.b != 0 && g.a.checked_add(g.b.wrapping_mul(alpha)) == Some(0))
};
let w = MAX_INTERIOR_POLE_OFFSET;
let one = RatK::one();
for alpha in first.alpha..=last.alpha {
if !den_can_vanish && !gamma_can_pole(alpha) {
continue;
}
let (blo, bhi) = match (alpha == first.alpha, alpha == last.alpha) {
(true, true) => (first.beta, last.beta.min(first.beta.saturating_add(w))),
(true, false) => (first.beta, first.beta.saturating_add(w)),
(false, true) => (last.beta.saturating_sub(w), last.beta),
(false, false) => (-w, w),
};
for beta in blo..=bhi {
let pt = Point { alpha, beta };
if let Value::Pole { .. } = value_at(&one, f, 0, pt) {
return Some(pt);
}
}
}
None
}
fn range_domain(lo: Point, hi: Point) -> Result<Option<i64>, String> {
let (Some(d), Some(e)) = (hi.alpha.checked_sub(lo.alpha), hi.beta.checked_sub(lo.beta)) else {
return Err("the summation limits are too large to compare".into());
};
let backwards = "the declared summation range runs backwards there (k_hi < k_lo - 1), where a \
sum over it is 0 under the empty-sum reading and a signed sum under the \
reversed-sum reading, so no recurrence for it is claimed";
match d.cmp(&0) {
std::cmp::Ordering::Equal => {
if e >= -1 {
Ok(None)
} else {
Err(format!("the range is empty at every n: {backwards}"))
}
}
std::cmp::Ordering::Greater => {
let num = (-1_i64)
.checked_sub(e)
.ok_or("the summation limits are too large to compare")?;
Ok(Some(ceil_div(num, d)))
}
std::cmp::Ordering::Less => Err(format!(
"the range is empty for every sufficiently large n: {backwards}"
)),
}
}
fn ceil_div(a: i64, b: i64) -> i64 {
let q = a.div_euclid(b);
if a.rem_euclid(b) == 0 {
q
} else {
q + 1
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct PolePoint {
s: Rational,
t: Rational,
}
impl PolePoint {
fn as_rn(&self) -> Rn {
rn_add(
&rn_mul(&rn_rat(self.s.clone()), &rn_var()),
&rn_rat(self.t.clone()),
)
}
fn as_integer_point(&self) -> Option<Point> {
if *self.s.clone().denom() != 1 || *self.t.clone().denom() != 1 {
return None;
}
Some(Point {
alpha: self.s.numer().to_i64()?,
beta: self.t.numer().to_i64()?,
})
}
fn hits_integers(&self) -> bool {
let c = Integer::from(self.s.denom().lcm_ref(self.t.denom()));
let p = self.s.numer().clone() * (c.clone() / self.s.denom().clone());
let q = self.t.numer().clone() * (c.clone() / self.t.denom().clone());
(q % Integer::from(p.gcd_ref(&c))) == 0
}
fn inside(&self, lo: Point, hi: Point) -> bool {
let (a_lo, b_lo) = (Rational::from(lo.alpha), Rational::from(lo.beta));
let (a_hi, b_hi) = (Rational::from(hi.alpha), Rational::from(hi.beta));
(self.s > a_lo || (self.s == a_lo && self.t >= b_lo))
&& (self.s < a_hi || (self.s == a_hi && self.t <= b_hi))
}
fn to_expr(&self, pool: &ExprPool, n: ExprId) -> ExprId {
rn_to_expr(pool, n, &self.as_rn())
}
}
impl std::fmt::Display for PolePoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.s == 0 {
write!(f, "{}", self.t)
} else if self.t == 0 {
write!(f, "{}*n", self.s)
} else {
write!(f, "{}*n + {}", self.s, self.t)
}
}
}
const POLE_SAMPLE_N: i64 = 12;
const POLE_MAX_DEN: i64 = 4;
const POLE_SLACK: i64 = 4;
const POLE_MAX_GRID: i64 = 4096;
fn interior_poles(setup: &Setup) -> Vec<PolePoint> {
let g = setup.r.mul(&setup.f.rat).normalize();
let mut out: Vec<PolePoint> = Vec::new();
for den in [&setup.f.rat.den, &g.den] {
for cand in affine_roots(den, setup.lo, setup.hi) {
if out.contains(&cand) || !cand.hits_integers() || !cand.inside(setup.lo, setup.hi) {
continue;
}
let regular = match cand.as_integer_point() {
Some(at) => {
is_finite(value_at(&setup.r, &setup.f, 0, at))
&& is_finite(value_at(&RatK::one(), &setup.f, 0, at))
}
None => false,
};
if !regular {
out.push(cand);
}
}
}
out
}
fn is_finite(v: Value) -> bool {
matches!(v, Value::Zero | Value::Finite(_))
}
fn affine_roots(den: &PolyK, lo: Point, hi: Point) -> Vec<PolePoint> {
if den.degree() < 1 || lo.alpha > hi.alpha {
return Vec::new();
}
let (Some(at0), Some(at1)) = (
specialize(den, POLE_SAMPLE_N),
specialize(den, POLE_SAMPLE_N + 1),
) else {
return Vec::new();
};
let w_lo = lo.alpha * POLE_SAMPLE_N + lo.beta - POLE_SLACK;
let w_hi = hi.alpha * POLE_SAMPLE_N + hi.beta + POLE_SLACK;
if w_hi < w_lo || (w_hi - w_lo).saturating_mul(POLE_MAX_DEN) > POLE_MAX_GRID {
return Vec::new();
}
let mut out: Vec<PolePoint> = Vec::new();
for c in 1..=POLE_MAX_DEN {
for a in (w_lo * c)..=(w_hi * c) {
let v0 = Rational::from((a, c));
if poly_at(&at0, &v0) != 0 {
continue;
}
for sc in 1..=POLE_MAX_DEN {
for sn in (lo.alpha * sc)..=(hi.alpha * sc) {
let s = Rational::from((sn, sc));
if poly_at(&at1, &(v0.clone() + s.clone())) != 0 {
continue;
}
let cand = PolePoint {
t: v0.clone() - s.clone() * Rational::from(POLE_SAMPLE_N),
s,
};
if !out.contains(&cand) && rn_is_zero(&poly_eval(den, &cand.as_rn())) {
out.push(cand);
}
}
}
}
}
out
}
fn specialize(p: &PolyK, m: i64) -> Option<Vec<Rational>> {
let d = p.degree();
if d < 0 {
return None;
}
let m = Rational::from(m);
let coeffs: Vec<Rational> = (0..=d as usize)
.map(|i| rn_eval(&p.coeff(i), &m))
.collect::<Option<Vec<_>>>()?;
if coeffs.iter().all(|c| *c == 0) {
return None;
}
Some(coeffs)
}
fn poly_at(coeffs: &[Rational], x: &Rational) -> Rational {
let mut acc = Rational::from(0);
for c in coeffs.iter().rev() {
acc = acc * x.clone() + c.clone();
}
acc
}
fn signed_window(from: i64, to: i64) -> Vec<(i64, i32)> {
if from <= to {
(from..=to).map(|t| (t, 1)).collect()
} else {
((to + 1)..=(from - 1)).map(|t| (t, -1)).collect()
}
}
fn scale_sign(a: &Rn, sign: i32) -> Rn {
if sign < 0 {
rn_neg(a)
} else {
a.clone()
}
}
fn coeff_as_rn(e: ExprId, n: ExprId, k: ExprId, pool: &ExprPool) -> Option<Rn> {
let r = as_ratk(e, n, k, pool, 0)?;
if r.num.degree() > 0 || r.den.degree() > 0 {
return None; }
rn_div(&r.num.coeff(0), &r.den.coeff(0))
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct GammaN {
a: i64,
c: Rational,
e: i32,
}
#[derive(Clone, Debug)]
struct HypTerm {
coeff: Rn,
base: Rational,
gammas: Vec<GammaN>,
}
enum Value {
Zero,
Finite(HypTerm),
Pole { order: u64 },
Undecidable(String),
}
fn push_value(out: &mut Vec<HypTerm>, v: Value, weight: &Rn) -> Result<(), String> {
match v {
Value::Zero => Ok(()),
Value::Pole { order } => Err(format!(
"the value is unbounded there (pole of order {order}), so the telescoped sum has \
no finite boundary value"
)),
Value::Undecidable(why) => Err(why),
Value::Finite(mut t) => {
t.coeff = rn_mul(&t.coeff, weight);
if !rn_is_zero(&t.coeff) {
out.push(t);
}
Ok(())
}
}
}
const MAX_GAMMA_EXPONENT: i32 = 64;
const MAX_GAMMA_ARG: i64 = 4096;
const MAX_GAMMA_SHIFT: i64 = 512;
fn value_at(extra: &RatK, f: &ProperTerm, n_shift: i64, at: Point) -> Value {
let q = extra.mul(&f.rat.shift_n(n_shift)).normalize();
if q.num.is_zero() {
return Value::Zero;
}
let kstar = rn_add(&rn_mul(&rn_int(at.alpha), &rn_var()), &rn_int(at.beta));
let (m_num, num_r) = deflate_root(&q.num, &kstar);
let (m_den, den_r) = deflate_root(&q.den, &kstar);
let mut order: i64 = m_num as i64 - m_den as i64;
let num_val = poly_eval(&num_r, &kstar);
let den_val = poly_eval(&den_r, &kstar);
if rn_is_zero(&den_val) {
return Value::Undecidable(
"the rational part's denominator vanishes identically at the endpoint".into(),
);
}
let Some(mut coeff) = rn_div(&num_val, &den_val) else {
return Value::Undecidable("the rational part is not invertible at the endpoint".into());
};
let (Some(z_alpha), Some(z_beta), Some(w_shift)) = (
rat_pow(&f.z, at.alpha),
rat_pow(&f.z, at.beta),
rat_pow(&f.w, n_shift),
) else {
return Value::Undecidable("an exponential factor has an unmanageable exponent".into());
};
let base = z_alpha * f.w.clone();
coeff = rn_mul(&coeff, &rn_rat(z_beta * w_shift));
let mut gammas: Vec<GammaN> = Vec::with_capacity(f.gammas.len());
for g in &f.gammas {
if g.e.unsigned_abs() > MAX_GAMMA_EXPONENT as u32 {
return Value::Undecidable("a gamma factor has an unmanageable exponent".into());
}
match gamma_at_point(g, n_shift, at) {
GammaAt::Finite { a, c } => gammas.push(GammaN { a, c, e: g.e }),
GammaAt::Pole { residue_scale } => {
let Some(lead) = rat_pow(&residue_scale, g.e as i64) else {
return Value::Undecidable(
"a gamma residue has an unmanageable exponent".into(),
);
};
coeff = rn_mul(&coeff, &rn_rat(lead));
order -= g.e as i64;
}
GammaAt::Degenerate(why) => return Value::Undecidable(why),
}
}
if order > 0 {
return Value::Zero;
}
if order < 0 {
return Value::Pole {
order: order.unsigned_abs(),
};
}
if rn_is_zero(&coeff) {
return Value::Zero;
}
Value::Finite(HypTerm {
coeff,
base,
gammas,
})
}
enum GammaAt {
Finite { a: i64, c: Rational },
Pole { residue_scale: Rational },
Degenerate(String),
}
fn gamma_at_point(g: &GammaFactor, n_shift: i64, at: Point) -> GammaAt {
let Some(a_eff) = g.b.checked_mul(at.alpha).and_then(|v| v.checked_add(g.a)) else {
return GammaAt::Degenerate("a gamma argument overflowed".into());
};
let Some(shift) =
g.b.checked_mul(at.beta)
.and_then(|v| g.a.checked_mul(n_shift).and_then(|w| v.checked_add(w)))
else {
return GammaAt::Degenerate("a gamma argument overflowed".into());
};
let c_eff = Rational::from(shift) + g.c.clone();
let at_gamma_pole = a_eff == 0 && *c_eff.clone().denom() == 1 && c_eff <= 0;
if !at_gamma_pole {
return GammaAt::Finite { a: a_eff, c: c_eff };
}
if g.b == 0 {
return GammaAt::Degenerate(
"the summand has a gamma factor at a pole independent of k".into(),
);
}
let Some(m) = (-c_eff)
.numer()
.to_u64()
.filter(|m| *m <= MAX_GAMMA_ARG as u64)
else {
return GammaAt::Degenerate("a gamma pole index is too large".into());
};
let sign = if m % 2 == 0 { 1 } else { -1 };
let residue_scale = Rational::from(sign) / (Rational::from(factorial(m)) * Rational::from(g.b));
GammaAt::Pole { residue_scale }
}
impl HypTerm {
fn canonical(&self) -> Option<(Rn, Rational, Vec<GammaN>)> {
let mut coeff = self.coeff.clone();
let mut sig: Vec<GammaN> = Vec::with_capacity(self.gammas.len());
for g in &self.gammas {
let m = floor_rat(&g.c);
let m_i64 = m.to_i64().filter(|v| v.abs() <= MAX_GAMMA_SHIFT)?;
let c0 = g.c.clone() - Rational::from(m);
if g.a == 0 && c0 == 0 {
let value = Rational::from(factorial(u64::try_from(m_i64 - 1).ok()?));
coeff = rn_mul(&coeff, &rn_rat(rat_pow(&value, g.e as i64)?));
continue;
}
let mut ladder = rn_one();
let range = if m_i64 >= 0 { 0..m_i64 } else { m_i64..0 };
for t in range {
let factor = rn_add(
&rn_mul(&rn_int(g.a), &rn_var()),
&rn_rat(c0.clone() + Rational::from(t)),
);
ladder = rn_mul(&ladder, &factor);
}
let ladder = if m_i64 >= 0 {
ladder
} else {
super::qfield::rn_inv(&ladder)?
};
coeff = rn_mul(&coeff, &rn_pow(&ladder, g.e)?);
sig.push(GammaN {
a: g.a,
c: c0,
e: g.e,
});
}
sig.sort_by(|x, y| x.a.cmp(&y.a).then_with(|| x.c.cmp(&y.c)));
let mut merged: Vec<GammaN> = Vec::with_capacity(sig.len());
for g in sig {
match merged.last_mut() {
Some(prev) if prev.a == g.a && prev.c == g.c => {
prev.e = prev.e.checked_add(g.e)?;
}
_ => merged.push(g),
}
}
merged.retain(|g| g.e != 0);
Some((coeff, self.base.clone(), merged))
}
fn eval(&self, n0: i64) -> Option<Rational> {
let mut acc = rn_eval(&self.coeff, &Rational::from(n0))?;
acc *= rat_pow(&self.base, n0)?;
let mut vanishes = false;
for g in &self.gammas {
let arg = Rational::from(g.a) * Rational::from(n0) + g.c.clone();
if *arg.clone().denom() != 1 {
return None;
}
let m = arg.numer().to_i64().filter(|m| *m <= MAX_GAMMA_ARG)?;
if m <= 0 {
if g.e > 0 {
return None;
}
vanishes = true;
continue;
}
acc *= rat_pow(&Rational::from(factorial((m - 1) as u64)), g.e as i64)?;
}
Some(if vanishes { Rational::from(0) } else { acc })
}
}
const WITNESS_POINTS: i64 = 16;
fn nonzero_witness(terms: &[HypTerm], from: i64) -> Option<i64> {
'points: for n0 in from..from.saturating_add(WITNESS_POINTS) {
let mut acc = Rational::from(0);
for t in terms {
let Some(v) = t.eval(n0) else {
continue 'points;
};
acc += v;
}
if acc != 0 {
return Some(n0);
}
}
None
}
fn classes_expr(classes: &[(Rational, Vec<GammaN>, Rn)], n: ExprId, pool: &ExprPool) -> ExprId {
let mut parts: Vec<ExprId> = Vec::with_capacity(classes.len());
for (base, gammas, coeff) in classes {
if rn_is_zero(coeff) {
continue;
}
let mut factors = vec![rn_to_expr(pool, n, coeff)];
if *base != 1 {
factors.push(pool.pow(rational_expr(pool, base), n));
}
for g in gammas {
let gam = pool.func("gamma", vec![gamma_arg_expr(pool, n, g)]);
factors.push(if g.e == 1 {
gam
} else {
pool.pow(gam, pool.integer(g.e))
});
}
parts.push(pool.mul(factors));
}
let sum = match parts.len() {
0 => pool.integer(0_i32),
1 => parts[0],
_ => pool.add(parts),
};
crate::simplify::simplify(sum, pool).value
}
fn gamma_arg_expr(pool: &ExprPool, n: ExprId, g: &GammaN) -> ExprId {
let mut parts = Vec::with_capacity(2);
if g.a != 0 {
parts.push(if g.a == 1 {
n
} else {
pool.mul(vec![pool.integer(g.a), n])
});
}
if g.c != 0 || parts.is_empty() {
parts.push(rational_expr(pool, &g.c));
}
if parts.len() == 1 {
parts[0]
} else {
pool.add(parts)
}
}
fn rational_expr(pool: &ExprPool, q: &Rational) -> ExprId {
if *q.clone().denom() == 1 {
pool.integer(q.numer().clone())
} else {
pool.rational(q.numer().clone(), q.denom().clone())
}
}
fn synthetic_div(p: &PolyK, root: &Rn) -> (PolyK, Rn) {
let d = p.degree();
if d <= 0 {
return (PolyK::zero(), p.coeff(0));
}
let d = d as usize;
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(root, &q[i]));
}
let rem = rn_add(&p.coeff(0), &rn_mul(root, &q[0]));
(PolyK::from_coeffs(q), rem)
}
fn deflate_root(p: &PolyK, root: &Rn) -> (usize, PolyK) {
let mut cur = p.clone();
let mut m = 0usize;
while !cur.is_zero() {
let (quot, rem) = synthetic_div(&cur, root);
if !rn_is_zero(&rem) {
break;
}
cur = quot;
m += 1;
}
(m, cur)
}
fn poly_eval(p: &PolyK, x: &Rn) -> Rn {
let d = p.degree();
if d < 0 {
return rn_zero();
}
let mut acc = p.coeff(d as usize);
for i in (0..d as usize).rev() {
acc = rn_add(&rn_mul(&acc, x), &p.coeff(i));
}
acc
}
fn rn_pow(a: &Rn, e: i32) -> Option<Rn> {
if e == 0 {
return Some(rn_one());
}
let base = if e < 0 {
super::qfield::rn_inv(a)?
} else {
a.clone()
};
let mut acc = rn_one();
for _ in 0..e.unsigned_abs() {
acc = rn_mul(&acc, &base);
}
Some(acc)
}
const MAX_RAT_POW: u64 = 4096;
fn rat_pow(q: &Rational, e: i64) -> Option<Rational> {
if e == 0 {
return Some(Rational::from(1));
}
if e.unsigned_abs() > MAX_RAT_POW {
return None;
}
if *q == 0 {
return if e > 0 { Some(Rational::from(0)) } else { None };
}
let base = if e < 0 { q.clone().recip() } else { q.clone() };
let mut acc = Rational::from(1);
for _ in 0..e.unsigned_abs() {
acc *= base.clone();
}
Some(acc)
}
fn factorial(m: u64) -> Integer {
let mut acc = Integer::from(1);
for t in 2..=m {
acc *= t;
}
acc
}
fn floor_rat(q: &Rational) -> Integer {
let num = q.numer().clone();
let den = q.denom().clone();
let mut quot = num.clone() / den.clone();
if num < 0 && quot.clone() * den != num {
quot -= 1;
}
quot
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holonomic::zeilberger::{zeilberger, ZeilbergerOpts};
use crate::kernel::Domain;
fn nk(pool: &ExprPool) -> (ExprId, ExprId) {
(
pool.symbol("n", Domain::Real),
pool.symbol("k", Domain::Real),
)
}
fn binom(pool: &ExprPool, top: ExprId, bot: ExprId) -> ExprId {
let one = pool.integer(1_i32);
let g1 = pool.func("gamma", vec![pool.add(vec![top, one])]);
let g2 = pool.func("gamma", vec![pool.add(vec![bot, one])]);
let g3 = pool.func(
"gamma",
vec![pool.add(vec![top, pool.mul(vec![bot, pool.integer(-1_i32)]), one])],
);
pool.mul(vec![
g1,
pool.pow(g2, pool.integer(-1_i32)),
pool.pow(g3, pool.integer(-1_i32)),
])
}
fn verdict(
f: ExprId,
n: ExprId,
k: ExprId,
pool: &ExprPool,
limits: Option<(ExprId, ExprId)>,
) -> BoundaryStatus {
let cert = zeilberger(f, n, k, pool, &ZeilbergerOpts::default()).expect("certificate");
boundary_status(&cert.value, f, n, k, limits, pool)
}
#[test]
fn natural_boundary_is_proved_to_vanish() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let f = binom(&pool, n, k);
let status = verdict(f, n, k, &pool, Some(natural_limits(n, &pool)));
assert_eq!(status, BoundaryStatus::Vanishes, "got {status:?}");
assert!(status.implies_sum_recurrence());
assert_eq!(status.tag(), "vanishes");
}
#[test]
fn franel_boundary_vanishes() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let f = pool.mul(vec![c, c, c]);
let status = verdict(f, n, k, &pool, Some(natural_limits(n, &pool)));
assert_eq!(status, BoundaryStatus::Vanishes, "got {status:?}");
}
#[test]
fn dixon_boundary_vanishes() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let sign = pool.pow(pool.integer(-1_i32), k);
let f = pool.mul(vec![sign, c, c, c]);
let status = verdict(f, n, k, &pool, Some(natural_limits(n, &pool)));
assert_eq!(status, BoundaryStatus::Vanishes, "got {status:?}");
}
#[test]
fn apery_boundary_vanishes() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let d = binom(&pool, pool.add(vec![n, k]), k);
let f = pool.mul(vec![c, c, d, d]);
let status = verdict(f, n, k, &pool, Some(natural_limits(n, &pool)));
assert_eq!(status, BoundaryStatus::Vanishes, "got {status:?}");
}
#[test]
fn nonvanishing_boundary_returns_a_usable_inhomogeneous_recurrence() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let kp1 = pool.add(vec![k, pool.integer(1_i32)]);
let f = pool.mul(vec![
binom(&pool, n, k),
pool.pow(kp1, pool.integer(-1_i32)),
]);
let cert = zeilberger(f, n, k, &pool, &ZeilbergerOpts::default()).expect("certificate");
let status = boundary_status(&cert.value, f, n, k, Some(natural_limits(n, &pool)), &pool);
let BoundaryStatus::Nonzero { rhs, .. } = &status else {
panic!("expected a nonzero boundary, got {status:?}");
};
assert_eq!(status.tag(), "nonzero");
assert!(status.side_conditions("k = 0..n")[0].contains("FALSE"));
let s = |m: f64| (2.0_f64.powf(m + 1.0) - 1.0) / (m + 1.0);
for ni in 2..7 {
let env = std::collections::HashMap::from([(n, ni as f64)]);
let lhs: f64 = cert
.value
.coeffs
.iter()
.enumerate()
.map(|(i, &c)| {
crate::eval_f64(c, &pool, &env).expect("a_i(n)") * s((ni + i) as f64)
})
.sum();
let b = crate::eval_f64(*rhs, &pool, &env).expect("b(n) evaluates");
assert!(
(lhs - b).abs() < 1e-6 * lhs.abs().max(1.0),
"at n = {ni}: Σ a_i S(n+i) = {lhs} but b(n) = {b}"
);
assert!(b.abs() > 1e-9, "b({ni}) must not be zero");
}
}
#[test]
fn absent_limits_are_unknown_not_assumed() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let f = binom(&pool, n, k);
let status = verdict(f, n, k, &pool, None);
let BoundaryStatus::Unknown { reason } = &status else {
panic!("expected unknown, got {status:?}");
};
assert!(reason.contains("limits"), "{reason}");
assert!(!status.implies_sum_recurrence());
assert!(status.side_conditions("an unspecified range")[0].contains("NOTHING"));
}
#[test]
fn a_range_that_cannot_be_placed_is_unknown() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let m = pool.symbol("m", Domain::Real);
let f = binom(&pool, n, k);
let status = verdict(f, n, k, &pool, Some((pool.integer(0_i32), m)));
assert!(
matches!(status, BoundaryStatus::Unknown { .. }),
"got {status:?}"
);
}
#[test]
fn truncating_the_range_changes_the_verdict() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let f = pool.mul(vec![c, c, c]);
let cert = zeilberger(f, n, k, &pool, &ZeilbergerOpts::default()).expect("certificate");
let full = boundary_status(&cert.value, f, n, k, Some(natural_limits(n, &pool)), &pool);
assert_eq!(full, BoundaryStatus::Vanishes, "got {full:?}");
let n_minus_1 = pool.add(vec![n, pool.integer(-1_i32)]);
let short = boundary_status(
&cert.value,
f,
n,
k,
Some((pool.integer(0_i32), n_minus_1)),
&pool,
);
assert!(
matches!(short, BoundaryStatus::Nonzero { .. }),
"dropping the k = n term leaves C(n,n)³ behind: got {short:?}"
);
}
#[test]
fn root_deflation_counts_multiplicity_exactly() {
let root = rn_var();
let linear = PolyK::from_coeffs(vec![rn_neg(&root), rn_int(1)]);
let other = PolyK::from_coeffs(vec![rn_int(1), rn_int(1)]);
let p = linear.mul(&linear).mul(&linear).mul(&other);
let (m, rest) = deflate_root(&p, &root);
assert_eq!(m, 3);
assert!(rest.eq_poly(&other));
assert!(!rn_is_zero(&poly_eval(&rest, &root)));
}
#[test]
fn signed_windows_carry_their_orientation() {
assert_eq!(signed_window(1, 2), vec![(1, 1), (2, 1)]);
assert_eq!(signed_window(1, 0), Vec::<(i64, i32)>::new());
assert_eq!(signed_window(1, -1), vec![(0, -1)]);
assert_eq!(signed_window(0, -1), Vec::<(i64, i32)>::new());
assert_eq!(signed_window(-1, -1), vec![(-1, 1)]);
}
#[test]
fn a_pole_inside_the_range_is_unknown_not_vanishes() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
for m in 2..=6_i32 {
let den = pool.add(vec![k, pool.integer(-m)]);
let f = pool.mul(vec![
binom(&pool, n, k),
pool.pow(den, pool.integer(-1_i32)),
]);
let status = verdict(f, n, k, &pool, Some(natural_limits(n, &pool)));
let BoundaryStatus::Unknown { reason } = &status else {
panic!("C(n,k)/(k-{m}) must not get a verdict: got {status:?}");
};
assert!(
reason.contains(&format!("k = {m}")),
"the reason must name the offending index: {reason}"
);
assert!(!status.implies_sum_recurrence());
}
}
#[test]
fn a_pole_below_the_range_still_gets_a_verdict() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let den = pool.add(vec![k, pool.integer(1_i32)]);
let f = pool.mul(vec![
binom(&pool, n, k),
pool.pow(den, pool.integer(-1_i32)),
]);
let status = verdict(f, n, k, &pool, Some(natural_limits(n, &pool)));
assert!(
matches!(status, BoundaryStatus::Nonzero { .. }),
"got {status:?}"
);
}
fn full_verdict(
f: ExprId,
n: ExprId,
k: ExprId,
pool: &ExprPool,
limits: (ExprId, ExprId),
) -> BoundaryVerdict {
let cert = zeilberger(f, n, k, pool, &ZeilbergerOpts::default()).expect("certificate");
boundary_verdict(&cert.value, f, n, k, Some(limits), pool)
}
#[test]
fn a_range_that_runs_backwards_is_never_a_recurrence() {
for (lo, hi) in [(5, 3), (3, 1), (2, 0), (4, 2)] {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let f = pool.mul(vec![c, c]);
let limits = (pool.integer(lo), pool.integer(hi));
let status = verdict(f, n, k, &pool, Some(limits));
let BoundaryStatus::Unknown { reason } = &status else {
panic!("k = {lo}..{hi} is empty: expected unknown, got {status:?}");
};
assert!(reason.contains("backwards"), "{reason}");
assert!(!status.implies_sum_recurrence());
let full = full_verdict(f, n, k, &pool, limits);
assert_eq!(full.tag(), "unknown");
assert_eq!(full.valid_from, None);
}
}
#[test]
fn an_n_dependent_range_carries_the_n_it_is_claimed_for() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let f = binom(&pool, n, k);
let three = pool.integer(3_i32);
let limits = (three, pool.add(vec![n, pool.integer(-3_i32)]));
let full = full_verdict(f, n, k, &pool, limits);
assert_eq!(
full.valid_from,
Some(5),
"k = 3..n-3 runs backwards at n = 3, 4 and is a range from n = 5 on: {:?}",
full.status
);
assert!(full.implies_sum_recurrence(), "got {:?}", full.status);
let conds = full.side_conditions("k = 3..n - 3", &pool);
assert!(
conds
.iter()
.any(|c| c.contains("n >= 5") && c.contains("FALSE")),
"the domain must be stated: {conds:?}"
);
let status = verdict(f, n, k, &pool, Some(limits));
let BoundaryStatus::Unknown { reason } = &status else {
panic!("expected unknown from the bare status, got {status:?}");
};
assert!(reason.contains("n < 5"), "{reason}");
}
#[test]
fn an_exactly_empty_range_is_still_a_proved_zero() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let f = pool.mul(vec![c, c]);
let cases = [
(pool.integer(0_i32), pool.integer(-1_i32)),
(pool.add(vec![n, pool.integer(1_i32)]), n),
];
for limits in cases {
let full = full_verdict(f, n, k, &pool, limits);
assert_eq!(full.status, BoundaryStatus::Vanishes, "{:?}", full.status);
assert_eq!(
full.valid_from, None,
"no n is excluded by an adjacent-empty range"
);
assert!(full.certificate_poles.is_empty());
assert_eq!(verdict(f, n, k, &pool, Some(limits)).tag(), "vanishes");
}
}
#[test]
fn an_interior_certificate_pole_is_reported_and_refused() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let one = pool.integer(1_i32);
let den = pool.add(vec![n, pool.mul(vec![pool.integer(-2_i32), k]), one]);
let f = pool.mul(vec![
binom(&pool, n, k),
pool.pow(den, pool.integer(-1_i32)),
]);
let full = full_verdict(f, n, k, &pool, natural_limits(n, &pool));
assert_eq!(full.tag(), "unknown", "{:?}", full.status);
assert!(
!full.certificate_poles.is_empty(),
"the interior pole must be reported, not merely refused"
);
let shown: Vec<String> = full
.certificate_poles
.iter()
.map(|&p| pool.display(p).to_string())
.collect();
assert!(
shown.iter().any(|s| s.contains("/2") || s.contains("1/2")),
"expected a half-integer location, got {shown:?}"
);
let conds = full.side_conditions("k = 0..n", &pool);
assert!(conds.iter().any(|c| c.contains("interior")), "{conds:?}");
}
#[test]
fn a_summand_pole_inside_the_range_is_refused() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let den = pool.add(vec![k, pool.integer(-3_i32)]);
let f = pool.mul(vec![
binom(&pool, n, k),
pool.pow(den, pool.integer(-1_i32)),
]);
let full = full_verdict(f, n, k, &pool, natural_limits(n, &pool));
assert_eq!(full.tag(), "unknown", "{:?}", full.status);
let shown: Vec<String> = full
.certificate_poles
.iter()
.map(|&p| pool.display(p).to_string())
.collect();
assert_eq!(shown, vec!["3".to_string()], "got {shown:?}");
}
#[test]
fn the_verdicts_that_were_already_right_stay_right() {
let pool = ExprPool::new();
let (n, k) = nk(&pool);
let c = binom(&pool, n, k);
let sq = pool.mul(vec![c, c]);
let neg_n = pool.mul(vec![pool.integer(-1_i32), n]);
let wide = full_verdict(sq, n, k, &pool, (neg_n, n));
assert_eq!(wide.tag(), "unknown", "{:?}", wide.status);
let half = full_verdict(c, n, k, &pool, (neg_n, pool.integer(0_i32)));
assert_eq!(half.tag(), "nonzero", "{:?}", half.status);
let one = pool.integer(1_i32);
let den = pool.add(vec![n, pool.mul(vec![pool.integer(-1_i32), k]), one]);
let g = pool.mul(vec![c, pool.pow(den, pool.integer(-1_i32))]);
let cont = full_verdict(g, n, k, &pool, natural_limits(n, &pool));
assert_eq!(cont.tag(), "nonzero", "{:?}", cont.status);
assert!(cont.certificate_poles.is_empty());
}
#[test]
fn the_range_domain_is_the_first_n_that_is_not_backwards() {
let pt = |alpha, beta| Point { alpha, beta };
assert_eq!(range_domain(pt(0, 0), pt(1, 0)), Ok(Some(-1)));
assert_eq!(range_domain(pt(0, 3), pt(1, -3)), Ok(Some(5)));
assert_eq!(range_domain(pt(1, 1), pt(1, 0)), Ok(None));
assert_eq!(range_domain(pt(0, 0), pt(0, 0)), Ok(None));
assert!(range_domain(pt(0, 5), pt(0, 3)).is_err());
assert!(range_domain(pt(1, 0), pt(0, 0)).is_err());
assert_eq!(range_domain(pt(0, 4), pt(2, -3)), Ok(Some(3)));
}
#[test]
fn only_integer_locations_inside_the_range_count() {
let p = |s: (i64, i64), t: (i64, i64)| PolePoint {
s: Rational::from(s),
t: Rational::from(t),
};
assert!(p((1, 2), (1, 2)).hits_integers());
assert!(!p((1, 2), (1, 3)).hits_integers());
assert!(p((0, 1), (3, 1)).hits_integers());
let (lo, hi) = (Point { alpha: 0, beta: 0 }, Point { alpha: 1, beta: 0 });
assert!(p((1, 2), (1, 2)).inside(lo, hi));
assert!(p((0, 1), (3, 1)).inside(lo, hi));
assert!(!p((1, 1), (1, 1)).inside(lo, hi));
assert!(!p((0, 1), (-1, 1)).inside(lo, hi));
}
#[test]
fn floor_is_a_floor() {
assert_eq!(floor_rat(&Rational::from((7, 2))), Integer::from(3));
assert_eq!(floor_rat(&Rational::from((-7, 2))), Integer::from(-4));
assert_eq!(floor_rat(&Rational::from(-3)), Integer::from(-3));
assert_eq!(floor_rat(&Rational::from(0)), Integer::from(0));
}
}