use ocas_domain::{Domain, FiniteField, Integer, IntegerDomain, number_theory};
use crate::dense::DenseUnivariatePolynomial;
use crate::factor::finite_field::{self, FpPoly};
pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
fn to_finite_field(f: &ZPoly, p: &Integer) -> FpPoly {
let field = FiniteField::new(p.to_bigint());
let coeffs = f
.coeffs()
.iter()
.map(|c| field.element(c.to_bigint()))
.collect();
FpPoly::from_coeffs(field, coeffs)
}
fn from_finite_field_symmetric(g: &FpPoly) -> ZPoly {
let domain = IntegerDomain;
let p = g.domain().prime();
let p_int = Integer::from(p.clone());
let coeffs = g
.coeffs()
.iter()
.map(|c| {
let c_int = Integer::from(c.value().clone());
number_theory::symmetric_mod(&c_int, &p_int)
})
.collect();
ZPoly::from_coeffs(domain, coeffs)
}
fn monic_fp(f: &FpPoly) -> FpPoly {
if f.is_zero() {
return f.zero();
}
let lc = f.leading_coeff().unwrap();
if f.domain().is_one(lc) {
return f.clone();
}
let inv = f.domain().inv(lc).expect("nonzero lc");
f.mul_scalar(&inv)
}
pub(crate) fn mignotte_bound(f: &ZPoly) -> Integer {
let n = f.degree().unwrap_or(0);
let mut sum_sq = Integer::from(0);
for c in f.coeffs() {
let v = c.abs();
sum_sq += &(&v * &v);
}
let norm = sum_sq.sqrt() + &Integer::from(1);
&Integer::from(2).pow_u32(n as u32) * &norm
}
fn bezout_mod_p(g: &FpPoly, h: &FpPoly) -> (FpPoly, FpPoly) {
let field = g.domain().clone();
let one = FpPoly::from_coeffs(field.clone(), vec![field.one()]);
let zero = FpPoly::from_coeffs(field.clone(), vec![field.zero()]);
let (mut old_r, mut r) = (g.clone(), h.clone());
let (mut old_s, mut s) = (one.clone(), zero.clone());
let (mut old_t, mut t) = (zero, one);
while !r.is_zero() {
let (q, rem) = old_r.div_rem(&r).unwrap_or_else(|| (r.zero(), r.zero()));
old_r = r;
r = rem;
let new_s = old_s.sub(&q.mul(&s));
let new_t = old_t.sub(&q.mul(&t));
old_s = s;
s = new_s;
old_t = t;
t = new_t;
}
if let Some(c) = old_r.leading_coeff()
&& !field.is_one(c)
{
let c_inv = field.inv(c).expect("gcd constant is nonzero");
old_s = old_s.mul_scalar(&c_inv);
old_t = old_t.mul_scalar(&c_inv);
}
(old_s, old_t)
}
fn hensel_lift_pair(
f: &ZPoly,
g0: &FpPoly,
h0: &FpPoly,
p: &Integer,
bound: &Integer,
) -> Option<(ZPoly, ZPoly)> {
let (s, t) = bezout_mod_p(g0, h0);
debug_assert!(
{
let one = g0.one();
let id = s.mul(g0).add(&t.mul(h0));
id.sub(&one).is_zero()
},
"Bรฉzout identity sยทg0 + tยทh0 = 1 failed"
);
let mut g = from_finite_field_symmetric(g0);
let mut h = from_finite_field_symmetric(h0);
let mut m = p.clone();
loop {
let gh = g.mul(&h);
let e = f.sub(&gh);
if e.is_zero() {
return Some((g, h));
}
let mut e_over_m = Vec::new();
for c in e.coeffs() {
let (q, r) = c.div_rem(&m);
if r.is_zero() {
e_over_m.push(q);
} else {
return None;
}
}
let e_over_m = ZPoly::from_coeffs(IntegerDomain, e_over_m);
if e_over_m.is_zero() {
return Some((g, h));
}
let e_bar = to_finite_field(&e_over_m, p);
let (_q1, dg) = t.mul(&e_bar).div_rem(g0)?;
let dividend = e_bar.sub(&dg.mul(h0));
let (dh, dh_rem) = dividend.div_rem(g0)?;
debug_assert!(
dh_rem.is_zero(),
"ฮh division not exact; Bรฉzout identity may be broken"
);
let dg_z = from_finite_field_symmetric(&dg);
let dh_z = from_finite_field_symmetric(&dh);
let m_int = m.clone();
g = g.add(&dg_z.mul_scalar(&m_int));
h = h.add(&dh_z.mul_scalar(&m_int));
m *= p;
if &m > bound {
return Some((g, h));
}
}
}
fn hensel_lift_multi(
f: &ZPoly,
factors: &[FpPoly],
p: &Integer,
bound: &Integer,
) -> Option<Vec<ZPoly>> {
if factors.len() <= 1 {
return Some(vec![f.clone()]);
}
let mut lifted: Vec<ZPoly> = Vec::new();
let mut work = factors.to_vec();
let mut f_current = f.clone();
while work.len() > 1 {
let g0 = monic_fp(&work[0].clone());
let h0 = monic_fp(&work[1..].iter().cloned().reduce(|a, b| a.mul(&b)).unwrap());
let (g, h) = hensel_lift_pair(&f_current, &g0, &h0, p, bound)?;
lifted.push(reduce_symmetric(&g, bound));
f_current = h;
work = work[1..].to_vec();
}
lifted.push(reduce_symmetric(&f_current, bound));
Some(lifted)
}
fn reduce_symmetric(f: &ZPoly, modulus: &Integer) -> ZPoly {
let coeffs = f
.coeffs()
.iter()
.map(|c| number_theory::symmetric_mod(c, modulus))
.collect();
ZPoly::from_coeffs(IntegerDomain, coeffs)
}
fn combinations(n: usize, k: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
let mut cur = Vec::new();
combos(0, n, k, &mut cur, &mut out);
out
}
fn combos(start: usize, n: usize, k: usize, cur: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if cur.len() == k {
out.push(cur.clone());
return;
}
for i in start..n {
cur.push(i);
combos(i + 1, n, k, cur, out);
cur.pop();
}
}
fn zassenhaus_combine(f: &ZPoly, lifted: &[ZPoly], modulus: &Integer) -> Vec<ZPoly> {
if lifted.is_empty() {
return Vec::new();
}
let one = f.one();
let lc_f = f
.leading_coeff()
.cloned()
.unwrap_or_else(|| Integer::from(1));
let lc_f_abs = lc_f.abs();
let mut remaining: Vec<ZPoly> = lifted.to_vec();
let mut result = Vec::new();
let mut size = 1usize;
while size <= remaining.len() && !remaining.is_empty() {
let n = remaining.len();
let mut found = false;
for combo in combinations(n, size) {
let mut prod = one.clone();
for &idx in &combo {
prod = prod.mul(&remaining[idx]);
}
let candidate = reduce_symmetric(&prod, modulus);
for d in divisors(&lc_f_abs) {
let scaled = candidate.mul_scalar(&d);
if let Some((_, r)) = f.div_rem(&scaled)
&& r.is_zero()
&& !scaled.is_one()
{
result.push(scaled);
let mut nr = Vec::new();
for (i, fac) in remaining.iter().enumerate() {
if !combo.contains(&i) {
nr.push(fac.clone());
}
}
remaining = nr;
found = true;
size = 1;
break;
}
}
if found {
break;
}
}
if !found {
size += 1;
}
}
for fac in remaining {
if !fac.is_one() {
result.push(fac);
}
}
result
}
fn divisors(n: &Integer) -> Vec<Integer> {
if n <= &Integer::from(0) {
return Vec::new();
}
let mut divs = vec![Integer::from(1)];
let mut remaining = n.clone();
let mut p = Integer::from(2);
while &p * &p <= remaining {
let mut count = 0u32;
while (&remaining % &p).is_zero() {
remaining /= &p;
count += 1;
}
if count > 0 {
let current = divs.clone();
for d in current {
for e in 1..=count {
let factor = &d * &p.pow_u32(e);
divs.push(factor);
}
}
}
p += &Integer::from(1);
}
if remaining > Integer::from(1) {
let current = divs.clone();
for d in current {
divs.push(&d * &remaining);
}
}
divs.sort();
divs
}
pub fn factor_square_free_monic(f: &ZPoly) -> Vec<ZPoly> {
if f.degree().unwrap_or(0) == 0 {
return Vec::new();
}
if f.degree() == Some(1) {
return vec![f.clone()];
}
let bound = mignotte_bound(f);
let two_bound = &Integer::from(2) * &bound;
let lc = f.leading_coeff().unwrap().abs();
let mut prime_count = 0usize;
for p in number_theory::primes_from(&Integer::from(2)) {
prime_count += 1;
if prime_count > 30 {
break;
}
if (&lc % &p).is_zero() {
continue;
}
let fp = to_finite_field(f, &p);
let fpp = fp.derivative();
if fpp.is_zero() || fp.gcd(&fpp).degree().unwrap_or(0) > 0 {
continue; }
let factors_p = finite_field::cantor_zassenhaus(&monic_fp(&fp));
if factors_p.len() <= 1 {
return vec![f.clone()]; }
let mut lift_mod = p.clone();
while lift_mod <= two_bound {
lift_mod *= &p;
}
if let Some(lifted) = hensel_lift_multi(f, &factors_p, &p, &lift_mod) {
let irreducibles = zassenhaus_combine(f, &lifted, &lift_mod);
if !irreducibles.is_empty() {
return irreducibles;
}
}
}
vec![f.clone()]
}
pub fn factor_primitive(f: &ZPoly) -> Vec<(ZPoly, usize)> {
if f.degree().unwrap_or(0) == 0 {
return Vec::new();
}
let sqfree = f.square_free_factorization();
let mut result = Vec::new();
for (g, mult) in sqfree {
let lc = g.leading_coeff().cloned().unwrap();
let sign = if lc.is_negative() {
Integer::from(-1i64)
} else {
Integer::from(1i64)
};
let g_pos = g.mul_scalar(&sign);
for irr in factor_square_free_monic(&g_pos.primitive_part()) {
result.push((irr, mult));
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn zpoly(coeffs: &[i64]) -> ZPoly {
ZPoly::from_coeffs(
IntegerDomain,
coeffs.iter().map(|&c| Integer::from(c)).collect(),
)
}
fn reconstruct(f: &ZPoly, factors: &[ZPoly]) -> bool {
let mut acc = f.one();
for g in factors {
acc = acc.mul(g);
}
let (q, r) = f.div_rem(&acc).unwrap_or((f.zero(), f.clone()));
r.is_zero() && q.degree() == Some(0)
}
#[test]
fn factor_x100_minus_1_over_z() {
let mut coeffs = vec![Integer::from(-1i64)];
coeffs.resize(101, Integer::from(0));
coeffs[100] = Integer::from(1);
let f = ZPoly::from_coeffs(IntegerDomain, coeffs);
let factors = factor_square_free_monic(&f);
assert!(reconstruct(&f, &factors));
assert_eq!(factors.len(), 9);
}
#[test]
fn factor_quadratic_split() {
let f = zpoly(&[6, 5, 1]); let factors = factor_square_free_monic(&f);
assert!(reconstruct(&f, &factors));
assert_eq!(factors.len(), 2);
}
#[test]
fn factor_irreducible_quadratic() {
let f = zpoly(&[1, 0, 1]); let factors = factor_square_free_monic(&f);
assert!(reconstruct(&f, &factors));
assert_eq!(factors.len(), 1);
}
#[test]
fn factor_three_linear() {
let f = zpoly(&[-6, 11, -6, 1]); let factors = factor_square_free_monic(&f);
assert!(reconstruct(&f, &factors));
assert_eq!(factors.len(), 3);
}
#[test]
fn factor_three_mixed() {
let a = zpoly(&[1, 0, 1]);
let b = zpoly(&[1, 1, 1]);
let c = zpoly(&[1, 1]);
let f = a.mul(&b).mul(&c);
let factors = factor_square_free_monic(&f);
assert!(reconstruct(&f, &factors));
assert_eq!(factors.len(), 3);
}
#[test]
fn mignotte_bound_sanity() {
let f = zpoly(&[1, 0, 1]); let b = mignotte_bound(&f);
assert!(
b >= Integer::from(6) && b <= Integer::from(10),
"mignotte(x^2+1) = {b}, expected ~6-10"
);
}
#[test]
fn factor_cyclotomic_matches_sympy_over_z() {
let cases: &[(usize, &[(usize, usize)])] = &[
(12, &[(1, 2), (2, 3), (4, 1)]),
(30, &[(1, 2), (2, 2), (4, 2), (8, 2)]),
(60, &[(1, 2), (2, 3), (4, 3), (8, 3), (16, 1)]),
(100, &[(1, 2), (2, 1), (4, 2), (8, 1), (20, 2), (40, 1)]),
];
for &(n, expected) in cases {
let mut coeffs = vec![Integer::from(-1i64)];
coeffs.resize(n + 1, Integer::from(0));
coeffs[n] = Integer::from(1);
let f = ZPoly::from_coeffs(IntegerDomain, coeffs);
let factors = factor_square_free_monic(&f);
assert!(
reconstruct(&f, &factors),
"x^{n}-1: factors do not reconstruct"
);
let mut obs: std::collections::BTreeMap<usize, usize> = Default::default();
for g in &factors {
*obs.entry(g.degree().unwrap()).or_insert(0) += 1;
}
let observed: Vec<(usize, usize)> = obs.into_iter().collect();
assert_eq!(
observed.as_slice(),
expected,
"x^{n}-1 over Z: degree histogram mismatch"
);
}
}
#[test]
fn factor_x30_minus_1_over_z() {
let mut coeffs = vec![Integer::from(-1i64)];
coeffs.resize(31, Integer::from(0));
coeffs[30] = Integer::from(1);
let f = ZPoly::from_coeffs(IntegerDomain, coeffs);
let factors = factor_square_free_monic(&f);
assert!(reconstruct(&f, &factors));
assert_eq!(factors.len(), 8, "expected 8 cyclotomic factors");
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn any_int_poly(max_degree: usize) -> impl Strategy<Value = ZPoly> {
(0..=max_degree)
.prop_flat_map(move |deg| {
let n = deg + 1;
prop::collection::vec(i64_range(), n)
})
.prop_map(|coeffs| {
let c: Vec<Integer> = coeffs.into_iter().map(Integer::from).collect();
ZPoly::from_coeffs(IntegerDomain, c)
})
}
fn i64_range() -> impl Strategy<Value = i64> {
-100i64..=100i64
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(500))]
#[test]
fn factor_then_multiply_roundtrip(p in any_int_poly(6)) {
let f = p.primitive_part();
if f.degree().unwrap_or(0) == 0 {
return Ok(());
}
let factors = factor_primitive(&f);
let mut acc = f.one();
for (g, m) in &factors {
for _ in 0..*m {
acc = acc.mul(g);
}
}
if let Some((q, r)) = f.div_rem(&acc) {
prop_assert!(r.is_zero());
prop_assert_eq!(q.degree(), Some(0));
}
}
}
}