pub mod cyclotomic;
pub mod field;
pub mod rootofunity;
pub mod search;
pub mod term;
pub use cyclotomic::{cyclotomic_polynomial, CycloElem, CycloField, MAX_CYCLOTOMIC_ORDER};
pub use rootofunity::{
q_specialize_at_root_of_unity, QRootOfUnitySpecialization, QRootOfUnityStatus,
};
pub use search::{q_zeilberger_on_term, QZeilbergerOpts, QZeilbergerReport, QZeilbergerResult};
pub use term::{QProperTerm, QSupport};
use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::holonomic::qfield::{rn_add, rn_is_zero, rn_mul, rn_one, Rn};
use crate::kernel::{ExprId, ExprPool};
use rug::Rational;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QHolonomicError {
NotQHypergeometric(String),
SearchExhausted(String),
CertificateVerificationFailed(String),
InvalidInput(String),
Unsupported(String),
}
impl fmt::Display for QHolonomicError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QHolonomicError::NotQHypergeometric(s) => {
write!(f, "q-holonomic: not a q-hypergeometric term: {s}")
}
QHolonomicError::SearchExhausted(s) => write!(f, "q-holonomic: search exhausted: {s}"),
QHolonomicError::CertificateVerificationFailed(s) => {
write!(f, "q-holonomic: certificate failed exact verification: {s}")
}
QHolonomicError::InvalidInput(s) => write!(f, "q-holonomic: invalid input: {s}"),
QHolonomicError::Unsupported(s) => write!(f, "q-holonomic: unsupported: {s}"),
}
}
}
impl std::error::Error for QHolonomicError {}
impl crate::errors::AlkahestError for QHolonomicError {
fn code(&self) -> &'static str {
match self {
QHolonomicError::NotQHypergeometric(_) => "E-HOLO-020",
QHolonomicError::SearchExhausted(_) => "E-HOLO-021",
QHolonomicError::CertificateVerificationFailed(_) => "E-HOLO-022",
QHolonomicError::InvalidInput(_) => "E-HOLO-023",
QHolonomicError::Unsupported(_) => "E-HOLO-024",
}
}
fn remediation(&self) -> Option<&'static str> {
Some(match self {
QHolonomicError::NotQHypergeometric(_) => {
"write the summand with qbinomial(N, K), qpochhammer(u, d, v), powers of q with a \
degree-2 exponent in n and k, and rational functions of q, q**n and q**k; a bare \
n or k outside an exponent is not q-hypergeometric"
}
QHolonomicError::SearchExhausted(_) => {
"raise max_order and/or max_degree; if the sum genuinely satisfies no such \
q-recurrence, q-Zeilberger does not apply"
}
QHolonomicError::CertificateVerificationFailed(_) => {
"internal: report the term as a minimal failing example"
}
QHolonomicError::InvalidInput(_) => {
"q, n and k must be three distinct symbols; max_order and max_degree must be at \
least 1; a q-Pochhammer base step must be at least 1"
}
QHolonomicError::Unsupported(_) => {
"the term is q-hypergeometric in shape but its shift quotient is not a rational \
function of q**n and q**k — e.g. (q; q**2)_k shifted in k. No algorithm in this \
family applies; close the branch"
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QBoundaryStatus {
Vanishes {
n_min: i64,
support: Option<(String, String)>,
},
Unknown {
reason: String,
},
}
impl QBoundaryStatus {
pub fn tag(&self) -> &'static str {
match self {
QBoundaryStatus::Vanishes { .. } => "vanishes",
QBoundaryStatus::Unknown { .. } => "unknown",
}
}
pub fn implies_sum_recurrence(&self) -> bool {
matches!(self, QBoundaryStatus::Vanishes { .. })
}
pub fn side_conditions(&self) -> Vec<String> {
match self {
QBoundaryStatus::Vanishes { n_min, support } => {
let mut out = vec![
format!(
"the summand was proved to have finite support in k and to be finite at \
every integer k, so the homogeneous recurrence sum_i a_i(q**n)*S(n+i) = 0 \
holds for S(n) = sum over all integer k of F(n,k), for every integer \
n >= {n_min} at which the coefficients a_i(q**n) are defined"
),
"q is treated as transcendental: this is an identity in Q(q) and does not by \
itself license specialising q to a root of unity"
.to_string(),
];
if let Some((lo, hi)) = support {
out.push(format!(
"the sum over all integer k is the finite sum over {lo} <= k <= {hi}, \
where the summand was proved to vanish outside that window"
));
}
out
}
QBoundaryStatus::Unknown { reason } => vec![
format!(
"no recurrence for the sum follows from this certificate: {reason}. The \
verified statement is the telescoping identity in k for the summand, and \
nothing more"
),
"q is treated as transcendental: every identity here is an identity in Q(q)"
.to_string(),
],
}
}
}
pub fn q_boundary_status(f: &QProperTerm, order: usize, n_min: i64) -> QBoundaryStatus {
let mut support: Option<(String, String)> = None;
for i in 0..=order as i64 {
let s = f.support(i, n_min);
if !s.finite {
return QBoundaryStatus::Unknown {
reason: format!(
"at the n-shift {i}, {}",
nonempty(
&s.reason,
"the summand could not be proved finite at every integer k"
)
),
};
}
if !s.bounded_above || !s.bounded_below {
return QBoundaryStatus::Unknown {
reason: format!(
"at the n-shift {i}, {}",
nonempty(
&s.reason,
"the summand's support in k could not be bounded on both sides"
)
),
};
}
if i == 0 {
if let (Some(lo), Some(hi)) = (&s.lo, &s.hi) {
support = Some((format_bound(lo), format_bound(hi)));
}
}
}
QBoundaryStatus::Vanishes { n_min, support }
}
fn nonempty<'a>(s: &'a str, fallback: &'a str) -> &'a str {
if s.is_empty() {
fallback
} else {
s
}
}
fn format_bound(b: &term::Rational2) -> String {
let (a, c) = (b.a.clone(), b.b.clone());
if a == 0 {
return format!("{c}");
}
let head = if a == 1 {
"n".to_string()
} else if a == -1 {
"-n".to_string()
} else {
format!("{a}*n")
};
match c.cmp0() {
std::cmp::Ordering::Equal => head,
std::cmp::Ordering::Greater => format!("{head} + {c}"),
std::cmp::Ordering::Less => format!("{head} - {}", -c),
}
}
#[derive(Debug, Clone)]
pub struct QCertificate {
pub report: QZeilbergerReport,
pub boundary: QBoundaryStatus,
pub term: QProperTerm,
}
pub fn q_zeilberger(
term: ExprId,
q: ExprId,
n: ExprId,
k: ExprId,
pool: &ExprPool,
opts: &QZeilbergerOpts,
) -> Result<DerivedExpr<QCertificate>, QHolonomicError> {
if q == n || q == k || n == k {
return Err(QHolonomicError::InvalidInput(
"q, the outer index n and the summation index k must be three distinct symbols".into(),
));
}
let f = QProperTerm::parse(term, q, n, k, pool)?;
let report = q_zeilberger_on_term(&f, q, n, k, pool, opts)?;
let boundary = q_boundary_status(&f, report.result.order, opts.n_min);
let mut log = DerivationLog::new();
log.push(RewriteStep::simple(
"q_zeilberger_certificate",
term,
report.result.certificate,
));
Ok(DerivedExpr::with_log(
QCertificate {
report,
boundary,
term: f,
},
log,
))
}
const MAX_EVAL_SPAN: i64 = 4096;
impl QProperTerm {
pub fn value_at(&self, n0: i64, k0: i64) -> Option<Rn> {
let mut acc = field::raty_at(&self.rat, n0, k0)?;
acc = rn_mul(&acc, &qq_pow_of(&self.z, k0)?);
acc = rn_mul(&acc, &qq_pow_of(&self.w, n0)?);
let (rn0, rk0) = (Rational::from(n0), Rational::from(k0));
let e = self.quad.a_kk.clone() * rk0.clone() * rk0.clone()
+ self.quad.b_nk.clone() * rn0.clone() * rk0.clone()
+ self.quad.c_nn.clone() * rn0.clone() * rn0.clone()
+ self.quad.d_k.clone() * rk0.clone()
+ self.quad.e_n.clone() * rn0
+ self.quad.konst.clone();
if *e.denom() != 1 {
return None;
}
acc = rn_mul(&acc, &field::qq_pow(e.numer().to_i64()?));
for f in &self.poch {
let u = f.u.cn.checked_mul(n0)? + f.u.ck.checked_mul(k0)? + f.u.c0;
let v = f.v.cn.checked_mul(n0)? + f.v.ck.checked_mul(k0)? + f.v.c0;
match poch_value(u, f.d, v)? {
PochValue::Finite(p) => {
if rn_is_zero(&p) {
if f.e > 0 {
return Some(crate::holonomic::qfield::rn_zero());
}
return None; }
acc = rn_mul(&acc, &qq_pow_of(&p, f.e as i64)?);
}
PochValue::Infinite => {
if f.e > 0 {
return None;
}
return Some(crate::holonomic::qfield::rn_zero());
}
}
}
Some(acc)
}
pub fn window_at(&self, n0: i64, n_min: i64) -> Result<(i64, i64), QHolonomicError> {
let s = self.support(0, n_min);
if !s.finite || !s.bounded_above || !s.bounded_below {
return Err(QHolonomicError::Unsupported(format!(
"the summand's support in k was not established, so its sum is not a finite sum \
this module can evaluate: {}",
s.reason
)));
}
let (Some(lo), Some(hi)) = (&s.lo, &s.hi) else {
return Err(QHolonomicError::Unsupported(
"the support window is not a single affine bound on each side".into(),
));
};
let lo_v = ceil_at(lo, n0);
let hi_v = floor_at(hi, n0);
if hi_v - lo_v > MAX_EVAL_SPAN {
return Err(QHolonomicError::Unsupported(format!(
"the support window at n = {n0} spans {} terms (limit {MAX_EVAL_SPAN})",
hi_v - lo_v
)));
}
Ok((lo_v, hi_v))
}
pub fn sum_at(&self, n0: i64, n_min: i64) -> Result<Rn, QHolonomicError> {
let (lo_v, hi_v) = self.window_at(n0, n_min)?;
let mut acc = crate::holonomic::qfield::rn_zero();
for k0 in lo_v..=hi_v {
let v = self.value_at(n0, k0).ok_or_else(|| {
QHolonomicError::Unsupported(format!(
"the summand is not finite at (n, k) = ({n0}, {k0})"
))
})?;
acc = rn_add(&acc, &v);
}
Ok(acc)
}
}
fn ceil_at(b: &term::Rational2, n0: i64) -> i64 {
let v = b.a.clone() * Rational::from(n0) + b.b.clone();
let num = v.numer().clone();
let den = v.denom().clone();
let (q, r) = num.div_rem_floor(den);
let mut out = q.to_i64().unwrap_or(0);
if r != 0 {
out += 1;
}
out
}
fn floor_at(b: &term::Rational2, n0: i64) -> i64 {
let v = b.a.clone() * Rational::from(n0) + b.b.clone();
let num = v.numer().clone();
let den = v.denom().clone();
num.div_rem_floor(den).0.to_i64().unwrap_or(0)
}
enum PochValue {
Finite(Rn),
Infinite,
}
fn poch_value(u: i64, d: i64, v: i64) -> Option<PochValue> {
if v == 0 {
return Some(PochValue::Finite(rn_one()));
}
if v.abs() > MAX_EVAL_SPAN {
return None;
}
let mut prod = rn_one();
if v > 0 {
for t in 0..v {
let e = u.checked_add(d.checked_mul(t)?)?;
if e == 0 {
return Some(PochValue::Finite(crate::holonomic::qfield::rn_zero()));
}
prod = rn_mul(&prod, &one_minus_q_pow(e));
}
Some(PochValue::Finite(prod))
} else {
for t in 1..=(-v) {
let e = u.checked_sub(d.checked_mul(t)?)?;
if e == 0 {
return Some(PochValue::Infinite);
}
prod = rn_mul(&prod, &one_minus_q_pow(e));
}
Some(PochValue::Finite(crate::holonomic::qfield::rn_inv(&prod)?))
}
}
fn one_minus_q_pow(e: i64) -> Rn {
crate::holonomic::qfield::rn_sub(&rn_one(), &field::qq_pow(e))
}
fn qq_pow_of(base: &Rn, e: i64) -> Option<Rn> {
if e == 0 {
return Some(rn_one());
}
if rn_is_zero(base) || e.unsigned_abs() > 4096 {
return None;
}
let b = if e < 0 {
crate::holonomic::qfield::rn_inv(base)?
} else {
base.clone()
};
let mut acc = rn_one();
for _ in 0..e.unsigned_abs() {
acc = rn_mul(&acc, &b);
}
Some(acc)
}
#[cfg(test)]
mod tests {
use super::field::RatX;
use super::*;
use crate::errors::AlkahestError;
use crate::holonomic::qfield::{rn_eq, rn_inv, rn_sub, rn_zero};
use crate::kernel::Domain;
fn syms(pool: &ExprPool) -> (ExprId, ExprId, ExprId) {
(
pool.symbol("q", Domain::Real),
pool.symbol("n", Domain::Real),
pool.symbol("k", Domain::Real),
)
}
fn qbinom(pool: &ExprPool, top: ExprId, bot: ExprId) -> ExprId {
pool.func("qbinomial", vec![top, bot])
}
fn q_poch_int(m: i64) -> Rn {
let mut acc = rn_one();
for t in 1..=m {
acc = rn_mul(&acc, &rn_sub(&rn_one(), &field::qq_pow(t)));
}
acc
}
fn q_binom_int(nn: i64, kk: i64) -> Rn {
if kk < 0 || kk > nn {
return rn_zero();
}
let den = rn_mul(&q_poch_int(kk), &q_poch_int(nn - kk));
rn_mul(&q_poch_int(nn), &rn_inv(&den).expect("nonzero"))
}
fn assert_annihilates(cert: &QCertificate, upto: i64) {
let order = cert.report.result.order as i64;
let s: Vec<Rn> = (0..=(upto + order))
.map(|m| cert.term.sum_at(m, 0).expect("the sum is finite"))
.collect();
for n0 in 0..=upto {
let mut acc = rn_zero();
for (i, a) in cert.report.result.coeffs_x.iter().enumerate() {
let ai = field::polyx_at_qn(a, n0);
acc = rn_add(&acc, &rn_mul(&ai, &s[(n0 + i as i64) as usize]));
}
assert!(
rn_is_zero(&acc),
"the recurrence must annihilate the exact q-series sum at n = {n0}"
);
}
}
#[test]
fn q_vandermonde_square_sum() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let b = qbinom(&pool, n, k);
let f = pool.mul(vec![b, b, pool.pow(q, pool.mul(vec![k, k]))]);
let start = std::time::Instant::now();
let cert = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect("q-Zeilberger must decide the q-Vandermonde square sum")
.value;
println!(
"q-Vandermonde: order {} in {:?} ({} probes)",
cert.report.result.order,
start.elapsed(),
cert.report.probes
);
assert_eq!(cert.report.result.order, 1);
assert_eq!(cert.boundary.tag(), "vanishes");
assert!(cert.boundary.implies_sum_recurrence());
println!(
" a_0 = {}\n a_1 = {}\n R = {}",
pool.display(cert.report.result.coeffs[0]),
pool.display(cert.report.result.coeffs[1]),
pool.display(cert.report.result.certificate)
);
let x = field::ratx_x_pow(1);
let qx = |e: i64, p: i64| {
let mut acc = RatX::from_rn(field::qq_pow(e));
for _ in 0..p {
acc = acc.mul(&x);
}
RatX::one().sub(&acc)
};
let want = qx(1, 2)
.mul(&qx(2, 2))
.div(&qx(1, 1).mul(&qx(1, 1)))
.expect("nonzero")
.neg();
let got = RatX::from_poly(cert.report.result.coeffs_x[0].clone())
.div(&RatX::from_poly(cert.report.result.coeffs_x[1].clone()))
.expect("the leading coefficient is nonzero");
assert!(
got.eq_ratk(&want),
"expected a_0/a_1 = -(1-q*x^2)(1-q^2*x^2)/(1-q*x)^2 with x = q^n"
);
for n0 in 0..6 {
let s = cert.term.sum_at(n0, 0).expect("finite sum");
assert!(
rn_eq(&s, &q_binom_int(2 * n0, n0)),
"sum_{{k}} [n;k]^2 q^{{k^2}} must be [2n;n]_q at n = {n0}"
);
}
assert_annihilates(&cert, 5);
}
#[test]
fn galois_numbers_order_two() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let f = qbinom(&pool, n, k);
let cert = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect("the Galois-number sum must be decided")
.value;
assert_eq!(cert.report.result.order, 2);
assert_eq!(cert.boundary.tag(), "vanishes");
for n0 in 0..5 {
let s = cert.term.sum_at(n0, 0).expect("finite sum");
let mut want = rn_zero();
for j in 0..=n0 {
want = rn_add(&want, &q_binom_int(n0, j));
}
assert!(rn_eq(&s, &want), "G_{n0} must be the sum of its row");
}
assert_annihilates(&cert, 4);
}
#[test]
fn half_integral_quadratic_exponent_is_accepted() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let half = pool.rational(1, 2);
let kk1 = pool.mul(vec![half, k, pool.add(vec![k, pool.integer(-1_i32)])]);
let sign = pool.pow(pool.integer(-1_i32), k);
let f = pool.mul(vec![sign, pool.pow(q, kk1), qbinom(&pool, n, k)]);
let cert = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect("the alternating q-binomial sum must be decided")
.value;
assert_eq!(cert.boundary.tag(), "vanishes");
for n0 in 1..5 {
let s = cert.term.sum_at(n0, 0).expect("finite sum");
assert!(
rn_is_zero(&s),
"the alternating sum must vanish at n = {n0}"
);
}
assert_annihilates(&cert, 4);
}
#[test]
fn the_certificate_really_does_have_a_pole_at_the_boundary() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let b = qbinom(&pool, n, k);
let f = pool.mul(vec![b, b, pool.pow(q, pool.mul(vec![k, k]))]);
let cert = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect("certificate")
.value;
let den = &cert.report.result.certificate_xy.den;
for n0 in 1..5 {
let at_boundary = field::polyy_at(den, n0, n0 + 1).expect("the denominator evaluates");
assert!(
rn_is_zero(&at_boundary),
"R must be singular at k = n+1 (n = {n0}); if it is not, the summand's zero \
there is unmatched and the boundary argument is being tested against the \
wrong premise"
);
assert!(rn_is_zero(&cert.term.value_at(n0, n0 + 1).expect("finite")));
}
}
#[test]
fn refuses_non_q_hypergeometric_input() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let bad = pool.func("sin", vec![pool.mul(vec![n, k])]);
let err = q_zeilberger(bad, q, n, k, &pool, &QZeilbergerOpts::default())
.expect_err("sin(nk) is not q-hypergeometric");
assert!(matches!(err, QHolonomicError::NotQHypergeometric(_)));
assert_eq!(err.code(), "E-HOLO-020");
}
#[test]
fn refuses_a_classical_hypergeometric_term() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let f = pool.mul(vec![
qbinom(&pool, n, k),
pool.pow(pool.add(vec![k, pool.integer(1_i32)]), pool.integer(-1_i32)),
]);
let err = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect_err("1/(k+1) is not a rational function of q^k");
assert_eq!(err.code(), "E-HOLO-020");
}
#[test]
fn refuses_a_base_incompatible_shift() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let f = pool.func("qpochhammer", vec![k, pool.integer(2_i32), n]);
let err = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect_err("a base-incompatible shift must be refused");
assert!(matches!(err, QHolonomicError::Unsupported(_)));
assert_eq!(err.code(), "E-HOLO-024");
}
#[test]
fn refuses_coincident_symbols() {
let pool = ExprPool::new();
let (q, n, _k) = syms(&pool);
let err = q_zeilberger(n, q, n, n, &pool, &QZeilbergerOpts::default())
.expect_err("n == k must be refused");
assert_eq!(err.code(), "E-HOLO-023");
}
#[test]
fn unbounded_support_yields_no_claim_about_the_sum() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let f = pool.pow(
pool.func(
"qpochhammer",
vec![
pool.integer(1_i32),
pool.integer(1_i32),
pool.add(vec![n, pool.mul(vec![k, pool.integer(-1_i32)])]),
],
),
pool.integer(-1_i32),
);
let cert = q_zeilberger(f, q, n, k, &pool, &QZeilbergerOpts::default())
.expect("the telescoping identity itself is fine")
.value;
assert_eq!(cert.boundary.tag(), "unknown");
assert!(!cert.boundary.implies_sum_recurrence());
assert!(cert
.boundary
.side_conditions()
.iter()
.any(|s| s.contains("no recurrence for the sum follows")));
}
#[test]
fn support_of_the_q_binomial_square_is_zero_to_n() {
let pool = ExprPool::new();
let (q, n, k) = syms(&pool);
let b = qbinom(&pool, n, k);
let f = pool.mul(vec![b, b, pool.pow(q, pool.mul(vec![k, k]))]);
let term = QProperTerm::parse(f, q, n, k, &pool).expect("in class");
let s = term.support(0, 0);
assert!(s.finite && s.bounded_above && s.bounded_below);
let lo = s.lo.expect("a lower bound");
let hi = s.hi.expect("an upper bound");
assert_eq!(
(lo.a.clone(), lo.b.clone()),
(Rational::new(), Rational::new())
);
assert_eq!(
(hi.a.clone(), hi.b.clone()),
(Rational::from(1), Rational::new())
);
for n0 in 0..4 {
assert!(rn_is_zero(&term.value_at(n0, -1).expect("finite")));
assert!(rn_is_zero(&term.value_at(n0, n0 + 1).expect("finite")));
}
}
}