use super::field::{
clear_denominators_x, clear_field_refusal, qq_pow, ratx_terms, take_field_refusal,
FieldRefusal, PolyX, PolyY, RatX, RatY, MAX_FIELD_ELEMENT_TERMS,
};
use super::term::QProperTerm;
use super::QHolonomicError;
use crate::holonomic::hyperterm::rn_to_expr;
use crate::holonomic::qfield::{
clear_denominators, clear_gcd_stop, enter_gcd_work_scope, rn_div, rn_is_zero, rn_poly,
take_gcd_stop, GcdStop, Rn,
};
use crate::holonomic::zeilberger::OrderSearch;
use crate::kernel::{ExprId, ExprPool};
const MAX_SYSTEM_CELLS: usize = 4_000;
const MAX_CUMULATIVE_SYSTEM_CELLS: usize = 20_000;
enum Probe {
Solved(Vec<RatX>, Vec<RatX>),
NoSolution,
Refused,
}
#[derive(Debug, Clone, Copy)]
pub struct QZeilbergerOpts {
pub max_order: usize,
pub max_degree: usize,
pub search: OrderSearch,
pub n_min: i64,
}
impl Default for QZeilbergerOpts {
fn default() -> Self {
QZeilbergerOpts {
max_order: 3,
max_degree: 6,
search: OrderSearch::CostOrdered,
n_min: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct QZeilbergerResult {
pub order: usize,
pub coeffs: Vec<ExprId>,
pub certificate: ExprId,
pub coeffs_x: Vec<PolyX>,
pub certificate_xy: RatY,
}
#[derive(Debug, Clone)]
pub struct QZeilbergerReport {
pub result: QZeilbergerResult,
pub order_is_minimal: bool,
pub probes: usize,
}
fn y_mono(j: usize) -> PolyY {
let mut coeffs = vec![RatX::zero(); j + 1];
coeffs[j] = RatX::one();
PolyY::from_coeffs(coeffs)
}
fn q_gosper_normal_form(mut p: PolyY, mut r: PolyY) -> Option<(PolyY, PolyY, PolyY)> {
if p.is_zero() {
return Some((PolyY::zero(), PolyY::one(), PolyY::one()));
}
if r.is_zero() {
return None;
}
let lc_p = p.leading_coeff();
let lc_r = r.leading_coeff();
let z_scale = lc_p.div(&lc_r)?;
p = p.scale(&lc_p.inv()?);
r = r.scale(&lc_r.inv()?);
let mut a = p;
let mut b = r;
let mut c = PolyY::one();
let bound = (a.degree().max(0) + b.degree().max(0)).max(1) as usize + 16;
loop {
let mut found = false;
for h in 0..=bound {
let bshift = b.qshift_y(h as i64);
let d = PolyY::gcd(&a, &bshift);
if d.is_zero() || d.degree() == 0 {
continue;
}
let Some(an) = PolyY::exact_div(&a, &d) else {
continue;
};
let Some(bn) = PolyY::exact_div(&b, &d.qshift_y(-(h as i64))) else {
continue;
};
a = an;
b = bn;
let mut prod = PolyY::one();
for j in 1..=h {
prod = prod.mul(&d.qshift_y(-(j as i64)));
}
c = c.mul(&prod);
found = true;
break;
}
if !found {
break;
}
}
a = a.scale(&z_scale);
Some((a, b, c))
}
fn field_solve(
mut mat: Vec<Vec<RatX>>,
mut rhs: Vec<RatX>,
) -> Result<Option<Vec<RatX>>, QHolonomicError> {
let nrows = mat.len();
if nrows == 0 {
return Ok(Some(vec![]));
}
let ncols = mat[0].len();
let mut row = 0;
for col in 0..ncols {
if row >= nrows {
break;
}
checkpoint()?;
let Some(pr) = (row..nrows).find(|&r| !mat[r][col].is_zero()) else {
continue;
};
mat.swap(row, pr);
rhs.swap(row, pr);
let Some(inv) = mat[row][col].inv() else {
return Ok(None);
};
for entry in mat[row].iter_mut().skip(col) {
*entry = entry.mul(&inv);
}
rhs[row] = rhs[row].mul(&inv);
let pivot_row = mat[row].clone();
let pivot_rhs = rhs[row].clone();
for r in 0..nrows {
if r == row {
continue;
}
let v = mat[r][col].clone();
if v.is_zero() {
continue;
}
checkpoint()?;
for (entry, pivot) in mat[r].iter_mut().zip(pivot_row.iter()).skip(col) {
*entry = entry.sub(&pivot.mul(&v));
if ratx_terms(entry) > MAX_FIELD_ELEMENT_TERMS {
return Err(size_ceiling_error(ratx_terms(entry)));
}
}
rhs[r] = rhs[r].sub(&pivot_rhs.mul(&v));
}
row += 1;
}
for (r, mrow) in mat.iter().enumerate() {
if mrow.iter().all(RatX::is_zero) && !rhs[r].is_zero() {
return Ok(None);
}
}
let mut sol = vec![RatX::zero(); ncols];
for r in (0..nrows).rev() {
if let Some(j) = mat[r].iter().position(|e| !e.is_zero()) {
checkpoint()?;
let mut sum = rhs[r].clone();
for cidx in (j + 1)..ncols {
sum = sum.sub(&mat[r][cidx].mul(&sol[cidx]));
}
let Some(q) = sum.div(&mat[r][j]) else {
return Ok(None);
};
sol[j] = q;
}
}
Ok(Some(sol))
}
fn trip_to_error(trip: crate::budget::BudgetTrip) -> QHolonomicError {
use crate::errors::AlkahestError;
crate::budget::record_trip(trip);
QHolonomicError::SearchExhausted(format!(
"the q-Zeilberger search was stopped before it finished, and no certificate was found \
up to that point — this is NOT a statement that none exists: {} [{}]",
trip,
trip.code()
))
}
fn checkpoint() -> Result<(), QHolonomicError> {
crate::budget::check_all().map_err(trip_to_error)
}
fn size_ceiling_error(terms: usize) -> QHolonomicError {
QHolonomicError::SearchExhausted(format!(
"{SIZE_CEILING_TAG}: a coefficient of the linear system reached {terms} rational \
numbers, past this module's MAX_FIELD_ELEMENT_TERMS = {MAX_FIELD_ELEMENT_TERMS}"
))
}
const SIZE_CEILING_TAG: &str = "q-Zeilberger field-element ceiling";
fn is_size_ceiling(e: &QHolonomicError) -> bool {
matches!(e, QHolonomicError::SearchExhausted(m) if m.starts_with(SIZE_CEILING_TAG))
}
fn try_solve(
aa: &PolyY,
b_eq: &PolyY,
c_ci: &[PolyY],
order: usize,
d: usize,
cumulative_cells: &mut usize,
) -> Result<Probe, QHolonomicError> {
let mut bx: Vec<PolyY> = Vec::with_capacity(d + 1);
for j in 0..=d {
let yj = y_mono(j);
let shifted = yj.scale(&RatX::from_rn(qq_pow(j as i64)));
bx.push(aa.mul(&shifted).sub(&b_eq.mul(&yj)));
}
let mut max_deg = 0i32;
for p in bx.iter().chain(c_ci.iter()) {
max_deg = max_deg.max(p.degree());
}
let n_eq = (max_deg.max(0) as usize) + 1;
let n_var = (d + 1) + order;
let cells = n_eq.saturating_mul(n_var);
if cells > MAX_SYSTEM_CELLS {
return Ok(Probe::Refused);
}
if cumulative_cells.saturating_add(cells) > MAX_CUMULATIVE_SYSTEM_CELLS {
return Ok(Probe::Refused);
}
*cumulative_cells += cells;
let mut mat = vec![vec![RatX::zero(); n_var]; n_eq];
let mut rhs = vec![RatX::zero(); n_eq];
for (m, row) in mat.iter_mut().enumerate() {
for (j, bxj) in bx.iter().enumerate() {
row[j] = bxj.coeff(m);
}
for i in 0..order {
row[(d + 1) + i] = c_ci[i].coeff(m).neg();
}
rhs[m] = c_ci[order].coeff(m);
}
let sol = match field_solve(mat, rhs) {
Ok(Some(sol)) => sol,
Ok(None) => return Ok(Probe::NoSolution),
Err(e) if is_size_ceiling(&e) => return Ok(Probe::Refused),
Err(e) => return Err(e),
};
Ok(Probe::Solved(sol[..=d].to_vec(), sol[(d + 1)..].to_vec()))
}
fn primitive_family(family: &[PolyX]) -> Option<(Vec<PolyX>, Rn)> {
let flat: Vec<Rn> = family
.iter()
.flat_map(|p| p.coeffs.iter().cloned())
.collect();
let cleared = clear_denominators(&flat);
let idx = flat.iter().position(|c| !rn_is_zero(c))?;
let scale = rn_div(&rn_poly(cleared[idx].clone()), &flat[idx])?;
let mut out = Vec::with_capacity(family.len());
let mut at = 0usize;
for p in family {
let len = p.coeffs.len();
out.push(PolyX::from_coeffs(
cleared[at..at + len].iter().cloned().map(rn_poly).collect(),
));
at += len;
}
Some((out, scale))
}
struct OrderState {
c: Vec<RatY>,
dden: PolyY,
aa: PolyY,
b_eq: PolyY,
cc: PolyY,
c_ci: Vec<PolyY>,
}
fn order_state(
f: &QProperTerm,
p: &RatY,
order: usize,
) -> Result<Option<OrderState>, QHolonomicError> {
let c: Vec<RatY> = (0..=order as i64)
.map(|i| f.ratio_n(i))
.collect::<Result<_, _>>()?;
let mut dden = PolyY::one();
for ci in &c {
dden = PolyY::lcm(&dden, &ci.den);
}
if dden.is_zero() {
return Ok(None);
}
let ci_polys: Option<Vec<PolyY>> = c
.iter()
.map(|ci| PolyY::exact_div(&dden.mul(&ci.num), &ci.den))
.collect();
let Some(ci_polys) = ci_polys else {
return Ok(None);
};
let rho_num = p.num.mul(&dden);
let rho_den = p.den.mul(&dden.qshift_y(1));
let Some((aa, bb, cc)) = q_gosper_normal_form(rho_num, rho_den) else {
return Ok(None);
};
let b_eq = bb.qshift_y(-1);
let c_ci: Vec<PolyY> = ci_polys.iter().map(|q| cc.mul(q)).collect();
Ok(Some(OrderState {
c,
dden,
aa,
b_eq,
cc,
c_ci,
}))
}
const ORDER_COST_IN_DEGREE_STEPS: usize = 3;
fn search_plan(max_order: usize, max_degree: usize, search: OrderSearch) -> Vec<(usize, usize)> {
let mut plan = Vec::with_capacity(max_order * (max_degree + 1));
match search {
OrderSearch::MinimalOrder => {
for order in 1..=max_order {
for d in 0..=max_degree {
plan.push((order, d));
}
}
}
OrderSearch::CostOrdered => {
let max_budget = ORDER_COST_IN_DEGREE_STEPS * (max_order - 1) + max_degree;
for budget in 0..=max_budget {
for order in 1..=max_order {
let spent = ORDER_COST_IN_DEGREE_STEPS * (order - 1);
if let Some(d) = budget.checked_sub(spent) {
if d <= max_degree {
plan.push((order, d));
}
}
}
}
}
}
plan
}
pub fn q_zeilberger_on_term(
f: &QProperTerm,
q: ExprId,
n: ExprId,
k: ExprId,
pool: &ExprPool,
opts: &QZeilbergerOpts,
) -> Result<QZeilbergerReport, QHolonomicError> {
if opts.max_order == 0 || opts.max_degree == 0 {
return Err(QHolonomicError::InvalidInput(
"max_order and max_degree must both be at least 1".into(),
));
}
crate::budget::clear_trip();
let _gcd_scope = enter_gcd_work_scope();
let p = f.ratio_k()?;
let mut states: Vec<Option<OrderState>> = Vec::with_capacity(opts.max_order);
let mut degrees_failed = vec![0usize; opts.max_order];
let mut cumulative_cells: usize = 0;
let mut refused_by_ceiling = false;
macro_rules! bail_on_field_refusal {
() => {
match take_field_refusal() {
None => {}
Some(FieldRefusal::Budget(t)) => return Err(trip_to_error(t)),
Some(FieldRefusal::SizeCeiling(_)) => {
refused_by_ceiling = true;
continue;
}
}
match take_gcd_stop() {
None => {}
Some(GcdStop::Budget(t)) => return Err(trip_to_error(t)),
Some(GcdStop::Size(_)) | Some(GcdStop::Work(_)) => {
refused_by_ceiling = true;
continue;
}
}
};
}
for (order, d) in search_plan(opts.max_order, opts.max_degree, opts.search) {
degrees_failed[order - 1] += 1;
checkpoint()?;
clear_field_refusal();
clear_gcd_stop();
while states.len() < order {
states.push(order_state(f, &p, states.len() + 1)?);
}
let Some(state) = &states[order - 1] else {
continue;
};
let probe = try_solve(
&state.aa,
&state.b_eq,
&state.c_ci,
order,
d,
&mut cumulative_cells,
)?;
bail_on_field_refusal!();
let (x_coeffs, lam_below) = match probe {
Probe::Solved(x, lam) => (x, lam),
Probe::NoSolution => continue,
Probe::Refused => {
refused_by_ceiling = true;
continue;
}
};
let mut lam_full = lam_below;
lam_full.push(RatX::one());
let x_poly = PolyY::from_coeffs(x_coeffs);
let r_pre = RatY {
num: state.b_eq.mul(&x_poly),
den: state.cc.mul(&state.dden),
}
.normalize();
bail_on_field_refusal!();
let (a_int, scale) = clear_denominators_x(&lam_full);
if a_int.iter().all(PolyX::is_zero) || a_int[order].is_zero() {
continue;
}
let mut r_final = RatY {
num: r_pre.num.scale(&scale),
den: r_pre.den.clone(),
}
.normalize();
bail_on_field_refusal!();
let a_int = primitive_family(&a_int)
.map(|(family, s)| {
r_final = r_final.mul(&RatY::from_ratx(RatX::from_rn(s)));
family
})
.unwrap_or(a_int);
let mut lhs = RatY::zero();
for (i, ci) in state.c.iter().enumerate() {
lhs = lhs.add(&RatY::from_ratx(RatX::from_poly(a_int[i].clone())).mul(ci));
}
bail_on_field_refusal!();
let rhs_check = r_final.qshift_y(1).mul(&p).sub(&r_final);
if !lhs.sub(&rhs_check).is_zero() {
continue;
}
bail_on_field_refusal!();
let simp = |e: ExprId| crate::simplify::simplify(e, pool).value;
let coeffs: Vec<ExprId> = a_int
.iter()
.map(|c| simp(polyx_to_expr(pool, q, n, c)))
.collect();
let certificate = simp(raty_to_expr(pool, q, n, k, &r_final));
let order_is_minimal = (1..order).all(|j| degrees_failed[j - 1] == opts.max_degree + 1);
return Ok(QZeilbergerReport {
result: QZeilbergerResult {
order,
coeffs,
certificate,
coeffs_x: a_int,
certificate_xy: r_final,
},
order_is_minimal,
probes: degrees_failed.iter().sum(),
});
}
let ceiling_note = if refused_by_ceiling {
format!(
" (at least one (order, degree) combination within these bounds was refused by this \
module's resource ceilings rather than attempted — MAX_SYSTEM_CELLS = \
{MAX_SYSTEM_CELLS} equations x unknowns for any single probe, \
MAX_CUMULATIVE_SYSTEM_CELLS = {MAX_CUMULATIVE_SYSTEM_CELLS} across the whole \
search, MAX_FIELD_ELEMENT_TERMS = {MAX_FIELD_ELEMENT_TERMS} rational numbers in \
any one coefficient of the linear system; so this is NOT a statement that no \
q-recurrence exists within these bounds)"
)
} else {
String::new()
};
Err(QHolonomicError::SearchExhausted(format!(
"no verified q-recurrence of order <= {} with certificate degree <= {} in q^k was \
found{ceiling_note}",
opts.max_order, opts.max_degree
)))
}
fn q_pow_var(pool: &ExprPool, q: ExprId, v: ExprId, e: usize) -> Option<ExprId> {
match e {
0 => None,
1 => Some(pool.pow(q, v)),
_ => Some(pool.pow(q, pool.mul(vec![pool.integer(e as i64), v]))),
}
}
fn qq_to_expr(pool: &ExprPool, q: ExprId, c: &Rn) -> ExprId {
rn_to_expr(pool, q, c)
}
pub fn polyx_to_expr(pool: &ExprPool, q: ExprId, n: ExprId, p: &PolyX) -> ExprId {
let mut terms = Vec::new();
for (deg, c) in p.coeffs.iter().enumerate() {
if rn_is_zero(c) {
continue;
}
let ce = qq_to_expr(pool, q, c);
terms.push(match q_pow_var(pool, q, n, deg) {
None => ce,
Some(xd) => pool.mul(vec![ce, xd]),
});
}
match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => pool.add(terms),
}
}
fn polyy_to_expr(pool: &ExprPool, q: ExprId, n: ExprId, k: ExprId, p: &PolyY) -> ExprId {
let mut terms = Vec::new();
for (deg, c) in p.coeffs.iter().enumerate() {
if c.is_zero() {
continue;
}
let ce = ratx_to_expr(pool, q, n, c);
terms.push(match q_pow_var(pool, q, k, deg) {
None => ce,
Some(yd) => pool.mul(vec![ce, yd]),
});
}
match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => pool.add(terms),
}
}
pub fn ratx_to_expr(pool: &ExprPool, q: ExprId, n: ExprId, r: &RatX) -> ExprId {
let num = polyx_to_expr(pool, q, n, &r.num);
if r.den.eq_poly(&PolyX::one()) {
return num;
}
let den = polyx_to_expr(pool, q, n, &r.den);
pool.mul(vec![num, pool.pow(den, pool.integer(-1_i32))])
}
pub fn raty_to_expr(pool: &ExprPool, q: ExprId, n: ExprId, k: ExprId, r: &RatY) -> ExprId {
let num = polyy_to_expr(pool, q, n, k, &r.num);
if r.den.eq_poly(&PolyY::one()) {
return num;
}
let den = polyy_to_expr(pool, q, n, k, &r.den);
pool.mul(vec![num, pool.pow(den, pool.integer(-1_i32))])
}