use rug::Rational;
use super::alg_field::{AlgElem, AlgExtension, RatFn};
use super::poly_rde::{
degree, poly_add, poly_deriv, poly_mul, poly_one, poly_scale, poly_zero, trim, QPoly,
};
use super::rational_rde::{
poly_div_exact, poly_divrem, poly_gcd, poly_monic, poly_pow, poly_sub, RdeDecline,
};
const DEG_CAP: usize = 6;
const X_DEG_SANITY_CAP: usize = 48;
const MAX_ALG_RESONANCE_SEARCH: i64 = 256;
const MAX_ALG_RDE_UNKNOWNS: usize = 512;
const MAX_SHEAR_WEIGHT: i64 = 4;
const MAX_PROVED_EXT_DEGREE: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AlgRdeOutcome {
Solved(AlgElem),
NoRationalSolution,
Declined(RdeDecline),
}
impl AlgRdeOutcome {
pub(crate) fn solution(self) -> Option<AlgElem> {
match self {
AlgRdeOutcome::Solved(y) => Some(y),
_ => None,
}
}
#[allow(dead_code)]
pub(crate) fn proves_no_solution(&self) -> bool {
matches!(self, AlgRdeOutcome::NoRationalSolution)
}
#[allow(dead_code)]
pub(crate) fn is_declined(&self) -> bool {
matches!(self, AlgRdeOutcome::Declined(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MinPolyStatus {
ProvedIrreducible,
Unknown,
}
pub(crate) fn alg_x_degree_bound(e: &AlgExtension, f: &AlgElem, g: &AlgElem) -> usize {
let comp_xdeg = |el: &AlgElem| -> i64 {
el.iter()
.map(|c| degree(c.numer()).max(degree(c.denom())))
.max()
.unwrap_or(0)
};
let mut base = comp_xdeg(f).max(comp_xdeg(g));
let d = e.degree() as usize;
let gen = e.generator();
for j in 0..d {
if let Some(aj) = e.pow(&gen, j as i64) {
base = base.max(comp_xdeg(&e.derivation(&aj)));
}
}
(base.max(0) + 2) as usize
}
pub(crate) fn solve_alg_rde_checked(
e: &AlgExtension,
f: &RatFn,
g: &AlgElem,
minpoly: MinPolyStatus,
) -> AlgRdeOutcome {
solve_alg_rde_general_checked(e, &e.constant(f.clone()), g, minpoly)
}
pub(crate) fn solve_alg_rde_general_checked(
e: &AlgExtension,
f: &AlgElem,
g: &AlgElem,
minpoly: MinPolyStatus,
) -> AlgRdeOutcome {
let d = e.degree();
if d <= 0 {
return AlgRdeOutcome::Declined(RdeDecline::MalformedInput);
}
let d = d as usize;
let cap = alg_x_degree_bound(e, f, g).clamp(DEG_CAP, X_DEG_SANITY_CAP);
let dens = candidate_denominators(e, f, g, d);
for den in &dens {
for ncap in 0..=cap {
if let DenSolve::Solved(y) = solve_with_denominator_outcome(e, f, g, den, ncap, d) {
return AlgRdeOutcome::Solved(y);
}
}
}
prove_no_solution(e, f, g, d, minpoly)
}
#[allow(dead_code)]
pub(crate) fn solve_alg_rde(e: &AlgExtension, f: &RatFn, g: &AlgElem) -> Option<AlgElem> {
solve_alg_rde_checked(e, f, g, MinPolyStatus::Unknown).solution()
}
pub(crate) fn solve_alg_rde_general(e: &AlgExtension, f: &AlgElem, g: &AlgElem) -> Option<AlgElem> {
solve_alg_rde_general_checked(e, f, g, MinPolyStatus::Unknown).solution()
}
fn prove_no_solution(
e: &AlgExtension,
f: &AlgElem,
g: &AlgElem,
d: usize,
minpoly: MinPolyStatus,
) -> AlgRdeOutcome {
if minpoly != MinPolyStatus::ProvedIrreducible {
return AlgRdeOutcome::Declined(RdeDecline::AlgebraicFieldNotProved);
}
if d > MAX_PROVED_EXT_DEGREE {
return AlgRdeOutcome::Declined(RdeDecline::AlgebraicBoundNotProved);
}
let Some(m) = system_matrix(e, f, d) else {
return AlgRdeOutcome::Declined(RdeDecline::AlgebraicBoundNotProved);
};
let c: Vec<RatFn> = (0..d)
.map(|k| g.get(k).cloned().unwrap_or_else(|| RatFn::int(0)))
.collect();
let den_star = match system_denominator_bound(&m, &c, d) {
Ok(q) => q,
Err(reason) => return AlgRdeOutcome::Declined(reason),
};
let via_matrix = system_infinity_bound(&m, &c, d);
let via_radical = radical_infinity_bound(e, f, g);
let n_bound = match (via_matrix, via_radical) {
(Ok(a), Some(b)) => a.min(b),
(Ok(a), None) => a,
(Err(_), Some(b)) => b,
(Err(reason), None) => return AlgRdeOutcome::Declined(reason),
};
let ncap = (n_bound + degree(&den_star).max(0)).max(0) as usize;
if ncap.saturating_add(1).saturating_mul(d) > MAX_ALG_RDE_UNKNOWNS {
return AlgRdeOutcome::Declined(RdeDecline::AnsatzTooLarge);
}
match solve_with_denominator_outcome(e, f, g, &den_star, ncap, d) {
DenSolve::Solved(y) => AlgRdeOutcome::Solved(y),
DenSolve::Inconsistent => AlgRdeOutcome::NoRationalSolution,
DenSolve::VerificationFailed => AlgRdeOutcome::Declined(RdeDecline::VerificationFailed),
}
}
fn system_matrix(e: &AlgExtension, f: &AlgElem, d: usize) -> Option<Vec<Vec<RatFn>>> {
let gen = e.generator();
let mut m = vec![vec![RatFn::int(0); d]; d];
for j in 0..d {
let aj = e.pow(&gen, j as i64)?;
let col = e.add(&e.derivation(&aj), &e.mul(f, &aj));
for (k, row) in m.iter_mut().enumerate() {
if let Some(r) = col.get(k) {
row[j] = r.clone();
}
}
}
Some(m)
}
fn system_denominator_bound(m: &[Vec<RatFn>], c: &[RatFn], d: usize) -> Result<QPoly, RdeDecline> {
let mut dm = poly_one();
for row in m {
for r in row {
dm = poly_lcm(&dm, r.denom());
}
}
let dm = poly_monic(&dm);
let mut dc = poly_one();
for r in c {
dc = poly_lcm(&dc, r.denom());
}
let dc = poly_monic(&dc);
let e_part = poly_gcd(&dc, &poly_deriv(&dc));
if degree(&dm) < 1 {
return Ok(poly_monic(&e_part)); }
if degree(&poly_gcd(&dm, &poly_deriv(&dm))) >= 1 {
return Err(RdeDecline::AlgebraicBoundNotProved);
}
let dmp = poly_deriv(&dm);
let mut mnum = vec![vec![poly_zero(); d]; d];
for (k, row) in mnum.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell = poly_mul(m[k][j].numer(), &poly_div_exact(&dm, m[k][j].denom()));
}
}
let Some(inv) = poly_inverse_mod(&dmp, &dm) else {
return Err(RdeDecline::VerificationFailed);
};
let mut rmat = vec![vec![poly_zero(); d]; d];
for (k, row) in rmat.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell = poly_divrem(&poly_mul(&mnum[k][j], &inv), &dm).1;
}
}
let bound = residue_spectral_bound(&rmat, &dm, d);
let k_max = bound.floor().numer().to_i64().unwrap_or(i64::MAX);
if k_max > MAX_ALG_RESONANCE_SEARCH {
return Err(RdeDecline::ResonanceSearchTooLarge);
}
let mut q = poly_one();
for k in 1..=k_max.max(0) {
let kd = poly_scale(&dmp, &Rational::from(k));
let mut probe = mnum.clone();
for (i, row) in probe.iter_mut().enumerate() {
row[i] = poly_sub(&row[i], &kd);
}
let theta = poly_matrix_det(&probe, d);
let gk = poly_gcd(&dm, &theta);
if degree(&gk) >= 1 {
let Ok(kk) = u32::try_from(k) else {
return Err(RdeDecline::ResonanceSearchTooLarge);
};
q = poly_mul(&q, &poly_pow(&gk, kk));
if degree(&q) > MAX_ALG_RDE_UNKNOWNS as i64 {
return Err(RdeDecline::AnsatzTooLarge);
}
}
}
Ok(poly_monic(&poly_mul(&e_part, &q)))
}
fn residue_spectral_bound(rmat: &[Vec<QPoly>], dm: &QPoly, d: usize) -> Rational {
let dd = degree(dm).max(0) as usize;
if dd == 0 {
return Rational::from(0);
}
let n = d * dd;
let mut rows = vec![Rational::from(0); n];
let mut cols = vec![Rational::from(0); n];
for (k, row) in rmat.iter().enumerate() {
for (j, entry) in row.iter().enumerate() {
let mut col = poly_divrem(entry, dm).1;
for l in 0..dd {
for (i, cell) in col.iter().enumerate() {
if i < dd {
let a = cell.clone().abs();
rows[k * dd + i] += a.clone();
cols[j * dd + l] += a;
}
}
let mut shifted = vec![Rational::from(0)];
shifted.extend(col.iter().cloned());
col = poly_divrem(&trim(shifted), dm).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 system_infinity_bound(m: &[Vec<RatFn>], c: &[RatFn], d: usize) -> Result<i64, RdeDecline> {
let mut best: Option<i64> = None;
let mut last_err = RdeDecline::AlgebraicBoundNotProved;
for w in 0..=MAX_SHEAR_WEIGHT {
let s: Vec<i64> = (0..d).map(|j| j as i64 * w).collect();
let (ms, cs) = shear_system(m, c, d, &s);
match unsheared_infinity_bound(&ms, &cs, d) {
Ok(nt) => best = Some(best.map_or(nt, |b: i64| b.min(nt))),
Err(reason) => last_err = reason,
}
}
best.ok_or(last_err)
}
fn shear_system(
m: &[Vec<RatFn>],
c: &[RatFn],
d: usize,
s: &[i64],
) -> (Vec<Vec<RatFn>>, Vec<RatFn>) {
let shift = |r: &RatFn, k: i64| -> RatFn {
if k >= 0 {
RatFn::new(poly_mul(r.numer(), &x_pow(k as usize)), r.denom().clone())
} else {
RatFn::new(
r.numer().clone(),
poly_mul(r.denom(), &x_pow((-k) as usize)),
)
}
};
let mut ms = vec![vec![RatFn::int(0); d]; d];
for (k, row) in ms.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell = shift(&m[k][j], s[k] - s[j]);
if k == j && s[k] != 0 {
let den_x = poly_mul(cell.denom(), &x_pow(1));
let num = poly_sub(
&poly_mul(cell.numer(), &x_pow(1)),
&poly_scale(cell.denom(), &Rational::from(s[k])),
);
*cell = RatFn::new(num, den_x);
}
}
}
let cs: Vec<RatFn> = (0..d).map(|k| shift(&c[k], s[k])).collect();
(ms, cs)
}
fn unsheared_infinity_bound(m: &[Vec<RatFn>], c: &[RatFn], d: usize) -> Result<i64, RdeDecline> {
let Some(gamma) = c.iter().filter_map(ratfn_deg_inf).max() else {
return Ok(0);
};
let Some(rho) = m.iter().flatten().filter_map(ratfn_deg_inf).max() else {
return Ok((gamma + 1).max(0)); };
if rho <= -2 {
return Ok((gamma + 1).max(0)); }
let mut lead = vec![vec![Rational::from(0); d]; d];
for (k, row) in lead.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
if ratfn_deg_inf(&m[k][j]) == Some(rho) {
*cell = ratfn_lead(&m[k][j]);
}
}
}
if rho >= 0 {
if qmat_det(&lead, d) == 0 {
return Err(RdeDecline::AlgebraicBoundNotProved);
}
return Ok(gamma - rho);
}
let bound = qmat_norm_bound(&lead, d);
let k_max = bound.floor().numer().to_i64().unwrap_or(i64::MAX);
if k_max > MAX_ALG_RESONANCE_SEARCH {
return Err(RdeDecline::ResonanceSearchTooLarge);
}
let mut best = (gamma + 1).max(0);
for k in 1..=k_max.max(0) {
let mut a = lead.clone();
for (i, row) in a.iter_mut().enumerate() {
row[i] += Rational::from(k);
}
if qmat_det(&a, d) == 0 {
best = best.max(k);
}
}
Ok(best)
}
fn radical_infinity_bound(e: &AlgExtension, f: &AlgElem, g: &AlgElem) -> Option<i64> {
let (n, p) = radical_shape(e)?;
let deg_p = degree(&p);
if deg_p < 1 {
return None;
}
let n_i = n as i64;
if gcd_i64(n_i, deg_p) != 1 {
return None; }
let vval = |el: &AlgElem| -> Option<i64> {
(0..n)
.filter_map(|j| {
el.get(j)
.and_then(ratfn_deg_inf)
.map(|dg| n_i * dg + j as i64 * deg_p)
})
.max()
};
let gamma = vval(g)?; let y_bound = match vval(f) {
None => (gamma + n_i).max(0), Some(phi) if phi > -n_i => gamma - phi,
Some(phi) if phi < -n_i => (gamma + n_i).max(0),
Some(_) => return None, };
Some(y_bound.div_euclid(n_i))
}
fn radical_shape(e: &AlgExtension) -> Option<(usize, QPoly)> {
let n = e.degree();
if n < 2 {
return None;
}
let n = n as usize;
let modulus = e.quotient().modulus();
if modulus.len() != n + 1 || modulus[n] != RatFn::int(1) {
return None;
}
for m in modulus.iter().take(n).skip(1) {
if !m.numer().is_empty() {
return None;
}
}
if degree(modulus[0].denom()) != 0 {
return None;
}
Some((n, poly_scale(modulus[0].numer(), &Rational::from(-1))))
}
pub(crate) fn radical_minpoly_status(n: usize, p: &QPoly) -> MinPolyStatus {
let deg_p = degree(p);
if n < 2 || deg_p < 1 {
return MinPolyStatus::Unknown;
}
let squarefree = degree(&poly_gcd(p, &poly_deriv(p))) < 1;
if squarefree || gcd_i64(n as i64, deg_p) == 1 {
MinPolyStatus::ProvedIrreducible
} else {
MinPolyStatus::Unknown
}
}
pub(crate) fn compositum_minpoly_status(p: &QPoly, q: &QPoly) -> MinPolyStatus {
if !is_square_in_qx(p) && !is_square_in_qx(q) && !is_square_in_qx(&poly_mul(p, q)) {
MinPolyStatus::ProvedIrreducible
} else {
MinPolyStatus::Unknown
}
}
pub(crate) fn nested_radical_minpoly_status(a: &QPoly, b: &QPoly) -> MinPolyStatus {
let disc = poly_sub(&poly_mul(a, a), b);
if !is_square_in_qx(b) && !is_square_in_qx(&disc) {
MinPolyStatus::ProvedIrreducible
} else {
MinPolyStatus::Unknown
}
}
fn is_square_in_qx(p: &QPoly) -> bool {
let dp = degree(p);
if dp < 0 {
return true; }
let lc = p[dp as usize].clone();
if !is_rational_square(&lc) {
return false;
}
if dp == 0 {
return true;
}
let inv = Rational::from(1) / lc;
poly_sqrt_monic(&poly_scale(p, &inv)).is_some()
}
fn is_rational_square(r: &Rational) -> bool {
*r >= 0 && r.numer().is_perfect_square() && r.denom().is_perfect_square()
}
fn poly_sqrt_monic(p: &QPoly) -> Option<QPoly> {
let dp = degree(p);
if dp < 0 || dp % 2 != 0 {
return None;
}
let m = (dp / 2) as usize;
let mut h = vec![Rational::from(0); m + 1];
h[m] = Rational::from(1);
for i in (0..m).rev() {
let mut s = Rational::from(0);
for j in (i + 1)..m {
let k = m + i - j;
if k > i && k < m {
s += h[j].clone() * h[k].clone();
}
}
let target = p.get(m + i).cloned().unwrap_or_else(|| Rational::from(0));
h[i] = (target - s) / Rational::from(2);
}
if trim(poly_mul(&h, &h)) == trim(p.clone()) {
Some(h)
} else {
None
}
}
fn ratfn_deg_inf(r: &RatFn) -> Option<i64> {
if r.numer().is_empty() {
None
} else {
Some(degree(r.numer()) - degree(r.denom()))
}
}
fn ratfn_lead(r: &RatFn) -> Rational {
let dn = degree(r.numer());
let dd = degree(r.denom());
if dn < 0 || dd < 0 {
return Rational::from(0);
}
r.numer()[dn as usize].clone() / r.denom()[dd as usize].clone()
}
fn qmat_det(m: &[Vec<Rational>], d: usize) -> Rational {
let mut a: Vec<Vec<Rational>> = m.to_vec();
let mut det = Rational::from(1);
for col in 0..d {
let Some(piv) = (col..d).find(|&r| a[r][col] != 0) else {
return Rational::from(0);
};
if piv != col {
a.swap(piv, col);
det = -det;
}
det *= a[col][col].clone();
let inv = Rational::from(1) / a[col][col].clone();
for r in (col + 1)..d {
if a[r][col] == 0 {
continue;
}
let factor = a[r][col].clone() * inv.clone();
let pivot_row: Vec<Rational> = a[col][col..d].to_vec();
for (dst, pv) in a[r][col..d].iter_mut().zip(pivot_row.iter()) {
*dst -= pv.clone() * factor.clone();
}
}
}
det
}
fn qmat_norm_bound(m: &[Vec<Rational>], d: usize) -> Rational {
let mut rows = vec![Rational::from(0); d];
let mut cols = vec![Rational::from(0); d];
for (i, row) in m.iter().enumerate() {
for (j, cell) in row.iter().enumerate() {
let a = cell.clone().abs();
rows[i] += a.clone();
cols[j] += a;
}
}
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 poly_matrix_det(m: &[Vec<QPoly>], d: usize) -> QPoly {
if d == 0 {
return poly_one();
}
let mut bound = 0i64;
for row in m.iter().take(d) {
let r = row.iter().take(d).map(degree).max().unwrap_or(-1);
if r < 0 {
return poly_zero(); }
bound += r;
}
let npts = bound.max(0) as usize + 1;
let xs: Vec<Rational> = (0..npts).map(|i| Rational::from(i as i64)).collect();
let ys: Vec<Rational> = xs
.iter()
.map(|t| {
let ev: Vec<Vec<Rational>> = m
.iter()
.take(d)
.map(|row| row.iter().take(d).map(|p| poly_eval(p, t)).collect())
.collect();
qmat_det(&ev, d)
})
.collect();
lagrange_interpolate(&xs, &ys)
}
fn poly_eval(p: &QPoly, t: &Rational) -> Rational {
let mut acc = Rational::from(0);
for c in p.iter().rev() {
acc = acc * t.clone() + c.clone();
}
acc
}
fn lagrange_interpolate(xs: &[Rational], ys: &[Rational]) -> QPoly {
let n = xs.len();
let mut acc = poly_zero();
for i in 0..n {
let mut num = poly_one();
let mut den = Rational::from(1);
for j in 0..n {
if i == j {
continue;
}
num = poly_mul(&num, &vec![-xs[j].clone(), Rational::from(1)]);
den *= xs[i].clone() - xs[j].clone();
}
let scale = ys[i].clone() / den;
acc = poly_add(&acc, &poly_scale(&num, &scale));
}
trim(acc)
}
fn poly_inverse_mod(a: &QPoly, m: &QPoly) -> Option<QPoly> {
let mut r0 = trim(m.clone());
let mut r1 = poly_divrem(a, m).1;
let mut t0 = poly_zero();
let mut t1 = poly_one();
while !r1.is_empty() {
let (q, r) = poly_divrem(&r0, &r1);
let t2 = poly_sub(&t0, &poly_mul(&q, &t1));
r0 = r1;
r1 = trim(r);
t0 = t1;
t1 = t2;
}
if degree(&r0) != 0 {
return None;
}
let inv = Rational::from(1) / r0[0].clone();
Some(poly_divrem(&poly_scale(&t0, &inv), m).1)
}
fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
a = a.abs();
b = b.abs();
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
fn candidate_denominators(e: &AlgExtension, f: &AlgElem, g: &AlgElem, d: usize) -> Vec<QPoly> {
let mut base = poly_one();
let gen = e.generator();
for j in 0..d {
if let Some(aj) = e.pow(&gen, j as i64) {
for c in e.derivation(&aj) {
base = poly_lcm(&base, c.denom());
}
}
}
for c in f {
base = poly_lcm(&base, c.denom());
}
for c in g {
base = poly_lcm(&base, c.denom());
}
let base2 = poly_mul(&base, &base);
let base3 = poly_mul(&base2, &base);
let mut out = vec![poly_one(), base, base2, base3];
out.dedup_by(|a, b| trim(a.clone()) == trim(b.clone()));
out
}
enum DenSolve {
Solved(AlgElem),
Inconsistent,
VerificationFailed,
}
fn solve_with_denominator_outcome(
e: &AlgExtension,
f: &AlgElem,
g: &AlgElem,
den: &QPoly,
ncap: usize,
d: usize,
) -> DenSolve {
let basis: Vec<(usize, usize)> = (0..d)
.flat_map(|j| (0..=ncap).map(move |m| (j, m)))
.collect();
let elems: Vec<AlgElem> = basis
.iter()
.map(|&(j, m)| {
let coeff = RatFn::new(x_pow(m), den.clone()); let mut v = vec![RatFn::int(0); d];
v[j] = coeff;
e.reduce(&v)
})
.collect();
let cols: Vec<AlgElem> = elems
.iter()
.map(|m| e.add(&e.derivation(m), &e.mul(f, m)))
.collect();
let (matrix, rhs) = extract_linear_system(&cols, g, d);
let Some(sol) = gauss_solve(matrix, rhs, basis.len()) else {
return DenSolve::Inconsistent;
};
let mut y = e.from_int(0);
for (idx, elem) in elems.iter().enumerate() {
if sol[idx] != 0 {
let s = e.constant(RatFn::from_poly(&vec![sol[idx].clone()]));
y = e.add(&y, &e.mul(&s, elem));
}
}
let lhs = e.add(&e.derivation(&y), &e.mul(f, &y));
if e.elem_eq(&lhs, g) {
DenSolve::Solved(y)
} else {
DenSolve::VerificationFailed
}
}
#[cfg(test)]
fn solve_with_denominator(
e: &AlgExtension,
f: &AlgElem,
g: &AlgElem,
den: &QPoly,
ncap: usize,
d: usize,
) -> Option<AlgElem> {
match solve_with_denominator_outcome(e, f, g, den, ncap, d) {
DenSolve::Solved(y) => Some(y),
_ => None,
}
}
fn extract_linear_system(
cols: &[AlgElem],
target: &AlgElem,
d: usize,
) -> (Vec<Vec<Rational>>, Vec<Rational>) {
let comp =
|a: &AlgElem, k: usize| -> RatFn { a.get(k).cloned().unwrap_or_else(|| RatFn::int(0)) };
let mut matrix: Vec<Vec<Rational>> = Vec::new();
let mut rhs: Vec<Rational> = Vec::new();
for k in 0..d {
let col_rf: Vec<RatFn> = cols.iter().map(|c| comp(c, k)).collect();
let tgt_rf = comp(target, k);
let mut d_x = poly_one();
for r in &col_rf {
d_x = poly_lcm(&d_x, r.denom());
}
d_x = poly_lcm(&d_x, tgt_rf.denom());
let s_cols: Vec<QPoly> = col_rf
.iter()
.map(|r| poly_mul(r.numer(), &poly_div_exact(&d_x, r.denom())))
.collect();
let s_tgt = poly_mul(tgt_rf.numer(), &poly_div_exact(&d_x, tgt_rf.denom()));
let max_m = s_cols
.iter()
.map(|s| s.len())
.chain(std::iter::once(s_tgt.len()))
.max()
.unwrap_or(0);
for m in 0..max_m {
matrix.push(
s_cols
.iter()
.map(|s| s.get(m).cloned().unwrap_or_else(|| Rational::from(0)))
.collect(),
);
rhs.push(s_tgt.get(m).cloned().unwrap_or_else(|| Rational::from(0)));
}
}
(matrix, rhs)
}
fn gauss_solve(
mut m: Vec<Vec<Rational>>,
mut b: Vec<Rational>,
ncols: usize,
) -> Option<Vec<Rational>> {
let nrows = m.len();
let mut pivot_row_of_col: Vec<Option<usize>> = vec![None; ncols];
let mut row = 0usize;
for col in 0..ncols {
if row >= nrows {
break;
}
let Some(sel) = (row..nrows).find(|&r| m[r][col] != 0) else {
continue;
};
m.swap(row, sel);
b.swap(row, sel);
let piv = m[row][col].clone();
for v in m[row].iter_mut() {
*v = v.clone() / piv.clone();
}
b[row] = b[row].clone() / piv.clone();
let pivot_row = m[row].clone();
let pivot_b = b[row].clone();
for r in 0..nrows {
if r != row && m[r][col] != 0 {
let factor = m[r][col].clone();
for (dst, pv) in m[r].iter_mut().zip(pivot_row.iter()) {
*dst -= factor.clone() * pv.clone();
}
b[r] -= factor * pivot_b.clone();
}
}
pivot_row_of_col[col] = Some(row);
row += 1;
}
for r in 0..nrows {
if m[r].iter().all(|v| *v == 0) && b[r] != 0 {
return None;
}
}
let mut x = vec![Rational::from(0); ncols];
for (col, pr) in pivot_row_of_col.iter().enumerate() {
if let Some(r) = pr {
x[col] = b[*r].clone();
}
}
Some(x)
}
fn poly_lcm(a: &QPoly, b: &QPoly) -> QPoly {
let g = poly_gcd(a, b);
poly_div_exact(&poly_mul(a, b), &g)
}
fn x_pow(m: usize) -> QPoly {
let mut p = vec![Rational::from(0); m + 1];
p[m] = Rational::from(1);
p
}
#[cfg(test)]
mod tests {
use super::*;
use crate::integrate::risch::poly_rde::poly_scale;
fn rat(n: i64) -> Rational {
Rational::from(n)
}
#[test]
fn cyclic_sqrt_recovers_solution() {
let e = AlgExtension::radical(2, &vec![rat(0), rat(1)]); let g: AlgElem = vec![
RatFn::int(0),
RatFn::from_poly(&vec![Rational::from((3, 2))]),
];
let f = RatFn::int(0);
let y = solve_alg_rde(&e, &f, &g).expect("should solve");
assert!(e.elem_eq(&e.derivation(&y), &g));
let expected: AlgElem = vec![RatFn::int(0), RatFn::from_poly(&vec![rat(0), rat(1)])];
assert!(e.elem_eq(&y, &expected), "y = {:?}", y);
}
#[test]
fn noncyclic_compositum_pure_antiderivative() {
let q: Vec<QPoly> = vec![
poly_one(), Vec::new(), poly_scale(&vec![rat(1), rat(2)], &rat(-2)), Vec::new(), poly_one(), ];
let e = AlgExtension::new(&q);
assert_eq!(e.degree(), 4);
let alpha = e.generator();
let g = e.derivation(&alpha); let f = RatFn::int(0);
let y = solve_alg_rde(&e, &f, &g).expect("coupled RDE should solve");
assert!(
e.elem_eq(&e.derivation(&y), &g),
"D(y) must equal g; y = {y:?}"
);
}
#[test]
fn noncyclic_compositum_with_f() {
let q: Vec<QPoly> = vec![
poly_one(),
Vec::new(),
poly_scale(&vec![rat(1), rat(2)], &rat(-2)),
Vec::new(),
poly_one(),
];
let e = AlgExtension::new(&q);
let alpha = e.generator();
let f = RatFn::new(poly_one(), vec![rat(0), rat(1)]); let f_elem = e.constant(f.clone());
let g = e.add(&e.derivation(&alpha), &e.mul(&f_elem, &alpha));
let y = solve_alg_rde(&e, &f, &g).expect("coupled RDE with f should solve");
let lhs = e.add(&e.derivation(&y), &e.mul(&f_elem, &y));
assert!(e.elem_eq(&lhs, &g), "D(y)+f·y must equal g; y = {y:?}");
}
#[test]
fn unsolvable_log_returns_none() {
let q: Vec<QPoly> = vec![
poly_one(),
Vec::new(),
poly_scale(&vec![rat(1), rat(2)], &rat(-2)),
Vec::new(),
poly_one(),
];
let e = AlgExtension::new(&q);
let g = e.constant(RatFn::new(poly_one(), vec![rat(0), rat(1)]));
let f = RatFn::int(0);
assert!(solve_alg_rde(&e, &f, &g).is_none());
}
#[test]
fn nonbase_f_sqrt_coupled() {
let e = AlgExtension::radical(2, &vec![rat(0), rat(1)]); let alpha = e.generator();
let f: AlgElem = vec![RatFn::int(0), RatFn::new(poly_one(), vec![rat(0), rat(2)])];
let y_true = alpha.clone();
let g = e.add(&e.derivation(&y_true), &e.mul(&f, &y_true));
let y = solve_alg_rde_general(&e, &f, &g).expect("non-base f coupled solve");
let lhs = e.add(&e.derivation(&y), &e.mul(&f, &y));
assert!(e.elem_eq(&lhs, &g), "D(y)+f·y must equal g; y = {y:?}");
assert!(e.elem_eq(&y, &y_true), "expected y = √x; got {y:?}");
}
#[test]
fn nonbase_f_cbrt_coupled() {
let e = AlgExtension::radical(3, &vec![rat(0), rat(1)]); let f: AlgElem = vec![
RatFn::int(0),
RatFn::int(0),
RatFn::new(poly_one(), vec![rat(0), rat(1)]),
];
let y_true = e.constant(RatFn::from_poly(&vec![rat(0), rat(1)]));
let g = e.add(&e.derivation(&y_true), &e.mul(&f, &y_true));
let y = solve_alg_rde_general(&e, &f, &g).expect("non-base cbrt f coupled solve");
let lhs = e.add(&e.derivation(&y), &e.mul(&f, &y));
assert!(e.elem_eq(&lhs, &g), "D(y)+f·y must equal g; y = {y:?}");
assert!(e.elem_eq(&y, &y_true), "expected y = x; got {y:?}");
}
#[test]
fn base_f_wrapper_matches_general() {
let e = AlgExtension::radical(2, &vec![rat(0), rat(1)]); let alpha = e.generator();
let f = RatFn::new(poly_one(), vec![rat(0), rat(1)]); let f_elem = e.constant(f.clone());
let g = e.add(&e.derivation(&alpha), &e.mul(&f_elem, &alpha));
let via_wrapper = solve_alg_rde(&e, &f, &g);
let via_general = solve_alg_rde_general(&e, &f_elem, &g);
match (&via_wrapper, &via_general) {
(Some(a), Some(b)) => assert!(e.elem_eq(a, b), "wrapper vs general differ"),
(None, None) => {}
_ => panic!("wrapper vs general disagree on solvability"),
}
assert!(via_wrapper.is_some(), "base-f case should solve");
}
#[test]
fn nonbase_f_unsolvable_returns_none() {
let e = AlgExtension::radical(2, &vec![rat(0), rat(1)]); let f = e.generator(); let g = e.constant(RatFn::new(poly_one(), vec![rat(0), rat(1)])); assert!(solve_alg_rde_general(&e, &f, &g).is_none());
}
#[test]
fn high_degree_solution_recovered_by_analytic_bound() {
let e = AlgExtension::radical(2, &vec![rat(0), rat(1)]); let mut p8 = vec![rat(0); 9];
p8[8] = rat(1);
let y_true: AlgElem = vec![RatFn::int(0), RatFn::from_poly(&p8)];
let f_elem = e.constant(RatFn::int(1)); let g = e.add(&e.derivation(&y_true), &e.mul(&f_elem, &y_true));
let bound = alg_x_degree_bound(&e, &f_elem, &g);
assert!(
bound >= 8,
"analytic bound {bound} must cover the degree-8 solution"
);
assert!(
bound > DEG_CAP,
"bound {bound} must exceed the old fixed DEG_CAP {DEG_CAP}"
);
assert!(
solve_with_denominator(&e, &f_elem, &g, &poly_one(), DEG_CAP, 2).is_none(),
"degree-{DEG_CAP} ansatz must NOT contain the degree-8 solution"
);
let y = solve_alg_rde_general(&e, &f_elem, &g)
.expect("analytic bound must recover the high-degree solution");
let lhs = e.add(&e.derivation(&y), &e.mul(&f_elem, &y));
assert!(e.elem_eq(&lhs, &g), "D(y)+f·y must equal g; y = {y:?}");
assert!(e.elem_eq(&y, &y_true), "expected y = x⁸·α; got {y:?}");
}
}
#[cfg(test)]
mod proof_tests {
use super::*;
use crate::integrate::risch::poly_rde::{poly_add, poly_scale};
fn rat(n: i64) -> Rational {
Rational::from(n)
}
fn compositum(p: &QPoly, q: &QPoly) -> AlgExtension {
let pq_sum = poly_add(p, q);
let pq_diff = poly_sub(p, q);
AlgExtension::new(&[
poly_mul(&pq_diff, &pq_diff),
poly_zero(),
poly_scale(&pq_sum, &rat(-2)),
poly_zero(),
poly_one(),
])
}
fn nested(a: &QPoly, b: &QPoly) -> AlgExtension {
AlgExtension::new(&[
poly_sub(&poly_mul(a, a), b),
poly_zero(),
poly_scale(a, &rat(-2)),
poly_zero(),
poly_one(),
])
}
#[test]
fn exp_sqrt_over_x_non_existence_is_proved() {
let p = vec![rat(0), rat(1)];
let e = AlgExtension::radical(2, &p);
let f: AlgElem = vec![RatFn::int(0), RatFn::new(poly_one(), vec![rat(0), rat(2)])];
let g = e.constant(RatFn::new(poly_one(), vec![rat(0), rat(1)]));
let m = system_matrix(&e, &f, 2).unwrap();
let c: Vec<RatFn> = (0..2)
.map(|k| g.get(k).cloned().unwrap_or_else(|| RatFn::int(0)))
.collect();
assert!(
system_infinity_bound(&m, &c, 2).is_err(),
"the matrix bound is expected to be unavailable (singular leading term)"
);
assert_eq!(radical_infinity_bound(&e, &f, &g), Some(-1));
let out = solve_alg_rde_general_checked(&e, &f, &g, radical_minpoly_status(2, &p));
assert_eq!(out, AlgRdeOutcome::NoRationalSolution, "got {out:?}");
}
#[test]
fn compositum_non_existence_is_proved() {
let p = vec![rat(0), rat(1)];
let q = vec![rat(1), rat(1)];
let e = compositum(&p, &q);
let g = e.generator();
let out = solve_alg_rde_checked(&e, &RatFn::int(1), &g, compositum_minpoly_status(&p, &q));
assert_eq!(out, AlgRdeOutcome::NoRationalSolution, "got {out:?}");
}
#[test]
fn nested_radical_non_existence_is_proved() {
let a = vec![rat(0), rat(1)];
let b = vec![rat(0), rat(1)];
let e = nested(&a, &b);
let g = e.generator();
let out = solve_alg_rde_checked(
&e,
&RatFn::int(1),
&g,
nested_radical_minpoly_status(&a, &b),
);
assert_eq!(out, AlgRdeOutcome::NoRationalSolution, "got {out:?}");
}
#[test]
fn unknown_minpoly_can_never_prove_non_existence() {
let p = vec![rat(0), rat(1)];
let q = vec![rat(1), rat(1)];
let e = compositum(&p, &q);
let g = e.generator();
let out = solve_alg_rde_checked(&e, &RatFn::int(1), &g, MinPolyStatus::Unknown);
assert_eq!(
out,
AlgRdeOutcome::Declined(RdeDecline::AlgebraicFieldNotProved),
"got {out:?}"
);
assert!(!out.proves_no_solution());
}
#[test]
fn solvable_compositum_still_solves() {
let p = vec![rat(0), rat(1)];
let q = vec![rat(1), rat(1)];
let e = compositum(&p, &q);
let alpha = e.generator();
let f_elem = e.constant(RatFn::int(1));
let g = e.add(&e.derivation(&alpha), &e.mul(&f_elem, &alpha));
let out = solve_alg_rde_checked(&e, &RatFn::int(1), &g, compositum_minpoly_status(&p, &q));
let y = out.solution().expect("D(α)+α is solved by y = α");
let lhs = e.add(&e.derivation(&y), &e.mul(&f_elem, &y));
assert!(e.elem_eq(&lhs, &g));
}
#[test]
fn zero_rhs_solves_rather_than_certifying() {
let p = vec![rat(0), rat(1)];
let e = AlgExtension::radical(2, &p);
let g = e.from_int(0);
let out = solve_alg_rde_checked(&e, &RatFn::int(1), &g, radical_minpoly_status(2, &p));
assert!(matches!(out, AlgRdeOutcome::Solved(_)), "got {out:?}");
}
#[test]
fn minpoly_status_recognises_the_reachable_shapes() {
assert_eq!(
radical_minpoly_status(2, &vec![rat(0), rat(1)]),
MinPolyStatus::ProvedIrreducible
);
assert_eq!(
radical_minpoly_status(2, &vec![rat(0), rat(0), rat(1)]),
MinPolyStatus::Unknown
);
assert_eq!(
radical_minpoly_status(3, &vec![rat(0), rat(0), rat(1)]),
MinPolyStatus::ProvedIrreducible
);
assert_eq!(
compositum_minpoly_status(&vec![rat(0), rat(1)], &vec![rat(1), rat(1)]),
MinPolyStatus::ProvedIrreducible
);
assert_eq!(
compositum_minpoly_status(&vec![rat(0), rat(1)], &vec![rat(0), rat(0), rat(0), rat(1)]),
MinPolyStatus::Unknown
);
assert_eq!(
nested_radical_minpoly_status(&vec![rat(0), rat(1)], &vec![rat(0), rat(1)]),
MinPolyStatus::ProvedIrreducible
);
assert_eq!(
nested_radical_minpoly_status(&vec![rat(0), rat(1)], &vec![rat(0), rat(0), rat(1)]),
MinPolyStatus::Unknown
);
}
#[test]
fn square_detection_over_qx() {
assert!(is_square_in_qx(&vec![rat(1), rat(2), rat(1)])); assert!(is_square_in_qx(&vec![rat(4), rat(8), rat(4)])); assert!(!is_square_in_qx(&vec![rat(2), rat(4), rat(2)])); assert!(!is_square_in_qx(&vec![rat(0), rat(1)])); assert!(!is_square_in_qx(&vec![rat(-1), rat(0), rat(1)])); assert!(is_square_in_qx(&vec![rat(9)])); assert!(!is_square_in_qx(&vec![rat(3)])); }
#[test]
fn polynomial_determinant_matches_hand_computation() {
let m = vec![
vec![vec![rat(0), rat(1)], poly_one()],
vec![vec![rat(0), rat(0), rat(1)], vec![rat(0), rat(1)]],
];
assert_eq!(trim(poly_matrix_det(&m, 2)), poly_zero());
let m = vec![
vec![vec![rat(1), rat(1)], vec![rat(2)]],
vec![vec![rat(3)], vec![rat(0), rat(1)]],
];
assert_eq!(trim(poly_matrix_det(&m, 2)), vec![rat(-6), rat(1), rat(1)]);
}
#[test]
fn polynomial_inverse_mod_round_trips() {
let m = vec![rat(-2), rat(0), rat(1)]; let a = vec![rat(1), rat(1)]; let inv = poly_inverse_mod(&a, &m).expect("x+1 is a unit mod x²−2");
let prod = poly_divrem(&poly_mul(&a, &inv), &m).1;
assert_eq!(trim(prod), poly_one());
assert!(poly_inverse_mod(&m, &m).is_none());
}
}
#[cfg(test)]
mod engine_verdict_tests {
use crate::errors::AlkahestError;
use crate::integrate::engine::{integrate, IntegrationError};
use crate::kernel::{Domain, ExprId, ExprPool};
use std::collections::HashMap;
fn run(src: &str) -> (ExprPool, ExprId, Result<ExprId, IntegrationError>) {
let pool = ExprPool::new();
let mut syms = HashMap::new();
let e = crate::parse::parse(src, &pool, &mut syms).expect("parse");
let x = pool.symbol("x", Domain::Real);
let r = integrate(e, x, &pool).map(|d| d.value);
(pool, e, r)
}
fn assert_certified(src: &str) {
let (_pool, _e, r) = run(src);
assert!(
matches!(r, Err(IntegrationError::NonElementary(_))),
"∫ {src} dx must stay certified E-INT-004; got {r:?}"
);
}
fn assert_not_certified(src: &str) {
let (_pool, _e, r) = run(src);
assert!(
!matches!(r, Err(IntegrationError::NonElementary(_))),
"∫ {src} dx must not be certified E-INT-004; got {r:?}"
);
}
#[test]
fn verdict_exp_sqrt_over_x_stays_certified() {
assert_certified("exp(sqrt(x))/x");
}
#[test]
fn verdict_exp_sqrt_x2_plus_1_stays_certified() {
assert_certified("exp(sqrt(x^2+1))");
}
#[test]
fn verdict_compositum_times_exp_stays_certified() {
assert_certified("(sqrt(x)+sqrt(x+1))*exp(x)");
}
#[test]
fn verdict_nested_radical_times_exp_stays_certified() {
assert_certified("sqrt(x+sqrt(x))*exp(x)");
}
#[test]
fn neighbouring_verdicts_stay_certified() {
for src in [
"exp(sqrt(x))/x^2",
"exp(sqrt(x))/(x+1)",
"exp(sqrt(x))/(x^2+1)",
"exp(sqrt(x))/(x-1)",
"exp(sqrt(x+1))/x",
"exp(sqrt(x^2+1))/x",
"(sqrt(x)-sqrt(x+1))*exp(x)",
"sqrt(x)*sqrt(x+1)*exp(x)",
"(sqrt(x)+sqrt(x^2+1))*exp(x)",
"(sqrt(x+1)+sqrt(x+2))*exp(x)",
"(sqrt(x)+sqrt(x+1))*exp(2*x)",
"(sqrt(x)+sqrt(x+1))*exp(x^2)",
"sqrt(x+sqrt(x^2+1))*exp(x)",
"sqrt(x+1+sqrt(x))*exp(x)",
"sqrt(x+sqrt(x))*exp(2*x)",
"sqrt(x^2+sqrt(x))*exp(x)",
"exp(x)/sqrt(x+sqrt(x))",
] {
assert_certified(src);
}
}
#[test]
fn solved_integrals_stay_solved() {
for src in [
"exp(sqrt(x))",
"exp(sqrt(x))*(1/(2*sqrt(x))+1/2)",
"x*exp(sqrt(x))",
"sqrt(x)*exp(sqrt(x))",
"exp(sqrt(x))/sqrt(x)",
"(sqrt(x)+1/(2*sqrt(x)))*exp(x)",
"(sqrt(x)+sqrt(x+1)+1/(2*sqrt(x))+1/(2*sqrt(x+1)))*exp(x)",
] {
let (_pool, _e, r) = run(src);
assert!(r.is_ok(), "∫ {src} dx should still solve; got {r:?}");
}
}
#[test]
fn degenerate_compositum_no_longer_falsely_certifies() {
assert_not_certified("sqrt(x)*exp(x) + exp(x)/sqrt(4*x)");
assert_not_certified("sqrt(9*x)*exp(x) + 3*exp(x)/sqrt(4*x)");
}
#[test]
fn degenerate_compositum_witness_is_elementary() {
let pool = ExprPool::new();
let mut syms = HashMap::new();
let x = pool.symbol("x", Domain::Real);
let integrand = crate::parse::parse("sqrt(x)*exp(x) + exp(x)/sqrt(4*x)", &pool, &mut syms)
.expect("parse");
let antideriv = crate::parse::parse("sqrt(x)*exp(x)", &pool, &mut syms).expect("parse");
let d = crate::diff::diff(antideriv, x, &pool).expect("diff");
let ds = crate::simplify::engine::simplify(d.value, &pool).value;
for &xv in &[0.4_f64, 1.1, 2.3, 5.7] {
let bindings = HashMap::from([(x, xv)]);
let lhs = crate::eval::eval_f64(ds, &pool, &bindings).expect("eval d/dx F");
let rhs = crate::eval::eval_f64(integrand, &pool, &bindings).expect("eval f");
assert!(
(lhs - rhs).abs() <= 1e-9 * (1.0 + rhs.abs()),
"x={xv}: d/dx(√x·eˣ) = {lhs} but the integrand is {rhs}"
);
}
}
#[test]
fn a_declined_algebraic_rde_reports_e_int_001_with_a_reason() {
for src in [
"(sqrt(x)+sqrt(4*x))*exp(x)",
"(sqrt(x)+sqrt(x^3))*exp(x)",
"sqrt(x)*exp(x) + exp(x)/sqrt(4*x)",
] {
let (_pool, _e, r) = run(src);
let Err(err) = r else {
continue; };
assert_eq!(
err.code(),
"E-INT-001",
"∫ {src} dx: a decline must be E-INT-001; got {err}"
);
assert!(
format!("{err}").contains("could not be decided"),
"∫ {src} dx: the decline must name its reason; got {err}"
);
}
}
}