use super::poly::{Axis, PolyM, AXIS_N};
use super::search::{Telescoping2dResult, TelescopingMdResult};
use super::term::{affine_partsm, as_ratm, GammaFactorM, ProperTermM};
use crate::kernel::{ExprId, ExprPool};
use rug::Rational;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoundaryStatus2d {
Vanishes,
Nonzero { rhs: ExprId, witness_n: i64 },
Unknown { reason: String },
}
impl BoundaryStatus2d {
pub fn tag(&self) -> &'static str {
match self {
BoundaryStatus2d::Vanishes => "vanishes",
BoundaryStatus2d::Nonzero { .. } => "nonzero",
BoundaryStatus2d::Unknown { .. } => "unknown",
}
}
pub fn implies_sum_recurrence(&self) -> bool {
!matches!(self, BoundaryStatus2d::Unknown { .. })
}
pub fn side_conditions(&self, j_range: &str, k_range: &str) -> Vec<String> {
match self {
BoundaryStatus2d::Vanishes => vec![format!(
"all four boundary strips for {j_range}, {k_range} were proved to \
vanish identically (pointwise, which is sufficient but not necessary), so the \
homogeneous recurrence sum_i a_i(n)*S(n+i) = 0 holds for the double sum"
)],
BoundaryStatus2d::Nonzero { witness_n, .. } => vec![format!(
"the boundary does not vanish; b({witness_n}) != 0 was checked exactly"
)],
BoundaryStatus2d::Unknown { reason } => vec![format!(
"the 2-D boundary for {j_range}, {k_range} could not be decided \
({reason}); the certificate proves the telescoping identity in (j,k) and \
NOTHING follows about sum_j sum_k F(n,j,k) until this is discharged \
independently"
)],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoundaryStatusMd {
Vanishes,
Nonzero { rhs: ExprId, witness_n: i64 },
Unknown { reason: String },
}
impl BoundaryStatusMd {
pub fn tag(&self) -> &'static str {
match self {
BoundaryStatusMd::Vanishes => "vanishes",
BoundaryStatusMd::Nonzero { .. } => "nonzero",
BoundaryStatusMd::Unknown { .. } => "unknown",
}
}
pub fn implies_sum_recurrence(&self) -> bool {
!matches!(self, BoundaryStatusMd::Unknown { .. })
}
pub fn side_conditions(&self, ranges: &[String]) -> Vec<String> {
let joined = ranges.join(", ");
match self {
BoundaryStatusMd::Vanishes => vec![format!(
"all {} boundary faces for {joined} were proved to vanish identically \
(pointwise, which is sufficient but not necessary), so the homogeneous \
recurrence sum_i a_i(n)*S(n+i) = 0 holds for the {}-fold sum",
2 * ranges.len(),
ranges.len()
)],
BoundaryStatusMd::Nonzero { witness_n, .. } => vec![format!(
"the boundary does not vanish; b({witness_n}) != 0 was checked exactly"
)],
BoundaryStatusMd::Unknown { reason } => vec![format!(
"the boundary for {joined} could not be decided ({reason}); the certificate \
proves the telescoping identity in the bound indices and NOTHING follows \
about the sum until this is discharged independently"
)],
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn boundary_status_2d(
result: &Telescoping2dResult,
term: ExprId,
n: ExprId,
j: ExprId,
k: ExprId,
j_limits: (ExprId, ExprId),
k_limits: (ExprId, ExprId),
pool: &ExprPool,
) -> BoundaryStatus2d {
let md_result = TelescopingMdResult {
order: result.order,
coeffs: result.coeffs.clone(),
certs: vec![result.cert1, result.cert2],
};
match boundary_status_md(&md_result, term, n, &[j, k], &[j_limits, k_limits], pool) {
BoundaryStatusMd::Vanishes => BoundaryStatus2d::Vanishes,
BoundaryStatusMd::Nonzero { rhs, witness_n } => {
BoundaryStatus2d::Nonzero { rhs, witness_n }
}
BoundaryStatusMd::Unknown { reason } => BoundaryStatus2d::Unknown { reason },
}
}
pub fn boundary_status_md(
result: &TelescopingMdResult,
term: ExprId,
n: ExprId,
indices: &[ExprId],
limits: &[(ExprId, ExprId)],
pool: &ExprPool,
) -> BoundaryStatusMd {
match analyze_md(result, term, n, indices, limits, pool) {
Ok(()) => BoundaryStatusMd::Vanishes,
Err(reason) => BoundaryStatusMd::Unknown { reason },
}
}
fn const_limit(
expr: ExprId,
n: ExprId,
indices: &[ExprId],
pool: &ExprPool,
) -> Result<Rational, String> {
let (coeffs, d) = affine_partsm(expr, n, indices, pool).ok_or_else(|| {
format!(
"limit {} is not an integer-affine expression in n and the bound indices",
pool.display(expr)
)
})?;
if coeffs.iter().any(|&c| c != 0) {
return Err(format!(
"limit {} depends on n or on a bound index; this module only supports constant \
(n-independent) rectangular ranges — see the module docs",
pool.display(expr)
));
}
if *d.clone().denom() != 1 {
return Err(format!("limit {} is not an integer", pool.display(expr)));
}
Ok(d)
}
pub const MAX_BOX_SCAN: u64 = 200_000;
fn box_bounds(los: &[Rational], his_p1: &[Rational]) -> Result<Vec<(i64, i64)>, String> {
let mut out = Vec::with_capacity(los.len());
for (t, (lo, hi1)) in los.iter().zip(his_p1).enumerate() {
match (lo.numer().to_i64(), hi1.numer().to_i64()) {
(Some(a), Some(b)) => out.push((a, b)),
_ => {
return Err(format!(
"the limits for bound index x{} are outside the range this analysis can \
scan for points where the summand is undefined",
t + 1
))
}
}
}
Ok(out)
}
fn gamma_label(g: &GammaFactorM) -> String {
let mut parts: Vec<String> = Vec::new();
for (axis, &c) in g.coeffs.iter().enumerate() {
if c == 0 {
continue;
}
let name = if axis == AXIS_N {
"n".to_string()
} else {
format!("x{axis}")
};
parts.push(match c {
1 => name,
-1 => format!("-{name}"),
_ => format!("{c}*{name}"),
});
}
if g.d != 0 || parts.is_empty() {
parts.push(g.d.to_string());
}
format!("gamma({})^{}", parts.join(" + "), g.e)
}
fn gamma_pole_in_box(g: &GammaFactorM, bounds: &[(i64, i64)]) -> bool {
if g.e <= 0 {
return false;
}
if *g.d.clone().denom() != 1 {
return false;
}
if g.coeffs[AXIS_N] != 0 {
return true;
}
let mut min = g.d.clone();
for (t, &(lo, hi)) in bounds.iter().enumerate() {
let c = g.coeffs[t + 1];
let at = if c >= 0 { lo } else { hi };
min += Rational::from(c) * Rational::from(at);
}
min <= 0
}
fn rational_pole_in_box(den: &PolyM, bounds: &[(i64, i64)], num_axes: usize) -> Option<String> {
if let Some(c) = den.as_constant() {
return (c == 0)
.then(|| "its rational prefactor has an identically zero denominator".to_string());
}
let mut total: u64 = 1;
for &(lo, hi) in bounds {
let width = u64::try_from(i128::from(hi) - i128::from(lo) + 1).unwrap_or(u64::MAX);
total = total.saturating_mul(width);
if total > MAX_BOX_SCAN {
return Some(format!(
"its rational prefactor has a non-constant denominator and the box has more \
than {MAX_BOX_SCAN} integer points, more than this analysis will scan for a \
zero of it"
));
}
}
let mut point: Vec<i64> = bounds.iter().map(|&(lo, _)| lo).collect();
loop {
let mut p = den.clone();
for (t, &v) in point.iter().enumerate() {
p = p.eliminate_axis(t + 1, &PolyM::constant(Rational::from(v), num_axes));
}
let at: Vec<String> = point
.iter()
.enumerate()
.map(|(t, v)| format!("x{} = {v}", t + 1))
.collect();
match p.as_constant() {
Some(c) if c != 0 => {}
Some(_) => {
return Some(format!(
"its rational prefactor divides by zero at {}, for every n",
at.join(", ")
))
}
None => {
return Some(format!(
"its rational prefactor's denominator at {} still depends on n, so it was \
not shown to be non-zero at every integer n",
at.join(", ")
))
}
}
let mut t = 0;
loop {
if t == point.len() {
return None;
}
point[t] += 1;
if point[t] <= bounds[t].1 {
break;
}
point[t] = bounds[t].0;
t += 1;
}
}
}
fn summand_pole_in_box(f: &ProperTermM, bounds: &[(i64, i64)]) -> Option<String> {
if bounds.iter().any(|&(lo, hi)| lo > hi) {
return None;
}
for g in &f.gammas {
if gamma_pole_in_box(g, bounds) {
return Some(format!(
"its factor {} has a non-positive integer argument — a pole of gamma — \
somewhere on the box",
gamma_label(g)
));
}
}
rational_pole_in_box(&f.rat.den, bounds, bounds.len() + 1)
}
fn analyze_md(
result: &TelescopingMdResult,
term: ExprId,
n: ExprId,
indices: &[ExprId],
limits: &[(ExprId, ExprId)],
pool: &ExprPool,
) -> Result<(), String> {
let m = indices.len();
if limits.len() != m {
return Err(format!(
"{m} bound indices were supplied but {} limit pairs",
limits.len()
));
}
if result.certs.len() != m {
return Err(format!(
"result carries {} certificates but {m} bound indices were supplied",
result.certs.len()
));
}
let f = ProperTermM::parse(term, n, indices, pool)
.map_err(|e| format!("term does not re-parse as proper hypergeometric: {e}"))?;
let mut certs = Vec::with_capacity(m);
for (t, &c) in result.certs.iter().enumerate() {
let parsed = as_ratm(c, n, indices, pool, 0)
.ok_or_else(|| format!("certificate {} does not parse back into Q(n,x)", t + 1))?;
certs.push(parsed);
}
let mut los = Vec::with_capacity(m);
let mut his_p1 = Vec::with_capacity(m);
for &(lo_e, hi_e) in limits {
let lo = const_limit(lo_e, n, indices, pool)?;
let hi = const_limit(hi_e, n, indices, pool)?;
his_p1.push(hi + Rational::from(1));
los.push(lo);
}
let bounds = box_bounds(&los, &his_p1)?;
if let Some(why) = summand_pole_in_box(&f, &bounds) {
return Err(format!(
"the summand was not shown to be defined at every integer point of the box and its \
{} boundary faces: {why}. S(n) has no value at such a point, and a face through one \
is not the zero function but a zero times an infinity, so no recurrence for the sum \
follows from the certificate",
2 * m
));
}
for t in 0..m {
let axis: Axis = t + 1;
for (label, value) in [
(format!("x{} = lo", t + 1), &los[t]),
(format!("x{} = hi + 1", t + 1), &his_p1[t]),
] {
if !face_vanishes(
&f.gammas,
&f.rat.den,
&certs[t].num,
&certs[t].den,
axis,
value,
m + 1,
) {
return Err(format!(
"boundary face at {label} was not provably zero by the (deliberately \
conservative) pointwise criterion this module implements"
));
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn face_vanishes(
gammas: &[GammaFactorM],
rat_den: &PolyM,
cert_num: &PolyM,
cert_den: &PolyM,
axis: Axis,
value: &Rational,
num_axes: usize,
) -> bool {
let mut zero_order: i64 = 0;
let mut pole_order: i64 = 0;
for g in gammas {
let elim_coeff = g.coeffs[axis];
let other_nonzero = (0..num_axes).any(|a| a != axis && g.coeffs[a] != 0);
if other_nonzero {
continue;
}
let new_const = g.d.clone() + Rational::from(elim_coeff) * value.clone();
if *new_const.clone().denom() != 1 {
continue;
}
let mnum = new_const.numer().clone();
if mnum > 0 {
continue; }
if g.e < 0 {
zero_order += (-g.e) as i64;
} else if g.e > 0 {
pole_order += g.e as i64;
}
}
if pole_order > 0 {
return false; }
let const_poly = PolyM::constant(value.clone(), num_axes);
if rat_den.eliminate_axis(axis, &const_poly).is_zero() {
return false; }
if cert_den.eliminate_axis(axis, &const_poly).is_zero() {
return false; }
if zero_order > 0 {
return true; }
cert_num.eliminate_axis(axis, &const_poly).is_zero()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holonomic::telescoping2d::search::{
telescope2d_search, telescope_md_search, Telescoping2dOpts, TelescopingMdOpts,
};
use crate::kernel::Domain;
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(pool: &ExprPool, top: ExprId, bot: ExprId) -> ExprId {
pool.func("binomial", vec![top, bot])
}
#[test]
fn 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,
binom(&pool, pool.integer(10_i32), j),
binom(&pool, pool.integer(10_i32), k),
]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts).expect("certificate");
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 boundary to vanish, got {status:?}"
);
}
#[test]
fn growing_support_correctly_refuses_vanishes() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let f = pool.mul(vec![binom(&pool, n, j), binom(&pool, n, k)]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts).expect("certificate");
let lo = pool.integer(0_i32);
let hi = pool.integer(30_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 dominate n-growing support; got {status:?}"
);
}
fn s_falling(ni: u32) -> rug::Rational {
let fact = |v: u32| -> Integer { Integer::from(Integer::factorial(v)) };
let mut acc = rug::Rational::from(0);
for kk in 0..=10u32 {
let c = fact(10) / (fact(kk) * fact(10 - kk));
acc += rug::Rational::from((c * fact(ni - kk), fact(ni)));
}
acc
}
#[test]
fn positive_gamma_pole_on_a_face_refuses_vanishes() {
let residual =
rug::Rational::from(110) * s_falling(11) - rug::Rational::from(100) * s_falling(10);
assert_eq!(
residual,
rug::Rational::from((1, Integer::from(Integer::factorial(9)))),
"the homogeneous recurrence is false at n = 9 by exactly 1/9!"
);
let pool = ExprPool::new();
let (n, _j, k) = njk(&pool);
let one = pool.integer(1_i32);
let gamma = |a: ExprId| pool.func("gamma", vec![a]);
let f = pool.mul(vec![
binom(&pool, pool.integer(10_i32), k),
gamma(pool.add(vec![n, pool.mul(vec![k, pool.integer(-1_i32)]), one])),
pool.pow(gamma(pool.add(vec![n, one])), pool.integer(-1_i32)),
]);
let opts = TelescopingMdOpts {
max_order: 2,
max_a_degree: 2,
max_cert_degree: 5,
};
let result = telescope_md_search(f, n, &[k], &pool, &opts).expect("certificate");
let status = boundary_status_md(
&result,
f,
n,
&[k],
&[(pool.integer(0_i32), pool.integer(10_i32))],
&pool,
);
assert_eq!(
status.tag(),
"unknown",
"a gamma pole on the face cancels the 1/gamma zero; got {status:?}"
);
}
#[test]
fn summand_pole_inside_the_box_refuses_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,
binom(&pool, pool.integer(10_i32), j),
binom(&pool, j, k),
pool.pow(
pool.add(vec![k, pool.integer(-3_i32)]),
pool.integer(-1_i32),
),
]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts).expect("certificate");
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(),
"unknown",
"S(n) does not exist: the k = 3 term is a division by zero; got {status:?}"
);
}
#[test]
fn positive_gamma_bounded_away_from_its_poles_still_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,
binom(&pool, pool.integer(10_i32), j),
binom(&pool, j, k),
]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts).expect("certificate");
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",
"the natural boundary is genuinely zero here; got {status:?}"
);
}
#[test]
fn a_box_too_large_to_scan_is_unknown_rather_than_assumed() {
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,
binom(&pool, pool.integer(10_i32), j),
binom(&pool, j, k),
pool.pow(
pool.add(vec![k, pool.integer(-3_i32)]),
pool.integer(-1_i32),
),
]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts).expect("certificate");
let lo = pool.integer(0_i32);
let hi = pool.integer(1_000_i32);
let status = boundary_status_2d(&result, f, n, j, k, (lo, hi), (lo, hi), &pool);
assert_eq!(status.tag(), "unknown");
}
#[test]
fn n_dependent_limit_is_unknown() {
let pool = ExprPool::new();
let (n, j, k) = njk(&pool);
let f = pool.mul(vec![binom(&pool, n, j), binom(&pool, n, k)]);
let opts = Telescoping2dOpts::default();
let result = telescope2d_search(f, n, j, k, &pool, &opts).expect("certificate");
let lo = pool.integer(0_i32);
let status = boundary_status_2d(
&result,
f,
n,
j,
k,
(lo, n),
(lo, pool.integer(30_i32)),
&pool,
);
assert_eq!(status.tag(), "unknown");
}
}