use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use crate::poly::Poly;
use crate::poly::polybridge;
use crate::simplify::combsimp;
use crate::transforms::eval;
use crate::transforms::subs;
pub(crate) const MAX_CERTIFICATE_DEGREE: usize = 64;
pub(crate) const MAX_DISPERSION: i64 = 100;
#[must_use]
pub(crate) fn poly_shift(p: &Poly, n: i64) -> Poly {
let deg = match p.degree() {
Some(d) => d,
None => return Poly::zero(),
};
let shift_poly = Poly::from_coeffs(vec![Ratio::from_integer(BigInt::from(n)), Ratio::one()]);
let coeffs = p.coeffs();
let mut result = Poly::constant(coeffs[deg].clone());
for i in (0..deg).rev() {
result = &(&result * &shift_poly) + &Poly::constant(coeffs[i].clone());
}
result
}
fn root_modulus_bound(p: &Poly) -> Option<BigInt> {
let deg = p.degree()?;
if deg == 0 {
return Some(BigInt::zero());
}
let lc = p.leading_coeff()?.abs();
if lc.is_zero() {
return None;
}
let mut max_ratio = Ratio::<BigInt>::zero();
for i in 0..deg {
let ratio = p.coeff(i).abs() / &lc;
if ratio > max_ratio {
max_ratio = ratio;
}
}
Some((Ratio::<BigInt>::one() + max_ratio).ceil().to_integer())
}
#[must_use]
pub(crate) fn gosper_normal(p: &Poly, q: &Poly) -> Option<(Poly, Poly, Poly)> {
tracing::debug!("gosper_normal: entering");
if p.is_zero() || q.is_zero() {
return None;
}
let g = Poly::gcd(p, q);
let p = if g.is_constant() && g.coeff(0).is_one() {
p.clone()
} else {
p.div(&g)
};
let q = if g.is_constant() && g.coeff(0).is_one() {
q.clone()
} else {
q.div(&g)
};
let lc_p = p.leading_coeff()?.clone();
let lc_q = q.leading_coeff()?.clone();
let z = &lc_p / &lc_q;
let mut a = p.make_monic(); let mut b = q.make_monic(); let mut c = Poly::from_int(1);
let deg_a = a.degree().unwrap_or(0);
let deg_b = b.degree().unwrap_or(0);
if deg_a > 0 && deg_b > 0 {
let bound = root_modulus_bound(&a)? + root_modulus_bound(&b)?;
let bound = bound.to_i64().unwrap_or(MAX_DISPERSION).min(MAX_DISPERSION);
for i in 0..=bound {
if a.is_constant() || b.is_constant() {
break;
}
let b_shifted_i = poly_shift(&b, i);
let d = Poly::gcd(&a, &b_shifted_i);
if d.is_constant() {
continue; }
a = a.div(&d);
let d_back = poly_shift(&d, -i);
b = b.div(&d_back);
for j in 1..=i {
let d_shifted = poly_shift(&d, -j);
c = &c * &d_shifted;
}
}
}
let za = a.scale(&z);
tracing::debug!(
"gosper: normal form deg(A)={}, deg(B)={}, deg(C)={}",
za.degree().unwrap_or(0),
b.degree().unwrap_or(0),
c.degree().unwrap_or(0)
);
Some((za, b, c))
}
#[allow(clippy::needless_range_loop)]
fn solve_rational_system(
mat: &mut [Vec<Ratio<BigInt>>],
nrows: usize,
ncols: usize,
) -> Option<Vec<Ratio<BigInt>>> {
let mut pivot_row = 0;
let mut pivot_cols = Vec::new();
for col in 0..ncols {
let mut found = None;
for row in pivot_row..nrows {
if !mat[row][col].is_zero() {
found = Some(row);
break;
}
}
let Some(pr) = found else { continue };
mat.swap(pivot_row, pr);
let scale = mat[pivot_row][col].clone();
let scale_inv = Ratio::one() / &scale;
for j in col..=ncols {
let v = &mat[pivot_row][j] * &scale_inv;
mat[pivot_row][j] = v;
}
for row in 0..nrows {
if row == pivot_row {
continue;
}
let factor = mat[row][col].clone();
if factor.is_zero() {
continue;
}
for j in col..=ncols {
let sub = &factor * &mat[pivot_row][j];
mat[row][j] -= sub;
}
}
pivot_cols.push((pivot_row, col));
pivot_row += 1;
}
for row in pivot_row..nrows {
if !mat[row][ncols].is_zero() {
return None;
}
}
let mut solution = vec![Ratio::zero(); ncols];
for &(pr, col) in &pivot_cols {
solution[col] = mat[pr][ncols].clone();
}
Some(solution)
}
#[must_use]
#[allow(clippy::needless_range_loop)]
pub(crate) fn gosper_certificate(a: &Poly, b: &Poly, c: &Poly) -> Option<Poly> {
tracing::debug!("gosper_certificate: entering");
if c.is_zero() {
return Some(Poly::zero());
}
let b_m1 = poly_shift(b, -1);
let deg_a = a.degree().unwrap_or(0);
let deg_bm = b_m1.degree().unwrap_or(0);
let deg_c = c.degree().unwrap_or(0);
let n = deg_a.max(deg_bm);
let d = compute_degree_bound(a, &b_m1, c, deg_a, deg_bm, deg_c, n)?;
tracing::debug!("gosper: certificate degree bound d={}", d);
let total_deg = n + d;
let nrows = total_deg + 1;
let ncols = d + 1;
let mut mat = vec![vec![Ratio::<BigInt>::zero(); ncols + 1]; nrows];
for j in 0..ncols {
let kp1_j = poly_shift(&Poly::monomial(Ratio::one(), j), 1); let a_term = a * &kp1_j;
let kj = Poly::monomial(Ratio::one(), j);
let b_term = &b_m1 * &kj;
let col_poly = &a_term - &b_term;
for i in 0..nrows {
mat[i][j] = col_poly.coeff(i);
}
}
for i in 0..nrows {
mat[i][ncols] = c.coeff(i);
}
let coeffs = solve_rational_system(&mut mat, nrows, ncols)?;
let result = Poly::from_coeffs(coeffs);
let x_shifted = poly_shift(&result, 1);
let lhs = &(a * &x_shifted) - &(&b_m1 * &result);
if &lhs != c {
return None;
}
Some(result)
}
fn compute_degree_bound(
a: &Poly,
b_m1: &Poly,
c: &Poly,
deg_a: usize,
deg_bm: usize,
deg_c: usize,
n: usize,
) -> Option<usize> {
if c.is_zero() {
return Some(0);
}
let lc_a = if a.is_zero() {
Ratio::zero()
} else {
a.coeff(deg_a)
};
let lc_b = if b_m1.is_zero() {
Ratio::zero()
} else {
b_m1.coeff(deg_bm)
};
let bound = if deg_a == deg_bm && lc_a == lc_b {
let mut d = deg_c + 1;
if n >= 1 {
let next_a = a.coeff(n - 1);
let next_b = b_m1.coeff(n - 1);
let d0 = (&next_b - &next_a) / &lc_a;
if d0.is_integer() && !d0.is_negative() {
let d0 = d0.to_integer().to_usize()?;
d = d.max(d0);
}
}
d
} else {
deg_c.saturating_sub(n)
};
if bound > MAX_CERTIFICATE_DEGREE {
tracing::debug!(
"gosper: certificate degree bound {} exceeds cap {}",
bound,
MAX_CERTIFICATE_DEGREE
);
return None;
}
Some(bound)
}
pub(crate) fn hypergeometric_ratio(
arena: &mut Arena,
f: ExprId,
k: ExprId,
) -> Option<(ExprId, ExprId)> {
if !walk::free_symbols(arena, f).contains(&k) {
return Some((arena.one, arena.one));
}
if f == k {
let k1 = arena.add(&[k, arena.one]);
return Some((k1, k));
}
let node = arena.node(f).clone();
match node {
ExprNode::Factorial(arg) if arg == k => {
let k1 = arena.add(&[k, arena.one]);
Some((k1, arena.one))
}
ExprNode::Factorial(arg) => {
if let Some(arg_poly) = polybridge::expr_to_poly(arena, arg, k)
&& arg_poly.degree() == Some(1)
&& arg_poly.coeff(1).is_one()
{
let shifted = poly_shift(&arg_poly, 1);
let numer_expr = polybridge::poly_to_expr(arena, &shifted, k);
return Some((numer_expr, arena.one));
}
try_ratio_by_substitution(arena, f, k)
}
ExprNode::Binomial(n, kk) if kk == k && !walk::free_symbols(arena, n).contains(&k) => {
let n_minus_k = arena.sub(n, k);
let k1 = arena.add(&[k, arena.one]);
Some((n_minus_k, k1))
}
ExprNode::Binomial(n, m) => {
binomial_ratio(arena, n, m, k).or_else(|| try_ratio_by_substitution(arena, f, k))
}
ExprNode::Pow(base, exp) if exp == k => {
if !walk::free_symbols(arena, base).contains(&k) {
Some((base, arena.one))
} else {
None
}
}
ExprNode::Pow(base, exp)
if !walk::free_symbols(arena, base).contains(&k)
&& walk::free_symbols(arena, exp).contains(&k) =>
{
if let Some(exp_poly) = polybridge::expr_to_poly(arena, exp, k) {
let shifted = poly_shift(&exp_poly, 1);
let diff = &shifted - &exp_poly;
if diff.is_constant() {
let diff_expr = polybridge::poly_to_expr(arena, &diff, k);
let ratio = arena.pow(base, diff_expr);
let ratio = eval::eval(arena, ratio);
return Some((ratio, arena.one));
}
}
None
}
ExprNode::Pow(base, exp) if base == k && !walk::free_symbols(arena, exp).contains(&k) => {
let k1 = arena.add(&[k, arena.one]);
if let Some(r) = arena.as_num(exp) {
let r = r.clone();
if r.is_negative() {
let pos_exp = arena.neg(exp);
let numer = arena.pow(k, pos_exp);
let denom = arena.pow(k1, pos_exp);
return Some((numer, denom));
}
}
let numer = arena.pow(k1, exp);
let denom = arena.pow(k, exp);
Some((numer, denom))
}
ExprNode::Pow(base, exp)
if !walk::free_symbols(arena, exp).contains(&k)
&& walk::free_symbols(arena, base).contains(&k) =>
{
if let Some(r) = arena.as_num(exp) {
let r = r.clone();
if r.is_integer() {
let n_val: i64 = r.to_integer().to_i64()?;
if n_val != 0 && n_val.abs() <= 20 {
if let Some((bp, bq)) = hypergeometric_ratio(arena, base, k) {
let abs_exp = arena.int(n_val.abs());
let (top, bottom) = if n_val > 0 { (bp, bq) } else { (bq, bp) };
let numer = arena.pow(top, abs_exp);
let denom = arena.pow(bottom, abs_exp);
return Some((numer, denom));
}
}
}
}
None
}
ExprNode::Neg(inner) => hypergeometric_ratio(arena, inner, k),
ExprNode::Mul(ref children) => {
let children_vec: Vec<ExprId> = children.iter().copied().collect();
let mut numer_parts = Vec::new();
let mut denom_parts = Vec::new();
for &child in &children_vec {
let (p, q) = hypergeometric_ratio(arena, child, k)?;
if p != arena.one {
numer_parts.push(p);
}
if q != arena.one {
denom_parts.push(q);
}
}
let p = match numer_parts.len() {
0 => arena.one,
1 => numer_parts[0],
_ => arena.mul(&numer_parts),
};
let q = match denom_parts.len() {
0 => arena.one,
1 => denom_parts[0],
_ => arena.mul(&denom_parts),
};
Some((p, q))
}
ExprNode::Add(_) => {
if let Some(f_poly) = polybridge::expr_to_poly(arena, f, k) {
let shifted = poly_shift(&f_poly, 1);
let numer_expr = polybridge::poly_to_expr(arena, &shifted, k);
let denom_expr = polybridge::poly_to_expr(arena, &f_poly, k);
Some((numer_expr, denom_expr))
} else {
try_ratio_by_substitution(arena, f, k)
}
}
_ => try_ratio_by_substitution(arena, f, k),
}
}
const MAX_FACTORIAL_SHIFT: i64 = 8;
fn binomial_ratio(arena: &mut Arena, n: ExprId, m: ExprId, k: ExprId) -> Option<(ExprId, ExprId)> {
let sn = int_slope_in(arena, n, k)?;
let sm = int_slope_in(arena, m, k)?;
let nm = arena.sub(n, m);
let nm = eval::eval(arena, nm);
let snm = int_slope_in(arena, nm, k)?;
let (p1, q1) = factorial_shift_ratio(arena, n, sn)?;
let (p2, q2) = factorial_shift_ratio(arena, m, sm)?;
let (p3, q3) = factorial_shift_ratio(arena, nm, snm)?;
let numer = arena.mul(&[p1, q2, q3]);
let denom = arena.mul(&[q1, p2, p3]);
Some((numer, denom))
}
fn int_slope_in(arena: &mut Arena, e: ExprId, k: ExprId) -> Option<i64> {
if !walk::contains(arena, e, k) {
return Some(0);
}
let monomials = crate::calculus::summation::sym_poly_in(arena, e, k)?;
let mut slope = 0i64;
for (deg, coeff) in monomials {
match deg {
0 => {}
1 => {
let r = arena.as_num(coeff)?;
if !r.is_integer() {
return None;
}
slope = r.to_integer().to_i64()?;
}
_ => return None,
}
}
Some(slope)
}
fn factorial_shift_ratio(arena: &mut Arena, a: ExprId, s: i64) -> Option<(ExprId, ExprId)> {
if s.abs() > MAX_FACTORIAL_SHIFT {
return None;
}
let one = arena.one;
let mut factors = Vec::with_capacity(s.unsigned_abs() as usize);
if s > 0 {
for j in 1..=s {
let je = arena.int(j);
factors.push(arena.add(&[a, je]));
}
} else {
for j in 0..-s {
let je = arena.int(-j);
factors.push(arena.add(&[a, je]));
}
}
let prod = match factors.len() {
0 => one,
1 => factors[0],
_ => arena.mul(&factors),
};
Some(if s >= 0 { (prod, one) } else { (one, prod) })
}
fn try_ratio_by_substitution(arena: &mut Arena, f: ExprId, k: ExprId) -> Option<(ExprId, ExprId)> {
let k_plus_1 = arena.add(&[k, arena.one]);
let f_k1 = subs::subs(arena, f, k, k_plus_1);
let ratio = arena.div(f_k1, f);
let ratio = combsimp::combsimp(arena, ratio);
let ratio = eval::eval(arena, ratio);
let (numer, denom) = polybridge::as_numer_denom(arena, ratio);
let _ = polybridge::expr_to_poly(arena, numer, k)?;
let _ = polybridge::expr_to_poly(arena, denom, k)?;
Some((numer, denom))
}
#[must_use]
pub(crate) fn is_hypergeometric(arena: &mut Arena, term: ExprId, k: ExprId) -> Option<ExprId> {
if !matches!(arena.node(k), ExprNode::Symbol(_)) {
return None;
}
let (numer, denom) = hypergeometric_ratio(arena, term, k)?;
let numer = eval::eval(arena, numer);
let denom = eval::eval(arena, denom);
let numer_x = arena.expand_expr(numer);
let denom_x = arena.expand_expr(denom);
crate::calculus::summation::sym_poly_in(arena, numer_x, k)?;
crate::calculus::summation::sym_poly_in(arena, denom_x, k)?;
if arena.is_zero_structural(denom_x) {
return None;
}
let ratio = arena.div(numer_x, denom_x);
let ratio = if polybridge::expr_to_poly(arena, numer_x, k).is_some()
&& polybridge::expr_to_poly(arena, denom_x, k).is_some()
{
polybridge::cancel(arena, ratio, k)
} else {
ratio
};
Some(eval::eval(arena, ratio))
}
#[must_use]
pub(crate) fn gosper_sum(
arena: &mut Arena,
f_expr: ExprId,
k_var: ExprId,
lower: ExprId,
upper: ExprId,
) -> Option<ExprId> {
tracing::debug!("gosper_sum: entering");
if !matches!(arena.node(k_var), ExprNode::Symbol(_)) {
return None;
}
let (numer_expr, denom_expr) = hypergeometric_ratio(arena, f_expr, k_var)?;
let numer_expr_eval = eval::eval(arena, numer_expr);
let denom_expr_eval = eval::eval(arena, denom_expr);
let numer_expanded = arena.expand_expr(numer_expr_eval);
let denom_expanded = arena.expand_expr(denom_expr_eval);
let p = polybridge::expr_to_poly(arena, numer_expanded, k_var)?;
let q = polybridge::expr_to_poly(arena, denom_expanded, k_var)?;
if q.is_zero() {
return None;
}
let (a, b, c) = gosper_normal(&p, &q)?;
let x = gosper_certificate(&a, &b, &c)?;
let b_m1 = poly_shift(&b, -1);
let bm1_x = &b_m1 * &x;
let common = Poly::gcd(&bm1_x, &c);
let numer_poly = if common.is_constant() && common.coeff(0).is_one() {
bm1_x.clone()
} else {
bm1_x.div(&common)
};
let denom_poly = if common.is_constant() && common.coeff(0).is_one() {
c.clone()
} else {
c.div(&common)
};
let numer_expr2 = polybridge::poly_to_expr(arena, &numer_poly, k_var);
let denom_expr2 = polybridge::poly_to_expr(arena, &denom_poly, k_var);
let g_k = if denom_poly.is_constant() && denom_poly.coeff(0).is_one() {
arena.mul(&[numer_expr2, f_expr])
} else {
let frac = arena.div(numer_expr2, denom_expr2);
arena.mul(&[frac, f_expr])
};
let one = arena.one;
let upper_plus_1 = arena.add(&[upper, one]);
let g_upper = subs::subs(arena, g_k, k_var, upper_plus_1);
let g_lower = subs::subs(arena, g_k, k_var, lower);
let result = arena.sub(g_upper, g_lower);
let result = eval::eval(arena, result);
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
use num_rational::Ratio;
fn r(n: i64) -> Ratio<BigInt> {
Ratio::from_integer(BigInt::from(n))
}
fn display(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
#[test]
fn shift_constant() {
let p = Poly::from_int(5);
let shifted = poly_shift(&p, 3);
assert_eq!(shifted, Poly::from_int(5));
}
#[test]
fn shift_zero() {
let p = Poly::zero();
let shifted = poly_shift(&p, 10);
assert!(shifted.is_zero());
}
#[test]
fn shift_x_by_1() {
let p = Poly::x();
let shifted = poly_shift(&p, 1);
let expected = Poly::from_coeffs(vec![r(1), r(1)]);
assert_eq!(shifted, expected);
}
#[test]
fn shift_x_by_neg1() {
let p = Poly::x();
let shifted = poly_shift(&p, -1);
let expected = Poly::from_coeffs(vec![r(-1), r(1)]);
assert_eq!(shifted, expected);
}
#[test]
fn shift_x_squared_by_1() {
let p = Poly::from_coeffs(vec![r(0), r(0), r(1)]);
let shifted = poly_shift(&p, 1);
let expected = Poly::from_coeffs(vec![r(1), r(2), r(1)]);
assert_eq!(shifted, expected);
}
#[test]
fn shift_quadratic_by_2() {
let p = Poly::from_coeffs(vec![r(0), r(1), r(1)]); let shifted = poly_shift(&p, 2);
let expected = Poly::from_coeffs(vec![r(6), r(5), r(1)]);
assert_eq!(shifted, expected);
}
#[test]
fn shift_polynomial_roundtrip() {
let p = Poly::from_coeffs(vec![r(1), r(-2), r(3)]);
let shifted = poly_shift(&poly_shift(&p, 3), -3);
assert_eq!(shifted, p);
}
#[test]
fn shift_eval_consistency() {
let p = Poly::from_coeffs(vec![r(1), r(-2), r(3), r(1)]);
let val_direct = p.eval(&r(5));
let shifted = poly_shift(&p, 3);
let val_shifted = shifted.eval(&r(2));
assert_eq!(val_direct, val_shifted);
}
#[test]
fn root_bound_linear() {
let p = Poly::from_coeffs(vec![r(-3), r(1)]);
assert_eq!(root_modulus_bound(&p), Some(BigInt::from(4)));
}
#[test]
fn root_bound_quadratic_with_scaling() {
let p = Poly::from_coeffs(vec![r(4), r(-6), r(2)]);
assert_eq!(root_modulus_bound(&p), Some(BigInt::from(4)));
}
#[test]
fn root_bound_constant_and_zero() {
assert_eq!(root_modulus_bound(&Poly::from_int(7)), Some(BigInt::zero()));
assert_eq!(root_modulus_bound(&Poly::zero()), None);
assert_eq!(root_modulus_bound(&Poly::x()), Some(BigInt::from(1)));
}
#[test]
fn normal_form_high_degree_is_fast() {
let kp1 = Poly::from_coeffs(vec![r(1), r(1)]);
let mut p = Poly::from_int(2);
let mut q = Poly::from_int(1);
for _ in 0..8 {
p = &p * &kp1;
q = &q * &Poly::x();
}
let start = std::time::Instant::now();
let (a, b, c) = gosper_normal(&p, &q).unwrap();
assert!(start.elapsed().as_secs_f64() < 1.0, "normal form too slow");
assert_eq!(a, Poly::from_int(2));
assert_eq!(b, Poly::from_int(1));
assert_eq!(c, q);
}
#[test]
fn normal_form_dispersion_beyond_cap_is_declined_safely() {
let p = Poly::from_coeffs(vec![r(1000), r(1)]);
let q = Poly::x();
let start = std::time::Instant::now();
let (a, b, c) = gosper_normal(&p, &q).unwrap();
assert!(start.elapsed().as_secs_f64() < 2.0);
assert_eq!(c, Poly::from_int(1));
for kv in 1..=5 {
let k_val = r(kv);
let k1_val = r(kv + 1);
let lhs = &p.eval(&k_val) / &q.eval(&k_val);
let rhs = &(&a.eval(&k_val) * &c.eval(&k1_val)) / &(&b.eval(&k_val) * &c.eval(&k_val));
assert_eq!(lhs, rhs);
}
let p = Poly::from_coeffs(vec![r(40), r(1)]);
let (a, b, c) = gosper_normal(&p, &q).unwrap();
assert_eq!(a, Poly::from_int(1));
assert_eq!(b, Poly::from_int(1));
assert_eq!(c.degree(), Some(40));
}
#[test]
fn normal_form_basic_coprime() {
let p = Poly::from_coeffs(vec![r(2), r(1)]); let q = Poly::x(); let (a, b, c) = gosper_normal(&p, &q).unwrap();
for kv in 1..=10 {
let k_val = r(kv);
let k1_val = r(kv + 1);
let lhs = &p.eval(&k_val) / &q.eval(&k_val);
let rhs = &(&a.eval(&k_val) * &c.eval(&k1_val)) / &(&b.eval(&k_val) * &c.eval(&k_val));
assert_eq!(lhs, rhs, "mismatch at k={kv}");
}
}
#[test]
fn normal_form_already_coprime_shifted() {
let p = Poly::from_coeffs(vec![r(1), r(2), r(1)]); let q = Poly::x(); let (a, b, c) = gosper_normal(&p, &q).unwrap();
for kv in 1..=10 {
let k_val = r(kv);
let k1_val = r(kv + 1);
let lhs = &p.eval(&k_val) / &q.eval(&k_val);
let rhs = &(&a.eval(&k_val) * &c.eval(&k1_val)) / &(&b.eval(&k_val) * &c.eval(&k_val));
assert_eq!(lhs, rhs, "mismatch at k={kv}");
}
}
#[test]
fn normal_form_constant_ratio() {
let p = Poly::from_int(2);
let q = Poly::from_int(1);
let (a, b, c) = gosper_normal(&p, &q).unwrap();
assert_eq!(a, Poly::from_int(2));
assert_eq!(b, Poly::from_int(1));
assert_eq!(c, Poly::from_int(1));
}
#[test]
fn certificate_geometric_r2() {
let a = Poly::from_int(2);
let b = Poly::from_int(1);
let c = Poly::from_int(1);
let x = gosper_certificate(&a, &b, &c).unwrap();
assert_eq!(x, Poly::from_int(1));
}
#[test]
fn certificate_factorial_sum() {
let a = Poly::from_coeffs(vec![r(1), r(1)]); let b = Poly::from_int(1);
let c = Poly::x(); let x = gosper_certificate(&a, &b, &c).unwrap();
assert_eq!(x, Poly::from_int(1));
}
#[test]
fn certificate_k_times_kp1() {
let a = Poly::from_int(1);
let b = Poly::from_int(1);
let c = Poly::from_coeffs(vec![r(0), r(1), r(1)]); let x = gosper_certificate(&a, &b, &c).unwrap();
let x_shifted = poly_shift(&x, 1);
let diff = &x_shifted - &x;
assert_eq!(diff, c);
}
#[test]
fn certificate_harmonic_no_solution() {
let a = Poly::x();
let b = Poly::from_coeffs(vec![r(1), r(1)]); let c = Poly::from_int(1);
assert!(gosper_certificate(&a, &b, &c).is_none());
}
#[test]
fn sum_geometric_2k() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let n = arena.symbol("n");
let two = arena.int(2);
let body = arena.pow(two, k); let lower = arena.zero;
let result = gosper_sum(&mut arena, body, k, lower, n);
assert!(result.is_some(), "2^k should be Gosper-summable");
let result = result.unwrap();
let ten = arena.int(10);
let evaluated = subs::subs(&mut arena, result, n, ten);
let evaluated = eval::eval(&mut arena, evaluated);
assert_eq!(display(&arena, evaluated), "2047");
}
#[test]
fn sum_geometric_3k() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let n = arena.symbol("n");
let three = arena.int(3);
let body = arena.pow(three, k);
let lower = arena.zero;
let result = gosper_sum(&mut arena, body, k, lower, n);
assert!(result.is_some(), "3^k should be Gosper-summable");
let result = result.unwrap();
let five = arena.int(5);
let evaluated = subs::subs(&mut arena, result, n, five);
let evaluated = eval::eval(&mut arena, evaluated);
assert_eq!(display(&arena, evaluated), "364");
}
#[test]
fn sum_k_factorial_k() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let n = arena.symbol("n");
let k_fact = arena.factorial(k);
let body = arena.mul(&[k, k_fact]); let lower = arena.zero;
let result = gosper_sum(&mut arena, body, k, lower, n);
assert!(result.is_some(), "k·k! should be Gosper-summable");
let result = result.unwrap();
let five = arena.int(5);
let evaluated = subs::subs(&mut arena, result, n, five);
let evaluated = eval::eval(&mut arena, evaluated);
assert_eq!(display(&arena, evaluated), "719");
let three = arena.int(3);
let evaluated = subs::subs(&mut arena, result, n, three);
let evaluated = eval::eval(&mut arena, evaluated);
assert_eq!(display(&arena, evaluated), "23");
}
fn check_gosper(arena: &mut Arena, body: ExprId, k: ExprId, lo: i64, ns: &[i64]) -> ExprId {
let n = arena.symbol("n");
let lo_id = arena.int(lo);
let result = gosper_sum(arena, body, k, lo_id, n)
.unwrap_or_else(|| panic!("{} should be Gosper-summable", display(arena, body)));
for &nv in ns {
let mut terms = Vec::new();
for i in lo..=nv {
let iv = arena.int(i);
let t = subs::subs(arena, body, k, iv);
terms.push(eval::eval(arena, t));
}
let expected = arena.add(&terms);
let expected = eval::eval(arena, expected);
let nv_id = arena.int(nv);
let got = subs::subs(arena, result, n, nv_id);
let got = eval::eval(arena, got);
assert_eq!(
display(arena, got),
display(arena, expected),
"n = {nv}: {}",
display(arena, result)
);
}
result
}
#[test]
fn sum_k_times_2_pow_k() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let two = arena.int(2);
let two_k = arena.pow(two, k);
let body = arena.mul(&[k, two_k]);
check_gosper(&mut arena, body, k, 0, &[0, 1, 2, 5, 10]);
}
#[test]
fn sum_quadratic_times_3_pow_k() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let three = arena.int(3);
let three_k = arena.pow(three, k);
let two = arena.int(2);
let k2 = arena.pow(k, two);
let quad = arena.add(&[k2, k]);
let body = arena.mul(&[quad, three_k]);
check_gosper(&mut arena, body, k, 0, &[0, 1, 3, 6]);
}
#[test]
fn sum_reciprocal_k_k_plus_1() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let one = arena.one;
let k1 = arena.add(&[k, one]);
let denom = arena.mul(&[k, k1]);
let body = arena.div(one, denom);
let result = check_gosper(&mut arena, body, k, 1, &[1, 2, 3, 9]);
assert!(!walk::has_unevaluated(&arena, result));
}
#[test]
fn is_hypergeometric_ratios() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let n = arena.symbol("n");
let kf = arena.factorial(k);
let r = is_hypergeometric(&mut arena, kf, k).unwrap();
assert_eq!(display(&arena, r), "k + 1");
let c = arena.binomial(n, k);
let r = is_hypergeometric(&mut arena, c, k).unwrap();
let five = arena.int(5);
let two = arena.int(2);
let at = subs::subs(&mut arena, r, n, five);
let at = subs::subs(&mut arena, at, k, two);
let at = eval::eval(&mut arena, at);
assert_eq!(display(&arena, at), "1");
let r = is_hypergeometric(&mut arena, n, k).unwrap();
assert_eq!(display(&arena, r), "1");
let s = arena.sin(k);
assert!(is_hypergeometric(&mut arena, s, k).is_none());
let kk = arena.pow(k, k);
assert!(is_hypergeometric(&mut arena, kk, k).is_none());
let h = arena.harmonic(k);
assert!(is_hypergeometric(&mut arena, h, k).is_none());
assert!(is_hypergeometric(&mut arena, kf, five).is_none());
}
#[test]
fn binomial_ratio_with_k_in_both_arguments() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let c = arena.symbol("c");
let kc = arena.add(&[k, c]);
let b = arena.binomial(kc, k);
let r = is_hypergeometric(&mut arena, b, k).unwrap();
let two = arena.int(2);
let three = arena.int(3);
let at = subs::subs(&mut arena, r, c, two);
let at = subs::subs(&mut arena, at, k, three);
let at = eval::eval(&mut arena, at);
assert_eq!(display(&arena, at), "3/2");
let b2 = arena.binomial(kc, c);
let r2 = is_hypergeometric(&mut arena, b2, k).unwrap();
let at = subs::subs(&mut arena, r2, c, two);
let at = subs::subs(&mut arena, at, k, three);
let at = eval::eval(&mut arena, at);
assert_eq!(display(&arena, at), "3/2");
let two_k = arena.mul(&[two, k]);
let b3 = arena.binomial(two_k, k);
let r3 = is_hypergeometric(&mut arena, b3, k).unwrap();
let at = subs::subs(&mut arena, r3, k, three);
let at = eval::eval(&mut arena, at);
assert_eq!(display(&arena, at), "7/2");
let n = arena.symbol("n");
let k2 = arena.add(&[k, two]);
let bin = arena.binomial(k2, k);
let half = arena.rational(1, 2);
let hk = arena.pow(half, k);
let body = arena.mul(&[bin, hk]);
let zero = arena.zero;
let s = gosper_sum(&mut arena, body, k, zero, n).unwrap();
let four = arena.int(4);
let at = subs::subs(&mut arena, s, n, four);
let at = eval::eval(&mut arena, at);
assert_eq!(display(&arena, at), "99/16");
}
#[test]
fn certificate_degree_bound_uses_second_coefficients() {
let a = Poly::from_int(1);
let b = Poly::from_int(1);
let c = Poly::from_coeffs(vec![r(0), r(0), r(0), r(1)]);
let x = gosper_certificate(&a, &b, &c).expect("certificate");
assert_eq!(x.degree(), Some(4));
let a = Poly::from_coeffs(vec![r(0), r(-3), r(1)]);
let b = Poly::from_coeffs(vec![r(-3), r(1), r(1)]);
let c = Poly::from_int(1);
assert_eq!(
poly_shift(&b, -1),
Poly::from_coeffs(vec![r(-3), r(-1), r(1)])
);
let x = gosper_certificate(&a, &b, &c).expect("degree-2 certificate");
assert_eq!(x.degree(), Some(2));
assert_eq!(
x,
Poly::from_coeffs(vec![
Ratio::new(BigInt::from(1), BigInt::from(3)),
Ratio::new(BigInt::from(4), BigInt::from(9)),
Ratio::new(BigInt::from(-2), BigInt::from(9))
])
);
let lhs = &(&a * &poly_shift(&x, 1)) - &(&poly_shift(&b, -1) * &x);
assert_eq!(lhs, c);
}
#[test]
fn certificate_degree_cap_declines_huge_systems() {
let a = Poly::from_int(1);
let b = Poly::from_int(1);
let c = Poly::monomial(r(1), MAX_CERTIFICATE_DEGREE + 6);
let start = std::time::Instant::now();
assert!(gosper_certificate(&a, &b, &c).is_none());
assert!(start.elapsed().as_secs_f64() < 1.0);
let c = Poly::monomial(r(1), 8);
assert!(gosper_certificate(&a, &b, &c).is_some());
}
#[test]
fn sum_harmonic_not_summable() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let n = arena.symbol("n");
let body = arena.div(arena.one, k); let lower = arena.one;
let result = gosper_sum(&mut arena, body, k, lower, n);
assert!(
result.is_none(),
"harmonic series should NOT be Gosper-summable"
);
}
#[test]
fn sum_binomial_not_gosper_summable() {
let mut arena = Arena::new();
let k = arena.symbol("k");
let n = arena.symbol("n");
let body = arena.binomial(n, k);
let lower = arena.zero;
let result = gosper_sum(&mut arena, body, k, lower, n);
assert!(result.is_none(), "C(n,k) should NOT be Gosper-summable");
}
}