use super::poly::{Axis, PolyM};
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)
}
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);
}
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, Telescoping2dOpts};
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 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:?}"
);
}
#[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");
}
}