use rug::Rational;
use super::number_field::{KElem, KPoly, NumberField};
use super::poly_rde::{
degree, poly_add, poly_deriv, poly_mul, poly_one, poly_scale, poly_zero, trim, QPoly,
};
pub fn poly_sub(a: &QPoly, b: &QPoly) -> QPoly {
poly_add(a, &poly_scale(b, &Rational::from(-1)))
}
fn coeff(p: &QPoly, i: i64) -> Rational {
if i < 0 {
return Rational::from(0);
}
p.get(i as usize)
.cloned()
.unwrap_or_else(|| Rational::from(0))
}
pub fn poly_divrem(a: &QPoly, b: &QPoly) -> (QPoly, QPoly) {
let b = trim(b.clone());
let bd = degree(&b);
debug_assert!(bd >= 0, "poly_divrem: division by zero polynomial");
let lcb = b[bd as usize].clone();
let mut r = trim(a.clone());
let ad = degree(&r);
if ad < bd {
return (poly_zero(), r);
}
let mut q = vec![Rational::from(0); (ad - bd + 1) as usize];
loop {
let rd = degree(&r);
if rd < bd {
break;
}
let shift = (rd - bd) as usize;
let factor = r[rd as usize].clone() / lcb.clone();
q[shift] += factor.clone();
for (i, bc) in b.iter().enumerate() {
r[shift + i] -= factor.clone() * bc.clone();
}
r = trim(r);
if r.is_empty() {
break;
}
}
(trim(q), trim(r))
}
pub fn poly_monic(p: &QPoly) -> QPoly {
let p = trim(p.clone());
let d = degree(&p);
if d < 0 {
return p;
}
let lc = p[d as usize].clone();
poly_scale(&p, &(Rational::from(1) / lc))
}
pub fn poly_gcd(a: &QPoly, b: &QPoly) -> QPoly {
crate::flint::qgcd::qpoly_gcd_monic(a, b)
}
#[cfg(test)]
pub(crate) fn poly_gcd_euclid(a: &QPoly, b: &QPoly) -> QPoly {
let mut a = trim(a.clone());
let mut b = trim(b.clone());
while !b.is_empty() {
let (_, r) = poly_divrem(&a, &b);
a = b;
b = r;
}
poly_monic(&a)
}
pub(crate) fn poly_gcd_budgeted(a: &QPoly, b: &QPoly) -> Option<QPoly> {
crate::budget::check().ok()?;
Some(poly_gcd(a, b))
}
pub fn poly_div_exact(a: &QPoly, b: &QPoly) -> QPoly {
let (q, r) = poly_divrem(a, b);
debug_assert!(trim(r).is_empty(), "poly_div_exact: nonzero remainder");
q
}
pub fn poly_pow(p: &QPoly, n: u32) -> QPoly {
let mut acc = poly_one();
for _ in 0..n {
acc = poly_mul(&acc, p);
}
acc
}
const MAX_QRATIONAL_DEGREE: usize = 1 << 13;
fn poly_pow_bounded(p: &QPoly, n: u32) -> Option<QPoly> {
let deg = trim(p.clone()).len().saturating_sub(1);
if deg.max(1).saturating_mul(n as usize) > MAX_QRATIONAL_DEGREE {
return None;
}
let mut acc = poly_one();
for _ in 0..n {
crate::budget::check().ok()?;
acc = poly_mul(&acc, p);
}
Some(acc)
}
fn polys_equal(a: &QPoly, b: &QPoly) -> bool {
trim(a.clone()) == trim(b.clone())
}
fn solve_linear_system(
mut mat: Vec<Vec<Rational>>,
mut rhs: Vec<Rational>,
cols: usize,
) -> Option<Vec<Rational>> {
let rows = mat.len();
let mut pivot_row_of_col: Vec<Option<usize>> = vec![None; cols];
let mut row = 0usize;
for col in 0..cols {
if row >= rows {
break;
}
let Some(sel) = (row..rows).find(|&r| mat[r][col] != 0) else {
continue;
};
mat.swap(row, sel);
rhs.swap(row, sel);
let piv = mat[row][col].clone();
for cell in mat[row][col..cols].iter_mut() {
*cell /= piv.clone();
}
rhs[row] /= piv.clone();
let pivot_row = mat[row].clone();
let pivot_rhs = rhs[row].clone();
for r in 0..rows {
if r != row && mat[r][col] != 0 {
let factor = mat[r][col].clone();
for (cell, pv) in mat[r][col..cols]
.iter_mut()
.zip(pivot_row[col..cols].iter())
{
*cell -= factor.clone() * pv.clone();
}
rhs[r] -= factor.clone() * pivot_rhs.clone();
}
}
pivot_row_of_col[col] = Some(row);
row += 1;
}
for r in 0..rows {
if mat[r].iter().all(|v| *v == 0) && rhs[r] != 0 {
return None;
}
}
let mut x = vec![Rational::from(0); cols];
for (col, pr) in pivot_row_of_col.iter().enumerate() {
if let Some(pr) = pr {
x[col] = rhs[*pr].clone();
}
}
Some(x)
}
pub(crate) fn numerator_degree_bound(deg_b: i64, deg_c: i64, deg_e: i64, deg_f: i64) -> usize {
let poly_part = (deg_c - deg_b).max(0);
(deg_e.max(0) + poly_part.max(deg_f.max(0)) + 2).max(0) as usize
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RdeDecline {
MalformedInput,
ResonanceSearchTooLarge,
AnsatzTooLarge,
VerificationFailed,
AlgebraicBoundNotProved,
AlgebraicFieldNotProved,
}
impl core::fmt::Display for RdeDecline {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RdeDecline::MalformedInput => write!(f, "malformed input (zero denominator)"),
RdeDecline::ResonanceSearchTooLarge => write!(
f,
"the integer-residue search needed for the denominator bound exceeds \
the internal cap"
),
RdeDecline::AnsatzTooLarge => {
write!(f, "the RDE ansatz exceeds the internal size cap")
}
RdeDecline::VerificationFailed => write!(
f,
"the candidate solution failed the exact substitution check"
),
RdeDecline::AlgebraicBoundNotProved => write!(
f,
"no provably complete denominator/degree bound is available for \
this coupled equation over an algebraic extension"
),
RdeDecline::AlgebraicFieldNotProved => write!(
f,
"the minimal polynomial of the algebraic generator was not proved \
irreducible over ℚ(x)"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RdeOutcome {
Solved {
num: QPoly,
den: QPoly,
},
NoRationalSolution,
Declined(RdeDecline),
}
impl RdeOutcome {
pub fn solution(self) -> Option<(QPoly, QPoly)> {
match self {
RdeOutcome::Solved { num, den } => Some((num, den)),
_ => None,
}
}
pub fn proves_no_solution(&self) -> bool {
matches!(self, RdeOutcome::NoRationalSolution)
}
pub fn is_declined(&self) -> bool {
matches!(self, RdeOutcome::Declined(_))
}
}
const MAX_RESONANCE_SEARCH: i64 = 1024;
const MAX_RDE_UNKNOWNS: usize = 512;
pub fn solve_rational_rde_checked(f: &QPoly, c_num: &QPoly, c_den: &QPoly) -> RdeOutcome {
let c_num = trim(c_num.clone());
let c_den = trim(c_den.clone());
if c_num.is_empty() {
return RdeOutcome::Solved {
num: poly_zero(),
den: poly_one(),
};
}
if c_den.is_empty() {
return RdeOutcome::Declined(RdeDecline::MalformedInput);
}
let g = poly_gcd(&c_num, &c_den);
let big_c = poly_div_exact(&c_num, &g);
let b_raw = trim(poly_div_exact(&c_den, &g));
let bd = degree(&b_raw);
if bd < 0 {
return RdeOutcome::Declined(RdeDecline::MalformedInput);
}
let scale = Rational::from(1) / b_raw[bd as usize].clone();
let big_b = poly_scale(&b_raw, &scale);
let big_c = poly_scale(&big_c, &scale);
let bprime = poly_deriv(&big_b);
let e_poly = poly_gcd(&big_b, &bprime);
let g_poly = poly_div_exact(&big_b, &e_poly);
let eprime = poly_deriv(&e_poly);
let ge = poly_mul(&g_poly, &e_poly); let gep = poly_mul(&g_poly, &eprime); let gef = poly_mul(&ge, f); let target = poly_mul(&big_c, &e_poly);
let deg_b = degree(&big_b);
let deg_c = degree(&big_c);
let deg_e = degree(&e_poly).max(0);
let deg_f = degree(f).max(0);
let dbound = numerator_degree_bound(deg_b, deg_c, deg_e, deg_f);
let cols = dbound + 1; if cols > MAX_RDE_UNKNOWNS {
return RdeOutcome::Declined(RdeDecline::AnsatzTooLarge);
}
let max_deg = (degree(&gef) + dbound as i64)
.max(degree(&ge) + dbound as i64)
.max(degree(&gep) + dbound as i64)
.max(degree(&target))
.max(0) as usize;
let n_rows = max_deg + 1;
let mut mat = vec![vec![Rational::from(0); cols]; n_rows];
for (d, row) in mat.iter_mut().enumerate() {
let d = d as i64;
for (j, cell) in row.iter_mut().enumerate() {
let jj = j as i64;
let mut v = Rational::from(jj) * coeff(&ge, d - jj + 1);
v -= coeff(&gep, d - jj);
v += coeff(&gef, d - jj);
*cell = v;
}
}
let rhs: Vec<Rational> = (0..n_rows).map(|d| coeff(&target, d as i64)).collect();
let Some(solution) = solve_linear_system(mat, rhs, cols) else {
return RdeOutcome::NoRationalSolution;
};
let n_poly = trim(solution);
let np = poly_deriv(&n_poly);
let lhs = poly_mul(
&poly_add(
&poly_sub(&poly_mul(&np, &e_poly), &poly_mul(&n_poly, &eprime)),
&poly_mul(&poly_mul(f, &n_poly), &e_poly),
),
&big_b,
);
let rhs_check = poly_mul(&big_c, &poly_mul(&e_poly, &e_poly));
if !polys_equal(&lhs, &rhs_check) {
return RdeOutcome::Declined(RdeDecline::VerificationFailed);
}
if n_poly.is_empty() {
return RdeOutcome::Solved {
num: poly_zero(),
den: poly_one(),
};
}
let gve = poly_gcd(&n_poly, &e_poly);
let num = poly_div_exact(&n_poly, &gve);
let den = poly_monic(&poly_div_exact(&e_poly, &gve));
RdeOutcome::Solved { num, den }
}
pub fn solve_rational_rde(f: &QPoly, c_num: &QPoly, c_den: &QPoly) -> Option<(QPoly, QPoly)> {
solve_rational_rde_checked(f, c_num, c_den).solution()
}
fn poly_ext_gcd(a: &QPoly, b: &QPoly) -> (QPoly, QPoly, QPoly) {
let mut r0 = trim(a.clone());
let mut r1 = trim(b.clone());
let mut s0 = poly_one();
let mut s1 = poly_zero();
let mut t0 = poly_zero();
let mut t1 = poly_one();
while !r1.is_empty() {
let (q, r) = poly_divrem(&r0, &r1);
let s2 = poly_sub(&s0, &poly_mul(&q, &s1));
let t2 = poly_sub(&t0, &poly_mul(&q, &t1));
r0 = r1;
r1 = trim(r);
s0 = s1;
s1 = s2;
t0 = t1;
t1 = t2;
}
let d = degree(&r0);
if d < 0 {
return (poly_zero(), poly_zero(), poly_zero());
}
let inv = Rational::from(1) / r0[d as usize].clone();
(
poly_scale(&r0, &inv),
poly_scale(&s0, &inv),
poly_scale(&t0, &inv),
)
}
fn multiplicity_one_part(d: &QPoly) -> QPoly {
let g = poly_gcd(d, &poly_deriv(d)); let d_star = poly_div_exact(d, &g); poly_div_exact(&d_star, &poly_gcd(&d_star, &g)) }
fn residue_magnitude_bound(beta: &QPoly, d1: &QPoly) -> Rational {
let dd = degree(d1).max(0) as usize;
let mut rows = vec![Rational::from(0); dd];
let mut cols = vec![Rational::from(0); dd];
let mut col = poly_divrem(beta, d1).1;
for c in cols.iter_mut() {
for (i, cell) in col.iter().enumerate() {
if i < dd {
let a = cell.clone().abs();
rows[i] += a.clone();
*c += a;
}
}
let mut shifted = vec![Rational::from(0)];
shifted.extend(col.iter().cloned());
col = poly_divrem(&trim(shifted), d1).1;
}
let max = |v: Vec<Rational>| {
v.into_iter()
.fold(Rational::from(0), |m, r| if r > m { r } else { m })
};
let (r, c) = (max(rows), max(cols));
if r < c {
r
} else {
c
}
}
fn resonant_denominator(big_a: &QPoly, big_b: &QPoly) -> Result<QPoly, RdeDecline> {
let d1 = multiplicity_one_part(big_b);
if degree(&d1) < 1 {
return Ok(poly_one()); }
let w = poly_div_exact(big_b, &d1);
let d1p = poly_deriv(&d1);
let denom = poly_mul(&d1p, &w);
let (g, s, _) = poly_ext_gcd(&denom, &d1);
if degree(&g) != 0 {
return Err(RdeDecline::VerificationFailed);
}
let beta = poly_divrem(&poly_mul(big_a, &s), &d1).1;
let bound = residue_magnitude_bound(&beta, &d1);
let k_max = bound.floor().numer().to_i64().unwrap_or(i64::MAX);
if k_max > MAX_RESONANCE_SEARCH {
return Err(RdeDecline::ResonanceSearchTooLarge);
}
let mut q = poly_one();
for k in 1..=k_max.max(0) {
let probe = poly_sub(big_a, &poly_scale(&denom, &Rational::from(k)));
let gk = poly_gcd(&d1, &probe);
if degree(&gk) >= 1 {
q = poly_mul(&q, &poly_pow(&gk, k as u32));
}
}
Ok(poly_monic(&q))
}
fn infinity_degree_bound(deg_a: i64, deg_b: i64, lc_a: &Rational, gamma: i64) -> Option<i64> {
if deg_a < 0 {
return Some((gamma + 1).max(0)); }
let phi = deg_a - deg_b;
let delta = if phi >= 0 {
gamma - phi
} else if phi == -1 {
let resonant = if lc_a.is_integer() {
-lc_a.numer().to_i64()?
} else {
i64::MIN
};
(gamma + 1).max(resonant)
} else {
gamma + 1
};
Some(delta.max(0))
}
pub fn solve_rational_rde_generalized_checked(
f_num: &QPoly,
f_den: &QPoly,
c_num: &QPoly,
c_den: &QPoly,
) -> RdeOutcome {
let f_den_t = trim(f_den.clone());
let f_num_t = trim(f_num.clone());
if f_den_t.is_empty() {
return RdeOutcome::Declined(RdeDecline::MalformedInput);
}
if f_num_t.is_empty() {
return solve_rational_rde_checked(&poly_zero(), c_num, c_den);
}
if degree(&f_den_t) == 0 {
let scale = Rational::from(1) / f_den_t[0].clone();
let f_poly = poly_scale(&f_num_t, &scale);
return solve_rational_rde_checked(&f_poly, c_num, c_den);
}
let gf = poly_gcd(&f_num_t, &f_den_t);
let a_raw = poly_div_exact(&f_num_t, &gf);
let b_raw = poly_div_exact(&f_den_t, &gf);
let b_raw = trim(b_raw);
let a_raw = trim(a_raw);
let b_d = degree(&b_raw);
if b_d <= 0 {
if b_d < 0 {
return RdeOutcome::Declined(RdeDecline::MalformedInput);
}
let scale = Rational::from(1) / b_raw[0].clone();
let f_poly = poly_scale(&a_raw, &scale);
return solve_rational_rde_checked(&f_poly, c_num, c_den);
}
let b_lc_inv = Rational::from(1) / b_raw[b_d as usize].clone();
let big_a = poly_scale(&a_raw, &b_lc_inv);
let big_b = poly_scale(&b_raw, &b_lc_inv);
let c_num_t = trim(c_num.clone());
let c_den_t = trim(c_den.clone());
if c_num_t.is_empty() {
return RdeOutcome::Solved {
num: poly_zero(),
den: poly_one(),
};
}
if c_den_t.is_empty() {
return RdeOutcome::Declined(RdeDecline::MalformedInput);
}
let gc = poly_gcd(&c_num_t, &c_den_t);
let big_c_raw = poly_div_exact(&c_num_t, &gc);
let d_raw = trim(poly_div_exact(&c_den_t, &gc));
let d_d = degree(&d_raw);
if d_d < 0 {
return RdeOutcome::Declined(RdeDecline::MalformedInput);
}
let d_lc_inv = Rational::from(1) / d_raw[d_d as usize].clone();
let big_d = poly_scale(&d_raw, &d_lc_inv);
let big_c = poly_scale(&big_c_raw, &d_lc_inv);
let e_poly = poly_gcd(&big_d, &poly_deriv(&big_d));
let q_res = match resonant_denominator(&big_a, &big_b) {
Ok(q) => q,
Err(reason) => return RdeOutcome::Declined(reason),
};
let big_q = poly_monic(&poly_mul(&e_poly, &q_res));
let q_prime = poly_deriv(&big_q);
let gamma = degree(&big_c) - degree(&big_d);
let deg_a = degree(&big_a);
debug_assert!(deg_a >= 0, "A is nonzero: f_num was checked nonempty");
let lc_a = big_a
.get(deg_a.max(0) as usize)
.cloned()
.unwrap_or_else(|| Rational::from(0));
let Some(delta) = infinity_degree_bound(deg_a, degree(&big_b), &lc_a, gamma) else {
return RdeOutcome::Declined(RdeDecline::AnsatzTooLarge);
};
let dbound = (degree(&big_q) + delta).max(0) as usize;
let cols = dbound + 1;
if cols > MAX_RDE_UNKNOWNS {
return RdeOutcome::Declined(RdeDecline::AnsatzTooLarge);
}
let bd = poly_mul(&big_b, &big_d);
let u_mul = poly_mul(&bd, &big_q); let v_mul = poly_sub(
&poly_mul(&poly_mul(&big_a, &big_d), &big_q),
&poly_mul(&bd, &q_prime),
); let target = poly_mul(&poly_mul(&big_c, &big_b), &poly_mul(&big_q, &big_q));
let max_deg = (degree(&u_mul) + dbound as i64 - 1)
.max(degree(&v_mul) + dbound as i64)
.max(degree(&target))
.max(0) as usize;
let n_rows = max_deg + 1;
let mut mat = vec![vec![Rational::from(0); cols]; n_rows];
for (d, row) in mat.iter_mut().enumerate() {
let d = d as i64;
for (j, cell) in row.iter_mut().enumerate() {
let jj = j as i64;
*cell = Rational::from(jj) * coeff(&u_mul, d - jj + 1) + coeff(&v_mul, d - jj);
}
}
let rhs: Vec<Rational> = (0..n_rows).map(|d| coeff(&target, d as i64)).collect();
let Some(solution) = solve_linear_system(mat, rhs, cols) else {
return RdeOutcome::NoRationalSolution;
};
let n_poly = trim(solution);
let lhs = poly_add(
&poly_mul(&u_mul, &poly_deriv(&n_poly)),
&poly_mul(&v_mul, &n_poly),
);
if !polys_equal(&lhs, &target) {
return RdeOutcome::Declined(RdeDecline::VerificationFailed);
}
if n_poly.is_empty() {
return RdeOutcome::Solved {
num: poly_zero(),
den: poly_one(),
};
}
let gvq = poly_gcd(&n_poly, &big_q);
let num = poly_div_exact(&n_poly, &gvq);
let den = poly_monic(&poly_div_exact(&big_q, &gvq));
RdeOutcome::Solved { num, den }
}
pub fn solve_rational_rde_generalized(
f_num: &QPoly,
f_den: &QPoly,
c_num: &QPoly,
c_den: &QPoly,
) -> Option<(QPoly, QPoly)> {
solve_rational_rde_generalized_checked(f_num, f_den, c_num, c_den).solution()
}
pub fn solve_rational_rde_k(
field: &NumberField,
f: &KPoly,
c_num: &KPoly,
c_den: &KPoly,
) -> Option<(KPoly, KPoly)> {
let c_num = NumberField::kpoly_trim(c_num.clone());
let c_den = NumberField::kpoly_trim(c_den.clone());
let one: KPoly = vec![field.from_int(1)];
if c_num.is_empty() {
return Some((Vec::new(), one));
}
if c_den.is_empty() {
return None; }
let g = field.kpoly_gcd(&c_num, &c_den)?;
let big_c = field.kpoly_div_exact(&c_num, &g)?;
let b_raw = field.kpoly_div_exact(&c_den, &g)?;
let bd = NumberField::kdeg(&b_raw);
let lead_inv = field.inv(&b_raw[bd as usize])?;
let big_b = field.kpoly_scale(&b_raw, &lead_inv);
let big_c = field.kpoly_scale(&big_c, &lead_inv);
let bprime = field.kpoly_deriv(&big_b);
let e_poly = field.kpoly_gcd(&big_b, &bprime)?;
let g_poly = field.kpoly_div_exact(&big_b, &e_poly)?;
let eprime = field.kpoly_deriv(&e_poly);
let ge = field.kpoly_mul(&g_poly, &e_poly);
let gep = field.kpoly_mul(&g_poly, &eprime);
let gef = field.kpoly_mul(&ge, f);
let target = field.kpoly_mul(&big_c, &e_poly);
let deg_b = NumberField::kdeg(&big_b);
let deg_c = NumberField::kdeg(&big_c);
let deg_e = NumberField::kdeg(&e_poly).max(0);
let deg_f = NumberField::kdeg(f).max(0);
let poly_part = (deg_c - deg_b).max(0);
let dbound = (deg_e + poly_part.max(deg_f) + 2).max(0) as usize;
let cols = dbound + 1;
let max_deg = (NumberField::kdeg(&gef) + dbound as i64)
.max(NumberField::kdeg(&ge) + dbound as i64)
.max(NumberField::kdeg(&gep) + dbound as i64)
.max(NumberField::kdeg(&target))
.max(0) as usize;
let n_rows = max_deg + 1;
let mut mat = vec![vec![NumberField::k_zero(); cols]; n_rows];
for (d, row) in mat.iter_mut().enumerate() {
let d = d as i64;
for (j, cell) in row.iter_mut().enumerate() {
let jj = j as i64;
let mut v = field.mul(&field.from_int(jj), &NumberField::kcoeff(&ge, d - jj + 1));
v = field.sub(&v, &NumberField::kcoeff(&gep, d - jj));
v = field.add(&v, &NumberField::kcoeff(&gef, d - jj));
*cell = v;
}
}
let rhs: Vec<KElem> = (0..n_rows)
.map(|d| NumberField::kcoeff(&target, d as i64))
.collect();
let solution = solve_linear_system_k(field, mat, rhs, cols)?;
let n_poly = NumberField::kpoly_trim(solution);
let np = field.kpoly_deriv(&n_poly);
let lhs = field.kpoly_mul(
&field.kpoly_add(
&field.kpoly_sub(
&field.kpoly_mul(&np, &e_poly),
&field.kpoly_mul(&n_poly, &eprime),
),
&field.kpoly_mul(&field.kpoly_mul(f, &n_poly), &e_poly),
),
&big_b,
);
let rhs_check = field.kpoly_mul(&big_c, &field.kpoly_mul(&e_poly, &e_poly));
if !NumberField::kpoly_eq(&lhs, &rhs_check) {
return None;
}
if n_poly.is_empty() {
return Some((Vec::new(), one));
}
let gve = field.kpoly_gcd(&n_poly, &e_poly)?;
let num = field.kpoly_div_exact(&n_poly, &gve)?;
let den = field.kpoly_monic(&field.kpoly_div_exact(&e_poly, &gve)?)?;
Some((num, den))
}
#[allow(clippy::type_complexity)]
pub fn solve_primitive_top_rde_k(
field: &NumberField,
c_num: &KPoly,
c_den: &KPoly,
gd_num: &KPoly,
gd_den: &KPoly,
d: i64,
) -> Option<Option<KElem>> {
let c_num = NumberField::kpoly_trim(c_num.clone());
let c_den = NumberField::kpoly_trim(c_den.clone());
if c_den.is_empty() {
return None; }
if c_num.is_empty() {
return Some(Some(NumberField::k_zero()));
}
let gd_num = NumberField::kpoly_trim(gd_num.clone());
let gd_den = NumberField::kpoly_trim(gd_den.clone());
let dp1 = field.from_int(d + 1);
let drift_num = field.kpoly_scale(&gd_num, &dp1);
if drift_num.is_empty() || gd_den.is_empty() {
return Some(None);
}
let g = field.kpoly_gcd(&c_den, &gd_den)?;
let cd_over_g = field.kpoly_div_exact(&c_den, &g)?;
let gdd_over_g = field.kpoly_div_exact(&gd_den, &g)?;
let bc_raw = field.kpoly_mul(&c_den, &gdd_over_g); let bcd = NumberField::kdeg(&bc_raw);
let lead_inv = field.inv(&bc_raw[bcd as usize])?;
let bc = field.kpoly_scale(&bc_raw, &lead_inv);
let cf = field.kpoly_scale(&field.kpoly_mul(&c_num, &gdd_over_g), &lead_inv);
let cg = field.kpoly_scale(&field.kpoly_mul(&drift_num, &cd_over_g), &lead_inv);
let bcp = field.kpoly_deriv(&bc);
let e_poly = field.kpoly_gcd(&bc, &bcp)?;
let g_poly = field.kpoly_div_exact(&bc, &e_poly)?;
let eprime = field.kpoly_deriv(&e_poly);
let ge = field.kpoly_mul(&g_poly, &e_poly);
let gep = field.kpoly_mul(&g_poly, &eprime);
let target = field.kpoly_mul(&cf, &e_poly);
let cg_e = field.kpoly_mul(&cg, &e_poly);
let deg_bc = NumberField::kdeg(&bc);
let deg_cf = NumberField::kdeg(&cf);
let deg_e = NumberField::kdeg(&e_poly).max(0);
let poly_part = (deg_cf - deg_bc).max(0);
let dbound = (deg_e + poly_part + 2).max(0) as usize;
let n_cols = dbound + 1; let cols = n_cols + 1;
let max_deg = (NumberField::kdeg(&ge) + dbound as i64)
.max(NumberField::kdeg(&gep) + dbound as i64)
.max(NumberField::kdeg(&target))
.max(NumberField::kdeg(&cg_e))
.max(0) as usize;
let n_rows = max_deg + 1;
let mut mat = vec![vec![NumberField::k_zero(); cols]; n_rows];
for (deg, row) in mat.iter_mut().enumerate() {
let deg = deg as i64;
for (j, cell) in row.iter_mut().take(n_cols).enumerate() {
let jj = j as i64;
let mut v = field.mul(&field.from_int(jj), &NumberField::kcoeff(&ge, deg - jj + 1));
v = field.sub(&v, &NumberField::kcoeff(&gep, deg - jj));
*cell = v;
}
row[n_cols] = NumberField::kcoeff(&cg_e, deg);
}
let rhs: Vec<KElem> = (0..n_rows)
.map(|deg| NumberField::kcoeff(&target, deg as i64))
.collect();
let solution = solve_linear_system_k(field, mat, rhs, cols)?;
let n_poly = NumberField::kpoly_trim(solution[..n_cols].to_vec());
let e_val = solution[n_cols].clone();
let np = field.kpoly_deriv(&n_poly);
let lhs = field.kpoly_mul(
&field.kpoly_sub(
&field.kpoly_mul(&np, &e_poly),
&field.kpoly_mul(&n_poly, &eprime),
),
&bc,
);
let rhs_num = field.kpoly_sub(&cf, &field.kpoly_scale(&cg, &e_val));
let rhs_check = field.kpoly_mul(&rhs_num, &field.kpoly_mul(&e_poly, &e_poly));
if !NumberField::kpoly_eq(&lhs, &rhs_check) {
return None;
}
Some(Some(e_val))
}
fn solve_linear_system_k(
field: &NumberField,
mut mat: Vec<Vec<KElem>>,
mut rhs: Vec<KElem>,
cols: usize,
) -> Option<Vec<KElem>> {
let rows = mat.len();
let mut pivot_row_of_col: Vec<Option<usize>> = vec![None; cols];
let mut row = 0usize;
for col in 0..cols {
if row >= rows {
break;
}
let Some(sel) = (row..rows).find(|&r| !NumberField::is_zero(&mat[r][col])) else {
continue;
};
mat.swap(row, sel);
rhs.swap(row, sel);
let piv_inv = field.inv(&mat[row][col])?;
for cell in mat[row][col..cols].iter_mut() {
*cell = field.mul(cell, &piv_inv);
}
rhs[row] = field.mul(&rhs[row], &piv_inv);
let pivot_row = mat[row].clone();
let pivot_rhs = rhs[row].clone();
for r in 0..rows {
if r != row && !NumberField::is_zero(&mat[r][col]) {
let factor = mat[r][col].clone();
for (cell, pv) in mat[r][col..cols]
.iter_mut()
.zip(pivot_row[col..cols].iter())
{
*cell = field.sub(cell, &field.mul(&factor, pv));
}
rhs[r] = field.sub(&rhs[r], &field.mul(&factor, &pivot_rhs));
}
}
pivot_row_of_col[col] = Some(row);
row += 1;
}
for r in 0..rows {
if mat[r].iter().all(NumberField::is_zero) && !NumberField::is_zero(&rhs[r]) {
return None;
}
}
let mut x = vec![NumberField::k_zero(); cols];
for (col, pr) in pivot_row_of_col.iter().enumerate() {
if let Some(pr) = pr {
x[col] = rhs[*pr].clone();
}
}
Some(x)
}
use crate::kernel::{ExprData, ExprId, ExprPool};
pub fn expr_to_qrational(expr: ExprId, var: ExprId, pool: &ExprPool) -> Option<(QPoly, QPoly)> {
if expr == var {
return Some((vec![Rational::from(0), Rational::from(1)], poly_one()));
}
match pool.get(expr) {
ExprData::Integer(n) => Some((vec![Rational::from(n.0.to_i64()?)], poly_one())),
ExprData::Rational(r) => Some((vec![r.0.clone()], poly_one())),
ExprData::Add(args) => {
let mut acc = (poly_zero(), poly_one());
for a in &args {
let term = expr_to_qrational(*a, var, pool)?;
acc = rat_add(&acc, &term);
}
Some(acc)
}
ExprData::Mul(args) => {
let mut acc = (poly_one(), poly_one());
for a in &args {
let factor = expr_to_qrational(*a, var, pool)?;
acc = rat_mul(&acc, &factor);
}
Some(acc)
}
ExprData::Pow { base, exp } => {
let n = super::tower::literal_integer(exp, pool)?;
let (bn, bd) = expr_to_qrational(base, var, pool)?;
let m = u32::try_from(n.unsigned_abs()).ok()?;
if n >= 0 {
Some((poly_pow_bounded(&bn, m)?, poly_pow_bounded(&bd, m)?))
} else {
if trim(bn.clone()).is_empty() {
return None; }
Some((poly_pow_bounded(&bd, m)?, poly_pow_bounded(&bn, m)?))
}
}
_ => None,
}
}
fn rat_add(a: &(QPoly, QPoly), b: &(QPoly, QPoly)) -> (QPoly, QPoly) {
let num = poly_add(&poly_mul(&a.0, &b.1), &poly_mul(&b.0, &a.1));
let den = poly_mul(&a.1, &b.1);
(num, den)
}
fn rat_mul(a: &(QPoly, QPoly), b: &(QPoly, QPoly)) -> (QPoly, QPoly) {
(poly_mul(&a.0, &b.0), poly_mul(&a.1, &b.1))
}
#[cfg(test)]
mod tests {
#[test]
fn a_huge_dense_power_is_declined_not_built() {
let p = ExprPool::new();
let x = p.symbol("x", crate::kernel::Domain::Real);
let big = p.pow(x, p.integer(1_i64 << 31));
let f = p.pow(p.add(vec![big, p.integer(1)]), p.integer(-1));
let t = std::time::Instant::now();
assert!(expr_to_qrational(f, x, &p).is_none());
assert!(t.elapsed().as_secs() < 1, "{:?}", t.elapsed());
let small = p.pow(
p.add(vec![p.pow(x, p.integer(3)), p.integer(1)]),
p.integer(-1),
);
assert!(expr_to_qrational(small, x, &p).is_some());
}
use super::*;
fn rat(n: i64) -> Rational {
Rational::from(n)
}
fn lcg(state: &mut u64) -> i64 {
*state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
((*state >> 33) % 21) as i64 - 10
}
#[test]
fn poly_gcd_flint_agrees_with_euclid_on_random_rationals() {
let mut s = 0x2545_F491_4F6C_DD1D_u64;
for trial in 0..400 {
let da = (trial % 7) + 1;
let db = (trial % 5) + 1;
let mk = |n: usize, s: &mut u64| -> QPoly {
let mut p: QPoly = (0..=n)
.map(|_| {
let num = lcg(s);
let den = lcg(s).unsigned_abs().max(1) as i64;
Rational::from((num, den))
})
.collect();
if trim(p.clone()).is_empty() {
p = vec![rat(1)];
}
p
};
let a = mk(da, &mut s);
let b = mk(db, &mut s);
let (a, b) = if trial % 2 == 0 {
let c = mk(2, &mut s);
(poly_mul(&a, &c), poly_mul(&b, &c))
} else {
(a, b)
};
assert_eq!(
poly_gcd(&a, &b),
poly_gcd_euclid(&a, &b),
"trial {trial}: FLINT gcd disagrees with ℚ-Euclid on {a:?}, {b:?}"
);
}
}
#[test]
fn poly_gcd_flint_agrees_with_euclid_on_degenerate_inputs() {
let zero: QPoly = Vec::new();
let cases: Vec<(QPoly, QPoly)> = vec![
(zero.clone(), zero.clone()),
(vec![rat(0), rat(0)], zero.clone()),
(zero.clone(), vec![rat(3), rat(6)]),
(vec![rat(3), rat(6)], zero.clone()),
(vec![rat(5)], vec![rat(7)]),
(vec![rat(0), rat(1)], vec![rat(0), rat(0), rat(1)]),
(
vec![Rational::from((1, 3)), Rational::from((2, 5))],
vec![Rational::from((7, 11))],
),
];
for (a, b) in cases {
assert_eq!(
poly_gcd(&a, &b),
poly_gcd_euclid(&a, &b),
"FLINT gcd disagrees with ℚ-Euclid on {a:?}, {b:?}"
);
}
}
#[test]
fn rational_elementary_exp_x() {
let f = vec![rat(1)]; let c_num = vec![rat(-1), rat(1)]; let c_den = vec![rat(0), rat(0), rat(1)]; let sol = solve_rational_rde(&f, &c_num, &c_den).expect("elementary");
assert_eq!(trim(sol.0.clone()), vec![rat(1)], "numerator should be 1");
assert_eq!(trim(sol.1.clone()), vec![rat(0), rat(1)], "denominator x");
}
#[test]
fn rational_nonelementary_x2_over_x_plus_1() {
let f = vec![rat(1)];
let c_num = vec![rat(0), rat(0), rat(1)]; let c_den = vec![rat(1), rat(1)]; assert!(
solve_rational_rde(&f, &c_num, &c_den).is_none(),
"x²/(x+1)·exp(x) must be certified non-elementary"
);
}
#[test]
fn rational_nonelementary_one_over_x() {
let f = vec![rat(1)];
let c_num = vec![rat(1)]; let c_den = vec![rat(0), rat(1)]; assert!(solve_rational_rde(&f, &c_num, &c_den).is_none());
}
#[test]
fn rational_nonelementary_one_over_x2() {
let f = vec![rat(1)];
let c_num = vec![rat(1)];
let c_den = vec![rat(0), rat(0), rat(1)]; assert!(solve_rational_rde(&f, &c_num, &c_den).is_none());
}
#[test]
fn rational_reduces_to_polynomial_case() {
let f = vec![rat(0), rat(2)]; let c_num = vec![rat(0), rat(1)]; let c_den = poly_one();
let sol = solve_rational_rde(&f, &c_num, &c_den).expect("elementary");
assert_eq!(trim(sol.0), vec![Rational::from((1, 2))]);
assert_eq!(trim(sol.1), vec![rat(1)]);
}
#[test]
fn divrem_gcd_basic() {
let a = vec![rat(-1), rat(0), rat(1)];
let b = vec![rat(1), rat(1)];
let (q, r) = poly_divrem(&a, &b);
assert_eq!(trim(q), vec![rat(-1), rat(1)]); assert!(trim(r).is_empty());
let c = vec![rat(1), rat(-2), rat(1)];
let g = poly_gcd(&a, &c);
assert_eq!(trim(g), vec![rat(-1), rat(1)]);
}
#[test]
fn qrational_parse() {
use crate::kernel::{Domain, ExprPool};
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let num = pool.add(vec![x, pool.integer(-1_i32)]);
let den = pool.pow(x, pool.integer(-2_i32));
let expr = pool.mul(vec![num, den]);
let (n, d) = expr_to_qrational(expr, x, &pool).expect("parse");
let lhs = poly_mul(&n, &vec![rat(0), rat(0), rat(1)]);
let rhs = poly_mul(&d, &vec![rat(-1), rat(1)]);
assert!(polys_equal(&lhs, &rhs), "n={n:?} d={d:?}");
}
fn field_sqrt2() -> NumberField {
NumberField::new(vec![rat(-2), rat(0), rat(1)])
}
fn kc(field: &NumberField, n: i64) -> KElem {
field.from_int(n)
}
#[test]
fn rational_rde_k_elementary_sqrt2() {
let field = field_sqrt2();
let f: KPoly = vec![kc(&field, 1)]; let sqrt2 = vec![rat(0), rat(1)]; let c0 = field.sub(&field.neg(&sqrt2), &kc(&field, 1));
let c_num: KPoly = vec![c0, kc(&field, 1)];
let base: KPoly = vec![field.neg(&sqrt2), kc(&field, 1)]; let c_den = field.kpoly_mul(&base, &base);
let (vn, vd) = solve_rational_rde_k(&field, &f, &c_num, &c_den).expect("elementary");
assert_eq!(NumberField::kdeg(&vn), 0);
assert_eq!(trim(vn[0].clone()), vec![rat(1)]);
assert!(NumberField::kpoly_eq(&vd, &base));
}
#[test]
fn rational_rde_k_nonelementary_sqrt2() {
let field = field_sqrt2();
let f: KPoly = vec![kc(&field, 1)];
let sqrt2 = vec![rat(0), rat(1)];
let c_num: KPoly = vec![NumberField::k_zero(), NumberField::k_zero(), kc(&field, 1)]; let c_den: KPoly = vec![field.neg(&sqrt2), kc(&field, 1)]; assert!(solve_rational_rde_k(&field, &f, &c_num, &c_den).is_none());
}
#[test]
fn rational_rde_k_reduces_to_polynomial() {
let field = field_sqrt2();
let f: KPoly = vec![NumberField::k_zero(), kc(&field, 2)]; let c_num: KPoly = vec![NumberField::k_zero(), kc(&field, 1)]; let c_den: KPoly = vec![kc(&field, 1)]; let (vn, vd) = solve_rational_rde_k(&field, &f, &c_num, &c_den).expect("elementary");
assert_eq!(trim(vn[0].clone()), vec![Rational::from((1, 2))]);
assert_eq!(trim(vd[0].clone()), vec![rat(1)]);
}
#[test]
fn primitive_top_inv_x_plus_sqrt2_over_log_x_none() {
let field = field_sqrt2();
let sqrt2 = vec![rat(0), rat(1)];
let c_num: KPoly = vec![kc(&field, 1)];
let c_den: KPoly = vec![sqrt2.clone(), kc(&field, 1)];
let gd_num: KPoly = vec![kc(&field, 1)];
let gd_den: KPoly = vec![NumberField::k_zero(), kc(&field, 1)];
let r = solve_primitive_top_rde_k(&field, &c_num, &c_den, &gd_num, &gd_den, 1);
assert!(
r.is_none(),
"eq (18) for 1/(x+√2) with b=x must be unsolvable (None); got {r:?}"
);
}
#[test]
fn primitive_top_inv_x_plus_sqrt2_sq_over_log_x_some() {
let field = field_sqrt2();
let sqrt2 = vec![rat(0), rat(1)];
let base: KPoly = vec![sqrt2.clone(), kc(&field, 1)]; let c_num: KPoly = vec![kc(&field, 1)]; let c_den = field.kpoly_mul(&base, &base); let gd_num: KPoly = vec![kc(&field, 1)];
let gd_den: KPoly = vec![NumberField::k_zero(), kc(&field, 1)]; let r = solve_primitive_top_rde_k(&field, &c_num, &c_den, &gd_num, &gd_den, 1);
assert!(
r.is_some(),
"eq (18) for 1/(x+√2)² with b=x must be solvable (Some); got {r:?}"
);
}
#[test]
fn primitive_top_log_derivative_same_arg_some() {
let field = field_sqrt2();
let sqrt2 = vec![rat(0), rat(1)];
let base: KPoly = vec![sqrt2.clone(), kc(&field, 1)]; let c_num: KPoly = vec![kc(&field, 1)]; let c_den = base.clone(); let gd_num: KPoly = vec![kc(&field, 1)];
let gd_den = base.clone();
let r = solve_primitive_top_rde_k(&field, &c_num, &c_den, &gd_num, &gd_den, 1);
assert!(
r.is_some(),
"eq (18) for 1/(x+√2) with b=x+√2 must be solvable (Some); got {r:?}"
);
}
#[test]
fn primitive_top_polynomial_coeff_some() {
let field = field_sqrt2();
let c_num: KPoly = vec![NumberField::k_zero(), kc(&field, 1)];
let c_den: KPoly = vec![kc(&field, 1)];
let gd_num: KPoly = vec![kc(&field, 1)];
let gd_den: KPoly = vec![NumberField::k_zero(), kc(&field, 1)];
let r = solve_primitive_top_rde_k(&field, &c_num, &c_den, &gd_num, &gd_den, 1);
assert!(
r.is_some(),
"eq (18) for the polynomial coeff f_1=x (v=x²/2, e=0) must be solvable; got {r:?}"
);
}
#[test]
fn gen_rde_exp_inv_x_nonelementary() {
let f_num = vec![rat(-1)];
let f_den = vec![rat(0), rat(0), rat(1)]; let c_num = poly_one();
let c_den = poly_one();
assert!(
solve_rational_rde_generalized(&f_num, &f_den, &c_num, &c_den).is_none(),
"∫ exp(1/x) dx must be certified non-elementary"
);
}
#[test]
fn gen_rde_inv_x2_exp_inv_x_elementary() {
let f_num = vec![rat(-1)];
let f_den = vec![rat(0), rat(0), rat(1)]; let c_num = poly_one(); let c_den = vec![rat(0), rat(0), rat(1)]; let (vn, vd) = solve_rational_rde_generalized(&f_num, &f_den, &c_num, &c_den)
.expect("∫ (1/x²)·exp(1/x) dx must be elementary");
assert_eq!(
trim(vn.clone()),
vec![rat(-1)],
"numerator should be −1, got {vn:?}"
);
assert_eq!(
trim(vd.clone()),
poly_one(),
"denominator should be 1, got {vd:?}"
);
}
#[test]
fn gen_rde_exp_neg_inv_x2_elementary() {
let f_num = vec![rat(2)];
let f_den = vec![rat(0), rat(0), rat(0), rat(1)]; let c_num = vec![rat(2)];
let c_den = vec![rat(0), rat(0), rat(0), rat(1)]; let (vn, vd) = solve_rational_rde_generalized(&f_num, &f_den, &c_num, &c_den)
.expect("∫ (2/x³)·exp(−1/x²) dx must be elementary");
assert_eq!(
trim(vn.clone()),
poly_one(),
"numerator should be 1, got {vn:?}"
);
assert_eq!(
trim(vd.clone()),
poly_one(),
"denominator should be 1, got {vd:?}"
);
}
fn assert_solves(
f_num: &QPoly,
f_den: &QPoly,
c_num: &QPoly,
c_den: &QPoly,
num: &QPoly,
den: &QPoly,
) {
let lhs = poly_add(
&poly_mul(
&poly_mul(f_den, c_den),
&poly_sub(
&poly_mul(&poly_deriv(num), den),
&poly_mul(num, &poly_deriv(den)),
),
),
&poly_mul(&poly_mul(f_num, c_den), &poly_mul(num, den)),
);
let rhs = poly_mul(&poly_mul(c_num, f_den), &poly_mul(den, den));
assert!(
polys_equal(&lhs, &rhs),
"v = ({num:?})/({den:?}) does not satisfy the RDE"
);
}
#[test]
fn gen_rde_resonant_simple_pole_kappa1() {
let f_num = vec![rat(1), rat(1)]; let f_den = vec![rat(0), rat(1)]; let c_num = poly_one();
let c_den = poly_one();
let (vn, vd) = match solve_rational_rde_generalized_checked(&f_num, &f_den, &c_num, &c_den)
{
RdeOutcome::Solved { num, den } => (num, den),
other => panic!("expected v = (x−1)/x, got {other:?}"),
};
assert_eq!(trim(vn.clone()), vec![rat(-1), rat(1)], "numerator ≠ x−1");
assert_eq!(trim(vd.clone()), vec![rat(0), rat(1)], "denominator ≠ x");
assert_solves(&f_num, &f_den, &c_num, &c_den, &vn, &vd);
}
#[test]
fn gen_rde_resonant_simple_pole_kappa2() {
let f_num = vec![rat(2), rat(1)]; let f_den = vec![rat(0), rat(1)]; let c_num = poly_one();
let c_den = poly_one();
let (vn, vd) = match solve_rational_rde_generalized_checked(&f_num, &f_den, &c_num, &c_den)
{
RdeOutcome::Solved { num, den } => (num, den),
other => panic!("expected v = (x²−2x+2)/x², got {other:?}"),
};
assert_eq!(
trim(vn.clone()),
vec![rat(2), rat(-2), rat(1)],
"numerator ≠ x²−2x+2"
);
assert_eq!(
trim(vd.clone()),
vec![rat(0), rat(0), rat(1)],
"denominator ≠ x²"
);
assert_solves(&f_num, &f_den, &c_num, &c_den, &vn, &vd);
}
#[test]
fn gen_rde_resonant_pole_irrational() {
let f_num = vec![rat(1), rat(2), rat(1)];
let f_den = vec![rat(1), rat(0), rat(1)];
let c_num = poly_one();
let c_den = poly_one();
let (vn, vd) = match solve_rational_rde_generalized_checked(&f_num, &f_den, &c_num, &c_den)
{
RdeOutcome::Solved { num, den } => (num, den),
other => panic!("expected v = (x²−2x+3)/(x²+1), got {other:?}"),
};
assert_eq!(trim(vn.clone()), vec![rat(3), rat(-2), rat(1)]);
assert_eq!(trim(vd.clone()), vec![rat(1), rat(0), rat(1)]);
assert_solves(&f_num, &f_den, &c_num, &c_den, &vn, &vd);
}
#[test]
fn gen_rde_negative_residue_still_proves_no_solution() {
let f_num = vec![rat(-1), rat(1)];
let f_den = vec![rat(0), rat(1)];
let out = solve_rational_rde_generalized_checked(&f_num, &f_den, &poly_one(), &poly_one());
assert_eq!(
out,
RdeOutcome::NoRationalSolution,
"∫ eˣ/x dx must stay *proved* non-elementary"
);
assert!(out.proves_no_solution());
}
#[test]
fn gen_rde_huge_residue_declines_rather_than_certifies() {
let f_num = vec![rat(4000), rat(1)]; let f_den = vec![rat(0), rat(1)]; let out = solve_rational_rde_generalized_checked(&f_num, &f_den, &poly_one(), &poly_one());
assert!(
out.is_declined(),
"a residue past the cap must decline; got {out:?}"
);
assert!(
!out.proves_no_solution(),
"a decline must never license a NonElementary certificate"
);
assert!(solve_rational_rde_generalized(&f_num, &f_den, &poly_one(), &poly_one()).is_none());
}
#[test]
fn poly_rde_checked_proves_no_solution() {
let f = poly_one(); let c_num = poly_one();
let c_den = vec![rat(0), rat(1)]; assert_eq!(
solve_rational_rde_checked(&f, &c_num, &c_den),
RdeOutcome::NoRationalSolution
);
}
#[test]
fn gen_rde_falls_back_to_polynomial_f() {
let f_num = vec![rat(1)];
let f_den = poly_one(); let c_num = vec![rat(-1), rat(1)]; let c_den = vec![rat(0), rat(0), rat(1)]; let (vn, vd) = solve_rational_rde_generalized(&f_num, &f_den, &c_num, &c_den)
.expect("fallback must succeed");
assert_eq!(trim(vn), vec![rat(1)]);
assert_eq!(trim(vd), vec![rat(0), rat(1)]);
}
}