use num_bigint::{BigInt, BigUint};
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
use super::multipoly::{MonomialOrd, MultiPoly};
pub use super::dense::Poly;
pub use super::generic::GenPoly;
pub mod traits {
pub use crate::poly::traits::{
BindingStrength, CoeffDisplay, EuclideanDomain, Field, IntegralCoeff, Ring,
};
}
pub type ModPFactorization = (u64, Vec<(Vec<u64>, u32)>);
pub type MultiFactorization<O> = (Ratio<BigInt>, Vec<(MultiPoly<O>, u32)>);
pub const PRIMES_TO_TRY: usize = 5;
pub const MAX_RECOMBINATION_SUBSETS: usize = 400_000;
const MAX_PRIMES_EXAMINED: usize = 600;
pub const MAX_PRIME: u64 = 1 << 31;
pub const MAX_KRONECKER_SUBSTITUTION_DEGREE: usize = 96;
const MAX_MULTIVARIATE_SUBSETS: usize = 20_000;
#[must_use]
pub fn factor_zassenhaus(f: &Poly) -> Vec<(Poly, u32)> {
factor_zassenhaus_with_content(f).1
}
#[must_use]
pub fn factor_zassenhaus_with_content(f: &Poly) -> (Ratio<BigInt>, Vec<(Poly, u32)>) {
if f.is_zero() {
return (Ratio::zero(), vec![]);
}
if f.is_constant() {
return (f.coeff(0), vec![]);
}
let mut content = f.content();
let mut prim = f.primitive_part();
if prim.leading_coeff().is_some_and(|lc| lc.is_negative()) {
content = -content;
prim = -&prim;
}
let mut all: Vec<(Poly, u32)> = Vec::new();
for (sf, mult) in super::dense::square_free_decomposition(&prim) {
let coeffs = poly_to_z(&sf);
for g in factor_squarefree_z(&coeffs) {
all.push((z_to_poly(&g), mult));
}
}
let mut check = Poly::from_int(1);
for (g, m) in &all {
for _ in 0..*m {
check = &check * g;
}
}
if check != prim {
tracing::error!(
"factor_zassenhaus: verification failed (product of factors ≠ input); returning unfactored"
);
return (content, vec![(prim, 1)]);
}
all.sort_by(|a, b| cmp_poly(&a.0, &b.0));
(content, all)
}
#[must_use]
pub fn factor_squarefree_z(f: &[BigInt]) -> Vec<Vec<BigInt>> {
let mut f = f.to_vec();
z_normalize(&mut f);
let Some(n) = z_degree(&f) else {
return vec![];
};
if n == 0 {
return vec![];
}
let mut f = z_primitive_part(&f);
if f.last().is_some_and(|lc| lc.is_negative()) {
for c in &mut f {
*c = -std::mem::take(c);
}
}
let mut factors: Vec<Vec<BigInt>> = Vec::new();
let low_zeros = f.iter().take_while(|c| c.is_zero()).count();
if low_zeros > 0 {
for _ in 0..low_zeros {
factors.push(vec![BigInt::zero(), BigInt::one()]);
}
f.drain(..low_zeros);
}
if z_degree(&f).unwrap_or(0) == 0 {
factors.sort_by(cmp_z);
return factors;
}
let fq = z_to_poly(&f);
if fq.is_squarefree() == Some(false) {
for (part, mult) in super::dense::square_free_decomposition(&fq) {
let sub = factor_squarefree_z(&poly_to_z(&part));
for _ in 0..mult {
factors.extend(sub.iter().cloned());
}
}
factors.sort_by(cmp_z);
return factors;
}
let (remaining, linear) = super::dense::extract_rational_roots(&fq);
for l in linear {
factors.push(poly_to_z(&l));
}
let mut remaining = poly_to_z(&remaining);
if remaining.last().is_some_and(|lc| lc.is_negative()) {
for c in &mut remaining {
*c = -std::mem::take(c);
}
}
match z_degree(&remaining) {
None | Some(0) => {}
Some(1) => factors.push(remaining),
Some(_) => match zassenhaus_core(&remaining) {
Some(fs) => factors.extend(fs),
None => {
tracing::warn!(
"factor_squarefree_z: no usable prime found; returning cofactor unfactored"
);
factors.push(remaining);
}
},
}
factors.sort_by(cmp_z);
factors
}
#[must_use]
pub fn is_irreducible_z(f: &Poly) -> Option<bool> {
if f.is_zero() || f.is_constant() {
return None;
}
let factors = factor_zassenhaus(f);
Some(factors.len() == 1 && factors[0].1 == 1)
}
#[must_use]
pub fn factor_mod_p(f: &Poly, p: u64) -> Option<ModPFactorization> {
if !is_small_odd_prime(p) {
return None;
}
let pb = BigInt::from(p);
let mut fp: FpPoly = Vec::with_capacity(f.coeffs().len());
for c in f.coeffs() {
let d = c.denom().mod_floor(&pb).to_u64()?;
if d == 0 {
return None;
}
let nmod = c.numer().mod_floor(&pb).to_u64()?;
fp.push(mod_mul(nmod, mod_inv(d, p), p));
}
fp_normalize(&mut fp);
let Some(deg) = fp_degree(&fp) else {
return Some((0, vec![]));
};
let lc = fp[deg];
if deg == 0 {
return Some((lc, vec![]));
}
let monic = fp_monic(&fp, p);
let mut out: Vec<(Vec<u64>, u32)> = Vec::new();
for (g, m) in fp_squarefree_factorization(&monic, p) {
for h in fp_factor_squarefree_monic(&g, p) {
out.push((h, m));
}
}
out.sort_by(|a, b| cmp_fp(&a.0, &b.0).then(a.1.cmp(&b.1)));
Some((lc, out))
}
#[must_use]
pub fn factor_multivariate<O: MonomialOrd>(f: &MultiPoly<O>) -> Option<MultiFactorization<O>> {
let nv = f.num_vars();
if f.is_zero() {
return Some((Ratio::zero(), vec![]));
}
let mut prim = f.primitive_part_q();
let mut content = f.leading_coeff()? / prim.leading_coeff()?;
if prim.leading_coeff().is_some_and(|c| c.is_negative()) {
prim = prim.neg();
content = -content;
}
let mut factors: Vec<(MultiPoly<O>, u32)> = Vec::new();
let mono = prim.monomial_content();
if mono.iter().any(|&e| e > 0) {
let mut stripped = MultiPoly::zero(nv);
for (exp, c) in prim.terms() {
let new_exp: Vec<u32> = exp.iter().zip(&mono).map(|(e, m)| e - m).collect();
stripped = stripped.add(&MultiPoly::monomial(c.clone(), new_exp));
}
for (i, &e) in mono.iter().enumerate() {
if e > 0 {
factors.push((MultiPoly::var(nv, i), e));
}
}
prim = stripped;
}
let present = prim.variables_present();
match present.len() {
0 => {
if let Some(c) = prim.leading_coeff() {
content *= c;
}
}
1 => {
let v = present[0];
let uni = multipoly_to_uni(&prim, v);
let (c, fs) = factor_zassenhaus_with_content(&uni);
content *= c;
for (g, m) in fs {
factors.push((uni_to_multipoly(&g, v, nv), m));
}
}
_ => {
let found = kronecker_factor_all_orders(&prim)?;
for g in found {
match factors.iter_mut().find(|(h, _)| *h == g) {
Some(entry) => entry.1 += 1,
None => factors.push((g, 1)),
}
}
}
}
let mut back = MultiPoly::constant(nv, content.clone());
for (g, m) in &factors {
for _ in 0..*m {
back = back.mul(g);
}
}
if back != *f {
tracing::error!("factor_multivariate: verification failed; returning None");
return None;
}
factors.sort_by(|a, b| cmp_multipoly(&a.0, &b.0));
Some((content, factors))
}
fn kronecker_factor_all_orders<O: MonomialOrd>(f: &MultiPoly<O>) -> Option<Vec<MultiPoly<O>>> {
let present = f.variables_present();
let mut orders: Vec<(usize, Vec<usize>)> = Vec::new();
for perm in permutations(&present) {
let mut radix = 1usize;
let mut total = 0usize;
let mut ok = true;
for &v in &perm {
let d = f.degree_in(v) as usize;
total = total.saturating_add(d.saturating_mul(radix));
radix = radix.saturating_mul(d + 1);
if total > MAX_KRONECKER_SUBSTITUTION_DEGREE {
ok = false;
break;
}
}
if ok {
orders.push((total, perm));
}
}
orders.sort();
for (_, order) in orders {
if let Some(result) = kronecker_factor_with_order(f, &order) {
return Some(result);
}
}
None
}
fn permutations(items: &[usize]) -> Vec<Vec<usize>> {
if items.len() <= 1 {
return vec![items.to_vec()];
}
let mut out = Vec::new();
for i in 0..items.len() {
let mut rest = items.to_vec();
let head = rest.remove(i);
for mut tail in permutations(&rest) {
tail.insert(0, head);
out.push(tail);
}
}
out
}
fn kronecker_factor_with_order<O: MonomialOrd>(
f: &MultiPoly<O>,
order: &[usize],
) -> Option<Vec<MultiPoly<O>>> {
let nv = f.num_vars();
let mut radices = Vec::with_capacity(order.len());
let mut radix = 1usize;
for &v in order {
radices.push(radix);
radix *= f.degree_in(v) as usize + 1;
}
let total_slots = radix;
let mut coeffs = vec![Ratio::zero(); total_slots];
for (exp, c) in f.terms() {
let k: usize = order
.iter()
.zip(&radices)
.map(|(&v, &r)| exp[v] as usize * r)
.sum();
coeffs[k] = c.clone();
}
let image = Poly::from_coeffs(coeffs);
let (_content, ufactors) = factor_zassenhaus_with_content(&image);
let mut pool: Vec<Poly> = Vec::new();
for (g, m) in ufactors {
for _ in 0..m {
pool.push(g.clone());
}
}
let mut remaining = f.clone();
let mut found: Vec<MultiPoly<O>> = Vec::new();
let mut budget = MAX_MULTIVARIATE_SUBSETS;
let mut s = 1usize;
'outer: while s <= pool.len() {
let r = pool.len();
let mut idx: Vec<usize> = (0..s).collect();
loop {
if budget == 0 {
break 'outer;
}
budget -= 1;
let mut prod = Poly::from_int(1);
for &i in &idx {
prod = &prod * &pool[i];
}
let cand = normalize_sign(map_back(&prod, order, &radices, nv));
if !cand.is_zero()
&& cand.total_degree().unwrap_or(0) > 0
&& let Some(q) = remaining.div_exact(&cand)
{
found.push(cand);
remaining = q;
for &i in idx.iter().rev() {
pool.remove(i);
}
continue 'outer;
}
if !next_combination(&mut idx, r) {
break;
}
}
s += 1;
}
if remaining.total_degree().unwrap_or(0) > 0 {
found.push(normalize_sign(remaining.primitive_part_q()));
}
Some(found)
}
fn map_back<O: MonomialOrd>(
p: &Poly,
order: &[usize],
radices: &[usize],
num_vars: usize,
) -> MultiPoly<O> {
let mut out = MultiPoly::zero(num_vars);
for (k, c) in p.coeffs().iter().enumerate() {
if c.is_zero() {
continue;
}
let mut exp = vec![0u32; num_vars];
let mut rest = k;
for j in (0..order.len()).rev() {
let digit = rest / radices[j];
rest %= radices[j];
exp[order[j]] = digit as u32;
}
out = out.add(&MultiPoly::monomial(c.clone(), exp));
}
out
}
fn normalize_sign<O: MonomialOrd>(p: MultiPoly<O>) -> MultiPoly<O> {
if p.leading_coeff().is_some_and(|c| c.is_negative()) {
p.neg()
} else {
p
}
}
fn multipoly_to_uni<O: MonomialOrd>(p: &MultiPoly<O>, v: usize) -> Poly {
let deg = p.degree_in(v) as usize;
let mut coeffs = vec![Ratio::zero(); deg + 1];
for (exp, c) in p.terms() {
coeffs[exp[v] as usize] += c;
}
Poly::from_coeffs(coeffs)
}
fn uni_to_multipoly<O: MonomialOrd>(p: &Poly, v: usize, num_vars: usize) -> MultiPoly<O> {
let mut out = MultiPoly::zero(num_vars);
for (k, c) in p.coeffs().iter().enumerate() {
if c.is_zero() {
continue;
}
let mut exp = vec![0u32; num_vars];
exp[v] = k as u32;
out = out.add(&MultiPoly::monomial(c.clone(), exp));
}
out
}
fn cmp_multipoly<O: MonomialOrd>(a: &MultiPoly<O>, b: &MultiPoly<O>) -> std::cmp::Ordering {
let ta: Vec<(Vec<u32>, Ratio<BigInt>)> =
a.terms().map(|(e, c)| (e.to_vec(), c.clone())).collect();
let tb: Vec<(Vec<u32>, Ratio<BigInt>)> =
b.terms().map(|(e, c)| (e.to_vec(), c.clone())).collect();
a.total_degree()
.cmp(&b.total_degree())
.then_with(|| ta.len().cmp(&tb.len()))
.then_with(|| ta.cmp(&tb))
}
type ZPoly = Vec<BigInt>;
type FpPoly = Vec<u64>;
fn zassenhaus_core(f: &ZPoly) -> Option<Vec<ZPoly>> {
let n = z_degree(f)?;
let lc = f[n].clone();
let mut degree_mask = vec![true; n + 1];
let mut best: Option<(u64, Vec<FpPoly>)> = None;
let mut usable = 0usize;
let mut examined = 0usize;
for p in odd_primes() {
examined += 1;
if examined > MAX_PRIMES_EXAMINED {
break;
}
if (&lc % p).is_zero() {
continue;
}
let fp = z_to_fp(f, p);
if fp_degree(&fp) != Some(n) || !fp_is_squarefree(&fp, p) {
continue;
}
let monic = fp_monic(&fp, p);
let factors = fp_factor_squarefree_monic(&monic, p);
if factors.len() <= 1 {
return Some(vec![f.clone()]);
}
let degs: Vec<usize> = factors.iter().map(|g| fp_degree(g).unwrap_or(0)).collect();
let achievable = subset_sums(°s, n);
for (m, a) in degree_mask.iter_mut().zip(achievable.iter()) {
*m &= *a;
}
if degree_mask
.iter()
.enumerate()
.all(|(d, &ok)| !ok || d == 0 || d == n)
{
tracing::debug!("factor_zassenhaus: degree analysis proves irreducibility");
return Some(vec![f.clone()]);
}
let better = match &best {
None => true,
Some((bp, bf)) => factors.len() < bf.len() || (factors.len() == bf.len() && p > *bp),
};
if better {
best = Some((p, factors));
}
usable += 1;
if usable >= PRIMES_TO_TRY {
break;
}
}
let (p, modular_factors) = best?;
let r = modular_factors.len();
tracing::debug!(
prime = p,
modular_factors = r,
degree = n,
"factor_zassenhaus: selected prime"
);
let bound = mignotte_bound(f);
let two_bound = &bound * 2;
let pb = BigInt::from(p);
let mut pk = pb.clone();
let mut k = 1u32;
while pk <= two_bound {
pk *= &pb;
k += 1;
}
let lifted = hensel_lift(f, p, &modular_factors, k);
debug_assert_eq!(lifted.len(), r);
let (mut found, remainder, complete) = recombine(f, &pk, lifted, °ree_mask);
if let Some(rem) = remainder {
if complete {
found.push(rem);
} else {
tracing::warn!(
degree = z_degree(&rem).unwrap_or(0),
"factor_zassenhaus: recombination budget exhausted; trying Kronecker fallback on cofactor"
);
found.extend(kronecker_fallback(&rem));
}
}
Some(found)
}
fn kronecker_fallback(f: &ZPoly) -> Vec<ZPoly> {
let mut remaining = z_to_poly(f);
let mut out: Vec<ZPoly> = Vec::new();
let max_trial = (remaining.degree().unwrap_or(0) / 2).min(super::MAX_KRONECKER_DEGREE);
for trial_deg in 2..=max_trial {
loop {
let rem_deg = remaining.degree().unwrap_or(0);
if rem_deg < 2 * trial_deg {
break;
}
match super::dense::kronecker_find_factor(&remaining, trial_deg) {
Some((fac, quot)) => {
out.extend(factor_squarefree_z(&poly_to_z(&fac)));
remaining = quot;
}
None => break,
}
}
}
if remaining.degree().unwrap_or(0) >= 1 {
out.push(poly_to_z(&super::dense::ensure_positive_lc(&remaining)));
}
out
}
fn mignotte_bound(f: &ZPoly) -> BigInt {
let n = z_degree(f).unwrap_or(0);
let sum_sq: BigInt = f.iter().map(|c| c * c).sum();
let norm = isqrt_ceil(&sum_sq);
let lc_abs = f[n].abs();
(BigInt::one() << n) * norm * lc_abs
}
fn isqrt_ceil(n: &BigInt) -> BigInt {
if n.is_zero() {
return BigInt::zero();
}
let s = n.sqrt();
if &s * &s < *n { s + 1 } else { s }
}
fn subset_sums(degs: &[usize], n: usize) -> Vec<bool> {
let mut reach = vec![false; n + 1];
reach[0] = true;
for &d in degs {
for s in (d..=n).rev() {
if reach[s - d] {
reach[s] = true;
}
}
}
reach
}
fn hensel_lift(f: &ZPoly, p: u64, factors: &[FpPoly], k: u32) -> Vec<ZPoly> {
let n = z_degree(f).unwrap_or(0);
let lc_p = f[n].mod_floor(&BigInt::from(p)).to_u64().unwrap_or(0);
let lc_inv = mod_inv(lc_p, p);
let r = factors.len();
let mut s_coeffs: Vec<FpPoly> = Vec::with_capacity(r);
for i in 0..r {
let mut others = vec![1u64];
for (j, g) in factors.iter().enumerate() {
if j != i {
others = fp_mul(&others, g, p);
}
}
let (u, _v, _g) = fp_extended_gcd(&others, &factors[i], p);
let (_, s) = fp_div_rem(&u, &factors[i], p);
s_coeffs.push(s);
}
let pb = BigInt::from(p);
let mut lifted: Vec<ZPoly> = factors
.iter()
.map(|g| g.iter().map(|&c| BigInt::from(c)).collect())
.collect();
let mut modulus = pb.clone();
for _step in 1..k {
let next = &modulus * &pb;
let mut prod: ZPoly = vec![f[n].mod_floor(&next)];
for g in &lifted {
prod = z_mul_mod(&prod, g, &next);
}
let mut e_p: FpPoly = vec![0; n.max(1)];
let mut any = false;
for (i, slot) in e_p.iter_mut().enumerate().take(n) {
let fi = f.get(i).cloned().unwrap_or_else(BigInt::zero);
let pi = prod.get(i).cloned().unwrap_or_else(BigInt::zero);
let diff = (fi - pi).mod_floor(&next);
debug_assert!((&diff % &modulus).is_zero());
let q = diff / &modulus;
let c = q.mod_floor(&pb).to_u64().unwrap_or(0);
if c != 0 {
any = true;
}
*slot = c;
}
fp_normalize(&mut e_p);
if any {
let e_scaled = fp_scale(&e_p, lc_inv, p);
for (i, g) in lifted.iter_mut().enumerate() {
let se = fp_mul(&s_coeffs[i], &e_scaled, p);
let (_, delta) = fp_div_rem(&se, &factors[i], p);
for (j, &d) in delta.iter().enumerate() {
if d != 0 {
g[j] += &modulus * BigInt::from(d);
}
}
}
}
modulus = next;
}
lifted
}
fn recombine(
f: &ZPoly,
pk: &BigInt,
mut g: Vec<ZPoly>,
degree_mask: &[bool],
) -> (Vec<ZPoly>, Option<ZPoly>, bool) {
let mut f_cur = f.clone();
let mut found: Vec<ZPoly> = Vec::new();
let mut budget = MAX_RECOMBINATION_SUBSETS;
let mut s = 1usize;
'outer: while 2 * s <= g.len() {
let r = g.len();
let n_cur = z_degree(&f_cur).unwrap_or(0);
let lc_cur = f_cur[n_cur].clone();
let lc_f0 = &lc_cur * &f_cur[0];
let degs: Vec<usize> = g.iter().map(|gi| z_degree(gi).unwrap_or(0)).collect();
let mut idx: Vec<usize> = (0..s).collect();
loop {
if budget == 0 {
break 'outer;
}
budget -= 1;
let deg_sum: usize = idx.iter().map(|&i| degs[i]).sum();
let degree_ok = deg_sum <= n_cur && degree_mask.get(deg_sum).copied().unwrap_or(false);
if degree_ok {
let mut c = lc_cur.mod_floor(pk);
for &i in &idx {
c = (&c * &g[i][0]).mod_floor(pk);
}
let c = symmetric(c, pk);
if !c.is_zero() && (&lc_f0 % &c).is_zero() {
let mut cand: ZPoly = vec![lc_cur.mod_floor(pk)];
for &i in &idx {
cand = z_mul_mod(&cand, &g[i], pk);
}
for coeff in &mut cand {
*coeff = symmetric(std::mem::take(coeff), pk);
}
z_normalize(&mut cand);
let cand = z_primitive_part(&cand);
if z_degree(&cand).is_some_and(|d| d >= 1)
&& let Some(quot) = z_div_exact(&f_cur, &cand)
{
found.push(cand);
f_cur = quot;
for &i in idx.iter().rev() {
g.remove(i);
}
continue 'outer;
}
}
}
if !next_combination(&mut idx, r) {
break;
}
}
s += 1;
}
let complete = 2 * s > g.len() || g.is_empty();
let remainder = if z_degree(&f_cur).is_some_and(|d| d >= 1) {
let mut rem = z_primitive_part(&f_cur);
if rem.last().is_some_and(|lc| lc.is_negative()) {
for c in &mut rem {
*c = -std::mem::take(c);
}
}
Some(rem)
} else {
None
};
(found, remainder, complete)
}
fn next_combination(idx: &mut [usize], n: usize) -> bool {
let k = idx.len();
if k == 0 {
return false;
}
let mut i = k;
while i > 0 {
i -= 1;
if idx[i] < n - k + i {
idx[i] += 1;
for j in i + 1..k {
idx[j] = idx[j - 1] + 1;
}
return true;
}
}
false
}
fn symmetric(x: BigInt, m: &BigInt) -> BigInt {
if &x * 2 > *m { x - m } else { x }
}
fn z_normalize(f: &mut ZPoly) {
while f.last().is_some_and(|c| c.is_zero()) {
f.pop();
}
}
fn z_degree(f: &ZPoly) -> Option<usize> {
if f.is_empty() {
None
} else {
Some(f.len() - 1)
}
}
fn z_mul_mod(a: &ZPoly, b: &ZPoly, m: &BigInt) -> ZPoly {
if a.is_empty() || b.is_empty() {
return vec![];
}
let mut out = vec![BigInt::zero(); a.len() + b.len() - 1];
for (i, ai) in a.iter().enumerate() {
if ai.is_zero() {
continue;
}
for (j, bj) in b.iter().enumerate() {
out[i + j] += ai * bj;
}
}
for c in &mut out {
*c = c.mod_floor(m);
}
z_normalize(&mut out);
out
}
fn z_content(f: &ZPoly) -> BigInt {
let mut g = BigInt::zero();
for c in f {
g = g.gcd(c);
}
g
}
fn z_primitive_part(f: &ZPoly) -> ZPoly {
let c = z_content(f);
if c.is_zero() || c.is_one() {
return f.clone();
}
f.iter().map(|x| x / &c).collect()
}
fn z_div_exact(f: &ZPoly, g: &ZPoly) -> Option<ZPoly> {
let dg = z_degree(g)?;
let Some(df) = z_degree(f) else {
return Some(vec![]);
};
if df < dg {
return None;
}
let lc_g = &g[dg];
let mut rem = f.clone();
let mut quot = vec![BigInt::zero(); df - dg + 1];
while let Some(dr) = z_degree(&rem) {
if dr < dg {
return None;
}
let (c, r) = rem[dr].div_rem(lc_g);
if !r.is_zero() {
return None;
}
let shift = dr - dg;
for (j, gj) in g.iter().enumerate() {
rem[shift + j] -= &c * gj;
}
quot[shift] = c;
z_normalize(&mut rem);
}
Some(quot)
}
fn z_to_fp(f: &ZPoly, p: u64) -> FpPoly {
let pb = BigInt::from(p);
let mut out: FpPoly = f
.iter()
.map(|c| c.mod_floor(&pb).to_u64().unwrap_or(0))
.collect();
fp_normalize(&mut out);
out
}
fn poly_to_z(p: &Poly) -> ZPoly {
let mut denom = BigInt::one();
for c in p.coeffs() {
denom = denom.lcm(c.denom());
}
let mut out: ZPoly = p
.coeffs()
.iter()
.map(|c| c.numer() * (&denom / c.denom()))
.collect();
z_normalize(&mut out);
out
}
fn z_to_poly(f: &ZPoly) -> Poly {
Poly::from_coeffs(f.iter().cloned().map(Ratio::from_integer).collect())
}
fn cmp_z(a: &ZPoly, b: &ZPoly) -> std::cmp::Ordering {
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
fn cmp_poly(a: &Poly, b: &Poly) -> std::cmp::Ordering {
a.coeffs()
.len()
.cmp(&b.coeffs().len())
.then_with(|| a.coeffs().cmp(b.coeffs()))
}
fn cmp_fp(a: &FpPoly, b: &FpPoly) -> std::cmp::Ordering {
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
#[inline]
fn mod_mul(a: u64, b: u64, p: u64) -> u64 {
((a as u128 * b as u128) % p as u128) as u64
}
fn mod_pow(mut base: u64, mut exp: u64, p: u64) -> u64 {
let mut result = 1u64 % p;
base %= p;
while exp > 0 {
if exp & 1 == 1 {
result = mod_mul(result, base, p);
}
base = mod_mul(base, base, p);
exp >>= 1;
}
result
}
fn mod_inv(a: u64, p: u64) -> u64 {
mod_pow(a % p, p - 2, p)
}
fn is_small_odd_prime(p: u64) -> bool {
if !(3..MAX_PRIME).contains(&p) || p.is_multiple_of(2) {
return false;
}
let mut d = 3u64;
while d * d <= p {
if p.is_multiple_of(d) {
return false;
}
d += 2;
}
true
}
fn odd_primes() -> impl Iterator<Item = u64> {
(3u64..MAX_PRIME)
.step_by(2)
.filter(|&p| is_small_odd_prime(p))
}
struct XorShift(u64);
impl XorShift {
fn new(seed: u64) -> Self {
XorShift(seed.max(1) ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
}
fn fp_normalize(f: &mut FpPoly) {
while f.last() == Some(&0) {
f.pop();
}
}
fn fp_degree(f: &FpPoly) -> Option<usize> {
if f.is_empty() {
None
} else {
Some(f.len() - 1)
}
}
#[cfg(test)]
fn fp_add(a: &FpPoly, b: &FpPoly, p: u64) -> FpPoly {
let n = a.len().max(b.len());
let mut out = Vec::with_capacity(n);
for i in 0..n {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
out.push((x + y) % p);
}
fp_normalize(&mut out);
out
}
fn fp_sub(a: &FpPoly, b: &FpPoly, p: u64) -> FpPoly {
let n = a.len().max(b.len());
let mut out = Vec::with_capacity(n);
for i in 0..n {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
out.push((x + p - y) % p);
}
fp_normalize(&mut out);
out
}
fn fp_scale(a: &FpPoly, c: u64, p: u64) -> FpPoly {
let mut out: FpPoly = a.iter().map(|&x| mod_mul(x, c, p)).collect();
fp_normalize(&mut out);
out
}
fn fp_mul(a: &FpPoly, b: &FpPoly, p: u64) -> FpPoly {
if a.is_empty() || b.is_empty() {
return vec![];
}
let mut out = vec![0u64; a.len() + b.len() - 1];
for (i, &ai) in a.iter().enumerate() {
if ai == 0 {
continue;
}
for (j, &bj) in b.iter().enumerate() {
out[i + j] = (out[i + j] + mod_mul(ai, bj, p)) % p;
}
}
fp_normalize(&mut out);
out
}
fn fp_div_rem(a: &FpPoly, b: &FpPoly, p: u64) -> (FpPoly, FpPoly) {
let db = fp_degree(b).expect("fp_div_rem: division by zero polynomial");
let Some(da) = fp_degree(a) else {
return (vec![], vec![]);
};
if da < db {
return (vec![], a.clone());
}
let inv_lc = mod_inv(b[db], p);
let mut rem = a.clone();
let mut quot = vec![0u64; da - db + 1];
while let Some(dr) = fp_degree(&rem) {
if dr < db {
break;
}
let c = mod_mul(rem[dr], inv_lc, p);
let shift = dr - db;
quot[shift] = c;
for (j, &bj) in b.iter().enumerate() {
let sub = mod_mul(c, bj, p);
rem[shift + j] = (rem[shift + j] + p - sub) % p;
}
fp_normalize(&mut rem);
}
fp_normalize(&mut quot);
(quot, rem)
}
fn fp_rem(a: &FpPoly, b: &FpPoly, p: u64) -> FpPoly {
fp_div_rem(a, b, p).1
}
fn fp_monic(a: &FpPoly, p: u64) -> FpPoly {
match fp_degree(a) {
None => vec![],
Some(d) => {
if a[d] == 1 {
a.clone()
} else {
fp_scale(a, mod_inv(a[d], p), p)
}
}
}
}
fn fp_gcd(a: &FpPoly, b: &FpPoly, p: u64) -> FpPoly {
let mut a = a.clone();
let mut b = b.clone();
while !b.is_empty() {
let r = fp_rem(&a, &b, p);
a = b;
b = r;
}
fp_monic(&a, p)
}
fn fp_extended_gcd(a: &FpPoly, b: &FpPoly, p: u64) -> (FpPoly, FpPoly, FpPoly) {
let (mut r0, mut r1) = (a.clone(), b.clone());
let (mut s0, mut s1) = (vec![1u64], vec![]);
let (mut t0, mut t1) = (vec![], vec![1u64]);
while !r1.is_empty() {
let (q, r) = fp_div_rem(&r0, &r1, p);
let s = fp_sub(&s0, &fp_mul(&q, &s1, p), p);
let t = fp_sub(&t0, &fp_mul(&q, &t1, p), p);
r0 = r1;
r1 = r;
s0 = s1;
s1 = s;
t0 = t1;
t1 = t;
}
if let Some(d) = fp_degree(&r0) {
let inv = mod_inv(r0[d], p);
return (
fp_scale(&s0, inv, p),
fp_scale(&t0, inv, p),
fp_scale(&r0, inv, p),
);
}
(s0, t0, r0)
}
fn fp_derivative(a: &FpPoly, p: u64) -> FpPoly {
if a.len() <= 1 {
return vec![];
}
let mut out: FpPoly = a
.iter()
.enumerate()
.skip(1)
.map(|(i, &c)| mod_mul(c, (i as u64) % p, p))
.collect();
fp_normalize(&mut out);
out
}
fn fp_is_squarefree(a: &FpPoly, p: u64) -> bool {
let d = fp_derivative(a, p);
if d.is_empty() {
return fp_degree(a) == Some(0);
}
fp_degree(&fp_gcd(a, &d, p)) == Some(0)
}
fn fp_powmod_u64(base: &FpPoly, mut exp: u64, m: &FpPoly, p: u64) -> FpPoly {
let mut result = vec![1u64];
let mut b = fp_rem(base, m, p);
while exp > 0 {
if exp & 1 == 1 {
result = fp_rem(&fp_mul(&result, &b, p), m, p);
}
exp >>= 1;
if exp > 0 {
b = fp_rem(&fp_mul(&b, &b, p), m, p);
}
}
result
}
fn fp_powmod_big(base: &FpPoly, exp: &BigUint, m: &FpPoly, p: u64) -> FpPoly {
let mut result = vec![1u64];
let mut b = fp_rem(base, m, p);
let bits = exp.bits();
for i in 0..bits {
if exp.bit(i) {
result = fp_rem(&fp_mul(&result, &b, p), m, p);
}
if i + 1 < bits {
b = fp_rem(&fp_mul(&b, &b, p), m, p);
}
}
result
}
fn fp_pth_root(a: &FpPoly, p: u64) -> FpPoly {
let step = p as usize;
let mut out: FpPoly = a.iter().step_by(step).copied().collect();
fp_normalize(&mut out);
out
}
fn fp_squarefree_factorization(a: &FpPoly, p: u64) -> Vec<(FpPoly, u32)> {
let mut result: Vec<(FpPoly, u32)> = Vec::new();
let Some(d) = fp_degree(a) else {
return result;
};
if d == 0 {
return result;
}
let da = fp_derivative(a, p);
if da.is_empty() {
let b = fp_pth_root(a, p);
for (g, m) in fp_squarefree_factorization(&b, p) {
result.push((g, m * (p as u32)));
}
return result;
}
let mut c = fp_gcd(a, &da, p);
let mut w = fp_div_rem(a, &c, p).0;
let mut i = 1u32;
while fp_degree(&w).unwrap_or(0) > 0 {
let y = fp_gcd(&w, &c, p);
let z = fp_div_rem(&w, &y, p).0;
if fp_degree(&z).unwrap_or(0) > 0 {
result.push((fp_monic(&z, p), i));
}
i += 1;
w = y;
c = fp_div_rem(&c, &w, p).0;
}
if fp_degree(&c).unwrap_or(0) > 0 {
let b = fp_pth_root(&c, p);
for (g, m) in fp_squarefree_factorization(&b, p) {
result.push((g, m * (p as u32)));
}
}
result
}
fn fp_factor_squarefree_monic(a: &FpPoly, p: u64) -> Vec<FpPoly> {
let mut out: Vec<FpPoly> = Vec::new();
let Some(d) = fp_degree(a) else {
return out;
};
if d == 0 {
return out;
}
let mut rng = XorShift::new(p ^ ((d as u64) << 32));
for (g, deg) in fp_distinct_degree(a, p) {
fp_equal_degree(&g, deg, p, &mut rng, &mut out);
}
out.sort_by(cmp_fp);
out
}
fn fp_distinct_degree(a: &FpPoly, p: u64) -> Vec<(FpPoly, usize)> {
let mut result = Vec::new();
let mut f = a.clone();
let x: FpPoly = vec![0, 1];
let mut h = x.clone();
let mut i = 1usize;
while fp_degree(&f).unwrap_or(0) >= 2 * i {
h = fp_powmod_u64(&h, p, &f, p);
let g = fp_gcd(&fp_sub(&h, &x, p), &f, p);
if fp_degree(&g).unwrap_or(0) > 0 {
result.push((g.clone(), i));
f = fp_div_rem(&f, &g, p).0;
h = fp_rem(&h, &f, p);
}
i += 1;
}
if fp_degree(&f).unwrap_or(0) > 0 {
let d = fp_degree(&f).unwrap_or(0);
result.push((f, d));
}
result
}
fn fp_equal_degree(g: &FpPoly, d: usize, p: u64, rng: &mut XorShift, out: &mut Vec<FpPoly>) {
let mut stack: Vec<FpPoly> = vec![g.clone()];
let exp = (BigUint::from(p).pow(d as u32) - BigUint::one()) / BigUint::from(2u32);
let one: FpPoly = vec![1];
while let Some(cur) = stack.pop() {
let deg = fp_degree(&cur).unwrap_or(0);
if deg == 0 {
continue;
}
if deg == d {
out.push(fp_monic(&cur, p));
continue;
}
let mut split: Option<FpPoly> = None;
for _attempt in 0..256 {
let mut a: FpPoly = (0..deg).map(|_| rng.next_u64() % p).collect();
fp_normalize(&mut a);
if fp_degree(&a).unwrap_or(0) == 0 {
continue;
}
let g1 = fp_gcd(&a, &cur, p);
let dg1 = fp_degree(&g1).unwrap_or(0);
if dg1 > 0 && dg1 < deg {
split = Some(g1);
break;
}
let b = fp_powmod_big(&a, &exp, &cur, p);
let h = fp_gcd(&fp_sub(&b, &one, p), &cur, p);
let dh = fp_degree(&h).unwrap_or(0);
if dh > 0 && dh < deg {
split = Some(h);
break;
}
}
match split {
Some(h) => {
let q = fp_div_rem(&cur, &h, p).0;
stack.push(h);
stack.push(q);
}
None => {
tracing::error!("factor_mod_p: equal-degree splitting failed to split a block");
out.push(fp_monic(&cur, p));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn zp(c: &[i64]) -> ZPoly {
c.iter().map(|&x| BigInt::from(x)).collect()
}
fn qp(c: &[i64]) -> Poly {
Poly::from_coeffs(
c.iter()
.map(|&x| Ratio::from_integer(BigInt::from(x)))
.collect(),
)
}
fn z_mul(a: &ZPoly, b: &ZPoly) -> ZPoly {
if a.is_empty() || b.is_empty() {
return vec![];
}
let mut out = vec![BigInt::zero(); a.len() + b.len() - 1];
for (i, ai) in a.iter().enumerate() {
for (j, bj) in b.iter().enumerate() {
out[i + j] += ai * bj;
}
}
z_normalize(&mut out);
out
}
fn product(fs: &[ZPoly]) -> ZPoly {
let mut acc = vec![BigInt::one()];
for f in fs {
acc = z_mul(&acc, f);
}
acc
}
fn xn_minus_1(n: usize) -> ZPoly {
let mut v = vec![BigInt::zero(); n + 1];
v[0] = BigInt::from(-1);
v[n] = BigInt::one();
v
}
#[test]
fn fp_div_rem_roundtrip() {
let p = 7;
let a = vec![3, 1, 4, 1, 5];
let b = vec![2, 0, 1];
let (q, r) = fp_div_rem(&a, &b, p);
let back = fp_add(&fp_mul(&q, &b, p), &r, p);
assert_eq!(back, a);
assert!(fp_degree(&r).unwrap_or(0) < 2);
}
#[test]
fn fp_extended_gcd_bezout() {
let p = 13;
let a = vec![1, 2, 3, 1];
let b = vec![5, 1, 1];
let (u, v, g) = fp_extended_gcd(&a, &b, p);
let lhs = fp_add(&fp_mul(&u, &a, p), &fp_mul(&v, &b, p), p);
assert_eq!(lhs, g);
}
#[test]
fn fp_squarefree_detects_square() {
let p = 5;
assert!(!fp_is_squarefree(&vec![1, 2, 1], p));
assert!(fp_is_squarefree(&vec![1, 0, 1], p));
}
#[test]
fn fp_factor_x4_minus_1_mod_5_splits_fully() {
let p = 5;
let f = vec![4, 0, 0, 0, 1]; let fs = fp_factor_squarefree_monic(&f, p);
assert_eq!(fs.len(), 4);
for g in &fs {
assert_eq!(fp_degree(g), Some(1));
}
}
#[test]
fn fp_squarefree_factorization_with_pth_power() {
let p = 3;
let f = vec![2, 1, 0, 2, 1];
let sqf = fp_squarefree_factorization(&f, p);
let mut acc = vec![1u64];
for (g, m) in &sqf {
for _ in 0..*m {
acc = fp_mul(&acc, g, p);
}
}
assert_eq!(acc, f);
assert!(sqf.iter().any(|(_, m)| *m == 3));
}
#[test]
fn factor_mod_p_public_api() {
let f = qp(&[1, 0, 1]);
let (lc, fs) = factor_mod_p(&f, 5).unwrap();
assert_eq!(lc, 1);
assert_eq!(fs, vec![(vec![2, 1], 1), (vec![3, 1], 1)]);
assert!(factor_mod_p(&f, 4).is_none());
assert!(factor_mod_p(&f, 2).is_none());
let half = Poly::from_coeffs(vec![
Ratio::new(BigInt::from(1), BigInt::from(5)),
Ratio::one(),
]);
assert!(factor_mod_p(&half, 5).is_none());
assert!(factor_mod_p(&half, 7).is_some());
}
#[test]
fn factor_mod_p_with_multiplicity() {
let f = qp(&[2, 5, 4, 1]);
let (_, fs) = factor_mod_p(&f, 7).unwrap();
assert_eq!(fs, vec![(vec![1, 1], 2), (vec![2, 1], 1)]);
}
#[test]
fn z_div_exact_works_and_rejects() {
let f = z_mul(&zp(&[1, 1]), &zp(&[-2, 3]));
assert_eq!(z_div_exact(&f, &zp(&[1, 1])), Some(zp(&[-2, 3])));
assert_eq!(z_div_exact(&f, &zp(&[-2, 3])), Some(zp(&[1, 1])));
assert_eq!(z_div_exact(&f, &zp(&[1, 2])), None);
assert_eq!(z_div_exact(&f, &zp(&[5, 1])), None);
}
#[test]
fn next_combination_enumerates_all() {
let mut idx = vec![0, 1];
let mut count = 1;
while next_combination(&mut idx, 5) {
count += 1;
}
assert_eq!(count, 10);
}
#[test]
fn hensel_lift_reconstructs_mod_pk() {
let f = product(&[zp(&[-1, 1]), zp(&[2, 1]), zp(&[3, 2])]);
let p = 7;
let fp = fp_monic(&z_to_fp(&f, p), p);
let factors = fp_factor_squarefree_monic(&fp, p);
assert_eq!(factors.len(), 3);
let k = 6;
let lifted = hensel_lift(&f, p, &factors, k);
let pk = BigInt::from(p).pow(k);
let n = z_degree(&f).unwrap();
let mut prod = vec![f[n].mod_floor(&pk)];
for g in &lifted {
prod = z_mul_mod(&prod, g, &pk);
}
let f_mod: ZPoly = f.iter().map(|c| c.mod_floor(&pk)).collect();
assert_eq!(prod, f_mod);
}
fn check_factorization(f: &ZPoly, expected_degrees: &[usize]) -> Vec<ZPoly> {
let fs = factor_squarefree_z(f);
let mut degs: Vec<usize> = fs.iter().map(|g| z_degree(g).unwrap()).collect();
degs.sort_unstable();
let mut exp = expected_degrees.to_vec();
exp.sort_unstable();
assert_eq!(degs, exp, "degrees for {f:?}: got {fs:?}");
let back = product(&fs);
let mut f_pos = f.clone();
if f_pos.last().unwrap().is_negative() {
for c in &mut f_pos {
*c = -std::mem::take(c);
}
}
assert_eq!(back, f_pos, "product of factors must equal input");
fs
}
#[test]
fn factor_x2_minus_1() {
check_factorization(&zp(&[-1, 0, 1]), &[1, 1]);
}
#[test]
fn factor_x2_plus_1_irreducible() {
check_factorization(&zp(&[1, 0, 1]), &[2]);
}
#[test]
fn factor_x4_plus_1_irreducible() {
check_factorization(&zp(&[1, 0, 0, 0, 1]), &[4]);
}
#[test]
fn factor_x4_plus_4_sophie_germain() {
let fs = check_factorization(&zp(&[4, 0, 0, 0, 1]), &[2, 2]);
assert!(fs.contains(&zp(&[2, -2, 1])));
assert!(fs.contains(&zp(&[2, 2, 1])));
}
#[test]
fn factor_6x4_minus_7x3_minus_8x2_plus_7x_plus_2() {
let f = zp(&[2, 7, -8, -7, 6]);
let fs = factor_squarefree_z(&f);
assert_eq!(product(&fs), f);
assert_eq!(fs, vec![zp(&[-1, 1]), zp(&[1, 1]), zp(&[-2, -7, 6])]);
}
#[test]
fn factor_x8_plus_x4_plus_1() {
check_factorization(&zp(&[1, 0, 0, 0, 1, 0, 0, 0, 1]), &[2, 2, 4]);
}
#[test]
fn factor_x12_minus_1() {
check_factorization(&xn_minus_1(12), &[1, 1, 2, 2, 2, 4]);
}
#[test]
fn factor_x_n_minus_1_for_n_up_to_30() {
for n in 2..=30usize {
let fs = factor_squarefree_z(&xn_minus_1(n));
assert_eq!(product(&fs), xn_minus_1(n), "x^{n} - 1");
let divisors = (1..=n).filter(|d| n % d == 0).count();
assert_eq!(fs.len(), divisors, "x^{n} - 1 should have τ(n) factors");
}
}
#[test]
fn factor_x60_minus_1() {
let n = 60;
let fs = factor_squarefree_z(&xn_minus_1(n));
assert_eq!(product(&fs), xn_minus_1(n));
assert_eq!(fs.len(), 12); }
#[test]
fn factor_x105_minus_1() {
let n = 105;
let start = std::time::Instant::now();
let fs = factor_squarefree_z(&xn_minus_1(n));
let elapsed = start.elapsed();
assert_eq!(product(&fs), xn_minus_1(n));
assert_eq!(fs.len(), 8); let phi105 = fs.iter().find(|g| z_degree(g) == Some(48)).unwrap();
assert!(phi105.iter().any(|c| *c == BigInt::from(-2)));
eprintln!("x^105 - 1 factored in {elapsed:?}");
}
#[test]
fn factor_swinnerton_dyer_degree_8_irreducible() {
let f = zp(&[576, 0, -960, 0, 352, 0, -40, 0, 1]);
check_factorization(&f, &[8]);
}
#[test]
fn factor_swinnerton_dyer_degree_4_irreducible() {
check_factorization(&zp(&[1, 0, -10, 0, 1]), &[4]);
}
#[test]
fn factor_random_products_of_irreducibles() {
let irreducibles: Vec<ZPoly> = vec![
zp(&[1, 0, 1]), zp(&[3, 1, 2]), zp(&[-2, 0, 1]), zp(&[1, 1, 0, 1]), zp(&[-3, 0, 0, 2]), zp(&[1, 0, 0, 0, 1]), zp(&[2, -1, 0, 1, 3]), zp(&[-1, -1, 0, 0, 0, 1]), zp(&[7, 0, 0, 1, 0, 5]), ];
let combos: [&[usize]; 5] = [
&[0, 1, 3],
&[2, 4, 5],
&[1, 3, 6, 7],
&[0, 2, 5, 8],
&[3, 4, 6, 8],
];
for combo in combos {
let parts: Vec<ZPoly> = combo.iter().map(|&i| irreducibles[i].clone()).collect();
let f = product(&parts);
let fs = factor_squarefree_z(&f);
assert_eq!(fs.len(), combo.len(), "combo {combo:?}: got {fs:?}");
assert_eq!(product(&fs), f);
for part in &parts {
assert!(fs.contains(part), "missing factor {part:?} in {fs:?}");
}
}
}
#[test]
fn factor_with_x_factor_and_content_via_poly_api() {
let f = qp(&[0, -4, 0, 4]);
let (content, fs) = factor_zassenhaus_with_content(&f);
assert_eq!(content, Ratio::from_integer(BigInt::from(4)));
assert_eq!(fs.len(), 3);
assert!(fs.iter().all(|(_, m)| *m == 1));
}
#[test]
fn factor_repeated_factors_multiplicities() {
let f =
&(&(&qp(&[1, 1]) * &qp(&[1, 1])) * &qp(&[1, 1])) * &(&qp(&[1, 0, 1]) * &qp(&[1, 0, 1]));
let fs = factor_zassenhaus(&f);
assert_eq!(fs, vec![(qp(&[1, 1]), 3), (qp(&[1, 0, 1]), 2)]);
}
#[test]
fn factor_rational_coefficients() {
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
let f = Poly::from_coeffs(vec![-half.clone(), Ratio::zero(), half.clone()]);
let (content, fs) = factor_zassenhaus_with_content(&f);
assert_eq!(content, half);
assert_eq!(fs.len(), 2);
}
#[test]
fn irreducibility_predicate() {
assert_eq!(is_irreducible_z(&qp(&[1, 0, 1])), Some(true));
assert_eq!(is_irreducible_z(&qp(&[-1, 0, 1])), Some(false));
assert_eq!(is_irreducible_z(&qp(&[1, 2, 1])), Some(false));
assert_eq!(is_irreducible_z(&qp(&[2, 2])), Some(true));
assert_eq!(is_irreducible_z(&qp(&[5])), None);
assert_eq!(is_irreducible_z(&Poly::zero()), None);
}
#[test]
fn factor_large_coefficients() {
let big = BigInt::from(1_000_000_000_000i64);
let a = vec![BigInt::one(), big.clone()];
let b = vec![-big.clone(), BigInt::one()];
let f = z_mul(&a, &b);
let fs = factor_squarefree_z(&f);
assert_eq!(fs.len(), 2);
assert_eq!(product(&fs), f);
}
#[test]
fn factor_squarefree_z_handles_repeated_factors() {
let f = product(&[zp(&[1, 1]), zp(&[1, 1]), zp(&[1, 0, 1])]);
let fs = factor_squarefree_z(&f);
assert_eq!(fs, vec![zp(&[1, 1]), zp(&[1, 1]), zp(&[1, 0, 1])]);
assert_eq!(product(&fs), f);
}
#[test]
fn factor_negative_leading_coefficient() {
let fs = factor_squarefree_z(&zp(&[1, 0, -1]));
assert_eq!(fs, vec![zp(&[-1, 1]), zp(&[1, 1])]);
}
#[test]
fn factor_constant_and_zero() {
assert!(factor_squarefree_z(&zp(&[5])).is_empty());
assert!(factor_squarefree_z(&[]).is_empty());
assert_eq!(
factor_zassenhaus_with_content(&Poly::zero()).0,
Ratio::zero()
);
assert!(factor_zassenhaus(&qp(&[3])).is_empty());
}
type MP = MultiPoly<super::super::multipoly::GrevLex>;
fn mv_vars(n: usize) -> Vec<MP> {
(0..n).map(|i| MultiPoly::var(n, i)).collect()
}
fn mv_check(f: &MP, expected_count: usize) -> Vec<(MP, u32)> {
let (content, fs) = factor_multivariate(f).expect("factorable");
let mut back = MultiPoly::constant(f.num_vars(), content);
for (g, m) in &fs {
for _ in 0..*m {
back = back.mul(g);
}
}
assert_eq!(&back, f, "product must reconstruct input");
assert_eq!(fs.len(), expected_count, "factors: {fs:?}");
fs
}
#[test]
fn mv_x2_minus_y2() {
let v = mv_vars(2);
let f = v[0].mul(&v[0]).sub(&v[1].mul(&v[1]));
let fs = mv_check(&f, 2);
assert!(fs.contains(&(v[0].sub(&v[1]), 1)));
assert!(fs.contains(&(v[0].add(&v[1]), 1)));
}
#[test]
fn mv_x3_minus_y3() {
let v = mv_vars(2);
let x3 = v[0].mul(&v[0]).mul(&v[0]);
let y3 = v[1].mul(&v[1]).mul(&v[1]);
let fs = mv_check(&x3.sub(&y3), 2);
assert!(fs.iter().any(|(g, _)| g.total_degree() == Some(1)));
assert!(fs.iter().any(|(g, _)| g.total_degree() == Some(2)));
}
#[test]
fn mv_perfect_square() {
let v = mv_vars(2);
let s = v[0].add(&v[1]);
let f = s.mul(&s);
let fs = mv_check(&f, 1);
assert_eq!(fs[0], (s, 2));
}
#[test]
fn mv_monomial_content() {
let v = mv_vars(2);
let f = v[0].mul(&v[0]).mul(&v[1]).sub(&v[1]);
let fs = mv_check(&f, 3);
assert!(fs.contains(&(v[1].clone(), 1)));
}
#[test]
fn mv_polynomial_content_in_other_variable() {
let v = mv_vars(2);
let one = MP::from_int(2, 1);
let yp1 = v[1].add(&one);
let f = yp1.mul(&v[0]).mul(&v[0]).sub(&yp1);
let fs = mv_check(&f, 3);
assert!(fs.contains(&(yp1, 1)));
assert!(fs.contains(&(v[0].sub(&one), 1)));
assert!(fs.contains(&(v[0].add(&one), 1)));
}
#[test]
fn mv_irreducible_stays_whole() {
let v = mv_vars(2);
let f = v[0].mul(&v[0]).add(&v[1].mul(&v[1]));
let fs = mv_check(&f, 1);
assert_eq!(fs[0], (f, 1));
}
#[test]
fn mv_three_variables() {
let v = mv_vars(3);
let a = v[0].add(&v[1]).add(&v[2]);
let b = v[0].sub(&v[1]);
let c = v[1].add(&v[2]);
let f = a.mul(&b).mul(&c);
let fs = mv_check(&f, 3);
assert!(fs.contains(&(a, 1)));
assert!(fs.contains(&(b, 1)));
assert!(fs.contains(&(c, 1)));
}
#[test]
fn mv_rational_content_and_sign() {
let v = mv_vars(2);
let f = v[0]
.mul(&v[0])
.sub(&v[1].mul(&v[1]))
.scale(&Ratio::new(BigInt::from(-3), BigInt::from(2)));
let (content, fs) = factor_multivariate(&f).unwrap();
assert_eq!(content, Ratio::new(BigInt::from(-3), BigInt::from(2)));
assert_eq!(fs.len(), 2);
}
#[test]
fn mv_univariate_input_delegates() {
let v = mv_vars(2);
let f = v[1].mul(&v[1]).sub(&MP::from_int(2, 1));
let fs = mv_check(&f, 2);
assert!(fs.iter().all(|(g, _)| g.degree_in(0) == 0));
}
}