use ocas_domain::{Domain, Rational, RationalDomain};
use ocas_poly::DenseUnivariatePolynomial;
use crate::tower::build::{Tower, tower_diff_kpoly};
use crate::tower::elem::{KElem, KPoly};
type DPoly = DenseUnivariatePolynomial<RationalDomain>;
const MAX_RT_DEGREE: usize = 8;
const MAX_RT_CANDIDATES: usize = 64;
const MAX_CONTENT: i64 = 1_000_000_000;
pub(crate) fn rothstein_trager(
tower: &Tower,
level: usize,
a1: &KPoly,
d1: &KPoly,
) -> Option<(Vec<(Rational, KElem)>, KElem)> {
let top = level;
let n = d1.n_vars;
let deg_d = d1.degree()?;
if deg_d == 0 || deg_d > MAX_RT_DEGREE {
return None;
}
let lc = d1.lc();
let (a1m, d1m) = if lc.eq_cross(&KElem::one(n)) {
(a1.clone(), d1.clone())
} else {
let lc_inv = lc.inv()?;
(a1.mul_kelem(&lc_inv), d1.monic())
};
if !d1m.gcd(&a1m).is_one() {
return None;
}
let dd1m = clean_kpoly(&tower_diff_kpoly(
&d1m,
&tower.gens[..level - 1],
&tower.gens[level - 1].dt,
))?;
let r_poly = rt_resultant_polynomial(&a1m, &d1m, &dd1m)?;
let roots = rational_roots_complete(&r_poly)?;
let mut v_polys: Vec<KPoly> = Vec::with_capacity(roots.len());
for c in &roots {
let shifted = a1m.sub(&dd1m.mul_kelem(&KElem::from_rational(c, n)));
let v = d1m.gcd(&shifted).monic();
if v.degree()? == 0 {
return None;
}
v_polys.push(v);
}
let mut prod = KPoly::one(top, n);
for v in &v_polys {
prod = prod.mul(v);
}
if !prod.sub(&d1m).is_zero() {
return None;
}
let mut rem = a1m.kelem().div(&d1m.kelem())?;
let mut logs_out: Vec<(Rational, KElem)> = Vec::with_capacity(v_polys.len());
for (c, v) in roots.iter().zip(v_polys.iter()) {
let dv = tower_diff_kpoly(v, &tower.gens[..level - 1], &tower.gens[level - 1].dt);
let term = dv.kelem().div(&v.kelem())?.mul_rational(c);
rem = rem.sub(&term);
logs_out.push((c.clone(), v.kelem()));
}
if rem.num.degree_in(top) > 0 || rem.den.degree_in(top) > 0 {
return None;
}
Some((logs_out, rem))
}
pub(crate) fn clean_kpoly(p: &KPoly) -> Option<KPoly> {
let top = p.top;
let e = p.kelem();
if e.den.degree_in(top) > 0 {
return None;
}
let out = KPoly::from_sparse(&e.num, top);
let den_inv = KElem::from_poly(e.den.clone()).inv()?;
Some(out.mul_kelem(&den_inv))
}
pub(crate) fn rt_resultant_polynomial(a1: &KPoly, d1: &KPoly, dd1: &KPoly) -> Option<Vec<KElem>> {
let n = d1.n_vars;
let deg_d = d1.degree()?;
let template_deg = a1.degree().unwrap_or(0).max(dd1.degree().unwrap_or(0));
let mut nodes: Vec<i64> = Vec::with_capacity(deg_d + 1);
let mut values: Vec<KElem> = Vec::with_capacity(deg_d + 1);
let mut j: i64 = 0;
while values.len() < deg_d + 1 {
if j > 4 * (deg_d as i64 + 2) {
return None;
}
let u = a1.sub(&dd1.mul_kelem(&KElem::from_rational(&Rational::new(j, 1), n)));
j += 1;
if u.degree() != Some(template_deg) {
continue; }
nodes.push(j - 1);
values.push(u.resultant(d1));
}
let mut out = vec![KElem::zero(n); deg_d + 1];
for (idx, y) in values.iter().enumerate() {
let zj = nodes[idx];
let mut basis = vec![Rational::new(1, 1)];
let mut denom: i64 = 1;
for (i, &zi) in nodes.iter().enumerate() {
if i == idx {
continue;
}
let mut next = vec![RationalDomain.zero(); basis.len() + 1];
for (k, c) in basis.iter().enumerate() {
let term = RationalDomain.mul(c, &Rational::new(zi, 1));
next[k] = RationalDomain.sub(&next[k], &term);
next[k + 1] = RationalDomain.add(&next[k + 1], c);
}
basis = next;
denom = denom.checked_mul(zj - zi)?;
}
for (k, c) in basis.iter().enumerate() {
let scale = RationalDomain.div(c, &Rational::new(denom, 1))?;
out[k] = out[k].add(&y.mul_rational(&scale));
}
}
while out.last().is_some_and(|c| c.is_zero()) {
out.pop();
}
if out.is_empty() {
return None;
}
Some(out)
}
pub(crate) fn rational_root_candidates(r: &[KElem]) -> Option<Vec<Rational>> {
if let Some(qcoeffs) = r
.iter()
.map(|c| c.as_rational())
.collect::<Option<Vec<_>>>()
{
let (roots, _fully_split) =
super::rational::rational_roots(&DPoly::from_coeffs(RationalDomain, qcoeffs))?;
return Some(roots);
}
let coeffs: Vec<(&crate::tower::elem::Sparse, &crate::tower::elem::Sparse)> =
r.iter().map(|e| (&e.num, &e.den)).collect();
let mut lcm: i64 = 1;
for (num, den) in &coeffs {
for c in num.terms_ref().values().chain(den.terms_ref().values()) {
let d = c.denom().to_i64()?;
lcm = num_integer::lcm(lcm, d);
if lcm > MAX_CONTENT {
return None;
}
}
}
let content = |p: &crate::tower::elem::Sparse| -> Option<i64> {
let mut g: i64 = 0;
for c in p.terms_ref().values() {
let v = RationalDomain.mul(c, &Rational::new(lcm, 1));
let v = v.numer().to_i64()?;
g = num_integer::gcd(g, v.checked_abs()?);
}
Some(g)
};
let m = r.len() - 1;
let mut g0 = content(coeffs[0].0)?;
let mut gm = content(coeffs[m].0)?;
for (i, (_, den)) in coeffs.iter().enumerate() {
let cd = content(den)?;
if i != 0 {
g0 = g0.checked_mul(cd)?;
}
if i != m {
gm = gm.checked_mul(cd)?;
}
}
if g0 == 0 || gm == 0 || g0 > MAX_CONTENT || gm > MAX_CONTENT {
return None;
}
let mut candidates: Vec<Rational> = Vec::new();
let divs = |g: i64| -> Vec<i64> {
let mut out = Vec::new();
let mut d = 1i64;
while d <= g / d {
if g % d == 0 {
out.push(d);
if d != g / d {
out.push(g / d);
}
}
d += 1;
}
out
};
for p in divs(g0) {
for q in divs(gm) {
for sign in [1, -1] {
let c = Rational::new(sign * p, q);
if !candidates.contains(&c) {
candidates.push(c);
}
}
}
}
if candidates.len() > MAX_RT_CANDIDATES {
return None;
}
Some(candidates)
}
pub(crate) fn rational_roots_complete(r: &[KElem]) -> Option<Vec<Rational>> {
let mut r: Vec<KElem> = r.to_vec();
while r.first().is_some_and(|c| c.is_zero()) {
r.remove(0);
}
if r.is_empty() {
return Some(Vec::new());
}
let candidates = rational_root_candidates(&r)?;
let mut cur = r.clone();
let mut roots: Vec<Rational> = Vec::new();
for c in candidates {
loop {
let mut acc = cur.last()?.clone();
for i in (0..cur.len() - 1).rev() {
acc = acc.mul_rational(&c).add(&cur[i]);
}
if !acc.is_zero() {
break;
}
let mut quo = vec![KElem::zero(cur[0].n_vars()); cur.len() - 1];
let mut carry = cur.last()?.clone();
for i in (1..cur.len()).rev() {
quo[i - 1] = carry.clone();
carry = cur[i - 1].add(&carry.mul_rational(&c));
}
cur = quo;
if !roots.contains(&c) {
roots.push(c.clone());
}
if cur.is_empty() {
break;
}
}
}
if cur.len() != 1 || cur[0].is_zero() {
return None;
}
Some(roots)
}
#[cfg(test)]
mod tests {
use ocas_atom::{Atom, AtomArena, Symbol};
use ocas_core::arena::Arena;
use super::*;
use crate::tower::build::build_tower;
fn simple_remainder<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
) -> (Tower<'a>, usize, KPoly, KPoly) {
let tower = build_tower(ctx, expr, Symbol::new("x")).expect("tower");
let level = tower.gens.len();
let rf = crate::tower::convert::atom_to_rational_extended(
tower.expr,
&tower.gen_atoms(),
tower.n_vars(),
)
.expect("field element");
let f = crate::tower::elem::KRat::new(
KPoly::from_sparse(&rf.numerator, level),
KPoly::from_sparse(&rf.denominator, level),
);
let (_p, r) = f.num.div_rem(&f.den);
let (_g, a1, d1) =
crate::integral::risch::hermite_tower(&tower, level, &r, &f.den).expect("hermite");
(tower, level, a1, d1)
}
#[test]
fn rt_exp_level_single_root() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let f = ctx.pow(ctx.add(&[ctx.num(1), ctx.fun("exp", &[x])]), ctx.num(-1));
let (tower, level, a1, d1) = simple_remainder(&ctx, f);
let (logs, rem) = rothstein_trager(&tower, level, &a1, &d1).expect("RT");
assert_eq!(logs.len(), 1);
assert_eq!(logs[0].0, Rational::new(-1, 1));
assert!(rem.num.degree_in(level) == 0 && rem.den.degree_in(level) == 0);
}
#[test]
fn rt_exp_level_double_root_once() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let two_x = ctx.mul(&[ctx.num(2), x]);
let f = ctx.pow(
ctx.add(&[ctx.fun("exp", &[two_x]), ctx.num(-1)]),
ctx.num(-1),
);
let (tower, level, a1, d1) = simple_remainder(&ctx, f);
let (logs, rem) = rothstein_trager(&tower, level, &a1, &d1).expect("RT");
assert_eq!(logs.len(), 1);
assert_eq!(logs[0].0, Rational::new(1, 2));
assert!(rem.num.degree_in(level) == 0 && rem.den.degree_in(level) == 0);
}
#[test]
fn rt_log_level_two_roots() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let log_x = ctx.fun("log", &[x]);
let den = ctx.mul(&[x, log_x, ctx.add(&[log_x, ctx.num(1)])]);
let f = ctx.pow(den, ctx.num(-1));
let (tower, level, a1, d1) = simple_remainder(&ctx, f);
let (logs, rem) = rothstein_trager(&tower, level, &a1, &d1).expect("RT");
assert_eq!(logs.len(), 2);
let cs: Vec<Rational> = logs.iter().map(|(c, _)| c.clone()).collect();
assert!(cs.contains(&Rational::new(-1, 1)) && cs.contains(&Rational::new(1, 1)));
assert!(rem.is_zero());
}
#[test]
fn rt_declines_a_non_constant_root() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let f = ctx.pow(ctx.add(&[ctx.fun("exp", &[x]), x]), ctx.num(-1));
let (tower, level, a1, d1) = simple_remainder(&ctx, f);
assert!(rothstein_trager(&tower, level, &a1, &d1).is_none());
}
}