use num_bigint::BigInt;
use num_traits::{One, ToPrimitive, Zero};
use ocas_domain::{Domain, FiniteField};
use crate::dense::DenseUnivariatePolynomial;
use crate::matrix::Matrix;
pub type FpPoly = DenseUnivariatePolynomial<FiniteField>;
fn monic(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("leading coefficient is nonzero");
f.mul_scalar(&inv)
}
fn x_var(field: &FiniteField) -> FpPoly {
FpPoly::from_coeffs(field.clone(), vec![field.zero(), field.one()])
}
pub fn poly_pow_mod(base: &FpPoly, exp: &BigInt, modulus: &FpPoly) -> FpPoly {
let field = base.domain().clone();
if modulus.is_zero() {
return base.clone();
}
let mut result = FpPoly::from_coeffs(field.clone(), vec![field.one()]);
let mut b = match base.div_rem(modulus) {
Some((_, r)) => r,
None => base.clone(),
};
let mut e = exp.clone();
while !e.is_zero() {
if (&e & &BigInt::one()) == BigInt::one() {
result = result.mul(&b);
if let Some((_, r)) = result.div_rem(modulus) {
result = r;
}
}
e >>= 1;
if !e.is_zero() {
b = b.mul(&b);
if let Some((_, r)) = b.div_rem(modulus) {
b = r;
}
}
}
result
}
fn distinct_degree_factorization(f: &FpPoly) -> Vec<(FpPoly, usize)> {
let field = f.domain().clone();
let p = field.prime().clone();
let mut result = Vec::new();
let mut current = f.clone();
let mut degree = 1usize;
let mut h = x_var(&field);
while let Some(deg_current) = current.degree() {
if deg_current < 2 * degree {
break;
}
h = poly_pow_mod(&h, &p, ¤t);
let h_minus_x = h.sub(&x_var(&field));
let g = current.gcd(&h_minus_x);
let g = monic(&g);
if g.degree().unwrap_or(0) > 0 {
result.push((g.clone(), degree));
if let Some((q, _)) = current.div_rem(&g) {
current = monic(&q);
if let Some((_, rh)) = h.div_rem(¤t) {
h = rh;
}
}
}
degree += 1;
}
if current.degree().unwrap_or(0) > 0 && !current.is_one() {
result.push((monic(¤t), current.degree().unwrap()));
}
result
}
struct Rng {
state: u64,
}
impl Rng {
fn new(seed: u64) -> Self {
Self { state: seed.max(1) }
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
}
fn random_candidate(field: &FiniteField, degree_bound: usize, seed: u64) -> FpPoly {
let p = field
.prime()
.to_u64()
.expect("EDF candidate generation requires a prime fitting in u64");
let mut rng = Rng::new(seed.wrapping_add(0x9E3779B97F4A7C15));
let mut coeffs = Vec::with_capacity(degree_bound);
for _ in 0..degree_bound {
coeffs.push(field.element(BigInt::from(rng.next_u64() % p)));
}
FpPoly::from_coeffs(field.clone(), coeffs)
}
fn equal_degree_factorization(f: &FpPoly, d: usize) -> Vec<FpPoly> {
let field = f.domain().clone();
let p_big = field.prime().clone();
let deg_f = f.degree().unwrap_or(0);
if deg_f == 0 || d == 0 || deg_f == d {
return vec![monic(f)];
}
let char_two = p_big == BigInt::from(2u32);
let half_exp = if !char_two {
Some((p_big.pow(d as u32) - BigInt::one()) / 2)
} else {
None
};
let mut factors: Vec<FpPoly> = vec![monic(f)];
let mut round: u64 = 0;
loop {
let mut made_progress = false;
let mut new_factors = Vec::new();
for (idx, factor) in factors.iter().enumerate() {
let deg = factor.degree().unwrap_or(0);
if deg == d || deg == 0 {
new_factors.push(factor.clone());
continue;
}
let r = deg / d; if r <= 1 {
new_factors.push(factor.clone());
continue;
}
let split = if char_two {
edf_split_char2(factor, d, round, idx as u64)
} else {
edf_split_odd(factor, half_exp.as_ref().unwrap(), round, idx as u64)
};
if let Some((g1, g2)) = split {
new_factors.push(monic(&g1));
new_factors.push(monic(&g2));
made_progress = true;
} else {
new_factors.push(factor.clone());
}
}
factors = new_factors;
if factors
.iter()
.all(|f| f.degree().unwrap_or(0) == 0 || f.degree().unwrap_or(0) == d)
{
break;
}
round += 1;
if !made_progress && round > 64 {
break;
}
}
factors
.into_iter()
.filter(|f| f.degree().unwrap_or(0) > 0)
.collect()
}
fn edf_split_odd(f: &FpPoly, half_exp: &BigInt, round: u64, idx: u64) -> Option<(FpPoly, FpPoly)> {
let field = f.domain().clone();
let deg_f = f.degree().unwrap_or(0);
for attempt in 0..256u64 {
let seed = round
.wrapping_mul(0x10000)
.wrapping_add(idx.wrapping_mul(0x100))
.wrapping_add(attempt);
let a = random_candidate(&field, deg_f, seed);
if a.is_zero() || a.degree().unwrap_or(0) == 0 {
continue;
}
let b = poly_pow_mod(&a, half_exp, f);
let b_minus_one = b.sub(&FpPoly::from_coeffs(field.clone(), vec![field.one()]));
let g = f.gcd(&b_minus_one);
let dg = g.degree().unwrap_or(0);
if dg > 0
&& dg < deg_f
&& let Some((q, _)) = f.div_rem(&g)
{
return Some((g, q));
}
}
None
}
fn edf_split_char2(f: &FpPoly, d: usize, round: u64, idx: u64) -> Option<(FpPoly, FpPoly)> {
let field = f.domain().clone();
let deg_f = f.degree().unwrap_or(0);
for attempt in 0..256u64 {
let seed = round
.wrapping_mul(0x10000)
.wrapping_add(idx.wrapping_mul(0x100))
.wrapping_add(attempt);
let a = random_candidate(&field, deg_f, seed);
if a.is_zero() || a.degree().unwrap_or(0) == 0 {
continue;
}
let mut trace = a.clone();
let mut term = a;
for _ in 1..d {
term = poly_pow_mod(&term, &BigInt::from(2u32), f);
trace = trace.add(&term);
if let Some((_, r)) = trace.div_rem(f) {
trace = r;
}
}
let g = f.gcd(&trace);
let dg = g.degree().unwrap_or(0);
if dg > 0
&& dg < deg_f
&& let Some((q, _)) = f.div_rem(&g)
{
return Some((g, q));
}
}
None
}
pub fn berlekamp(f: &FpPoly) -> Vec<FpPoly> {
let n = match f.degree() {
Some(d) if d > 0 => d,
_ => return Vec::new(),
};
if n == 1 {
return vec![monic(f)];
}
let field = f.domain().clone();
let p = field.prime().clone();
let q = frobenius_matrix(f, n);
let mut a_data = Vec::with_capacity(n * n);
for i in 0..n {
for (j, q_row) in q.iter().enumerate().take(n) {
let mut val = q_row[i].clone();
if i == j {
val = field.sub(&val, &field.one());
}
a_data.push(val);
}
}
let a = Matrix::new(n, n, a_data, field.clone());
let nullspace = nullspace_mod_p(&a, n);
let mut factors = vec![monic(f)];
for v in nullspace {
if v.degree().unwrap_or(0) == 0 {
continue;
}
let mut new_factors = Vec::new();
for factor in &factors {
let deg = factor.degree().unwrap_or(0);
if deg <= 1 {
new_factors.push(factor.clone());
continue;
}
let mut remaining = factor.clone();
let p_u64 = p.to_u64().unwrap_or(1);
for a_val in 0..p_u64 {
if remaining.degree().unwrap_or(0) <= 1 {
break;
}
let a_el = field.element(BigInt::from(a_val));
let va = v.sub(&FpPoly::from_coeffs(field.clone(), vec![a_el]));
let g = monic(&remaining.gcd(&va));
let dg = g.degree().unwrap_or(0);
if dg > 0 && dg < remaining.degree().unwrap_or(0) {
let (q, _) = remaining.div_rem(&g).unwrap();
new_factors.push(g);
remaining = monic(&q);
}
}
if remaining.degree().unwrap_or(0) > 0 {
new_factors.push(monic(&remaining));
}
}
factors = new_factors;
}
factors
.into_iter()
.filter(|g| !g.is_zero() && !g.is_one())
.collect()
}
fn frobenius_matrix(f: &FpPoly, n: usize) -> Vec<Vec<<FiniteField as Domain>::Element>> {
let field = f.domain().clone();
let p = field.prime().clone();
let zero = field.zero();
let x = x_var(&field);
let xp = poly_pow_mod(&x, &p, f);
let mut q = Vec::with_capacity(n);
let mut row0 = vec![zero.clone(); n];
row0[0] = field.one();
q.push(row0);
let mut row1 = xp.coeffs().to_vec();
row1.resize(n, zero.clone());
q.push(row1);
for i in 2..n {
let prev_poly = FpPoly::from_coeffs(field.clone(), q[i - 1].clone());
let prod = prev_poly.mul(&xp);
let (_, reduced) = prod.div_rem(f).unwrap_or_else(|| (prod.zero(), prod));
let mut r = reduced.coeffs().to_vec();
r.resize(n, zero.clone());
q.push(r);
}
q
}
fn nullspace_mod_p(a: &Matrix<FiniteField>, n: usize) -> Vec<FpPoly> {
let field = a.domain().clone();
let mut m = a.clone();
let mut pivot_col = vec![None; n]; let mut row = 0;
for col in 0..n {
if row >= n {
break;
}
let mut pivot = None;
for r in row..n {
if !field.is_zero(&m[(r, col)]) {
pivot = Some(r);
break;
}
}
let pivot_row = match pivot {
Some(r) => r,
None => continue,
};
m.swap_rows(row, pivot_row, col);
let pivot_val = m[(row, col)].clone();
let inv = field.inv(&pivot_val).expect("pivot is nonzero");
for c in col..n {
m[(row, c)] = field.mul(&m[(row, c)], &inv);
}
for r in 0..n {
if r == row {
continue;
}
if !field.is_zero(&m[(r, col)]) {
let factor = m[(r, col)].clone();
for c in col..n {
let term = field.mul(&m[(row, c)], &factor);
m[(r, c)] = field.sub(&m[(r, c)], &term);
}
}
}
pivot_col[row] = Some(col);
row += 1;
}
let mut is_pivot = vec![false; n];
for &pc in &pivot_col {
if let Some(c) = pc {
is_pivot[c] = true;
}
}
let mut basis = Vec::new();
for free_col in 0..n {
if is_pivot[free_col] {
continue;
}
let mut coeffs = vec![field.zero(); n];
coeffs[free_col] = field.one();
for r in 0..n {
if let Some(pc) = pivot_col[r] {
let val = m[(r, free_col)].clone();
if !field.is_zero(&val) {
coeffs[pc] = field.neg(&val);
}
}
}
let poly = FpPoly::from_coeffs(field.clone(), coeffs);
if !poly.is_zero() {
basis.push(poly);
}
}
basis
}
pub fn cantor_zassenhaus(f: &FpPoly) -> Vec<FpPoly> {
if f.degree().unwrap_or(0) == 0 {
return Vec::new();
}
let mut result = Vec::new();
for (g, d) in distinct_degree_factorization(f) {
for irr in equal_degree_factorization(&g, d) {
result.push(irr);
}
}
result
}
pub fn factor_over_finite_field(f: &FpPoly) -> Vec<(FpPoly, usize)> {
let field = f.domain().clone();
let mut result = Vec::new();
if f.is_zero() {
return result;
}
let lc = f.leading_coeff().cloned().unwrap_or_else(|| field.zero());
let monic_f = monic(f);
if !field.is_one(&lc) && !field.is_zero(&lc) {
result.push((FpPoly::from_coeffs(field.clone(), vec![lc]), 1));
}
for (g, multiplicity) in square_free_factorization_ff(&monic_f) {
if g.degree().unwrap_or(0) == 0 {
continue;
}
let use_berlekamp = field.prime() <= &BigInt::from(1000u32);
let irr_factors = if use_berlekamp {
berlekamp(&g)
} else {
cantor_zassenhaus(&g)
};
for irr in irr_factors {
result.push((irr, multiplicity));
}
}
result
}
fn pth_root_prime(f: &FpPoly) -> FpPoly {
let field = f.domain().clone();
let p = field
.prime()
.to_u64()
.expect("p-th root extraction requires a prime fitting in u64") as usize;
let deg = f.degree().unwrap_or(0);
let mut coeffs = Vec::new();
let mut j = 0;
while p * j <= deg {
let c = f.coeff(p * j).cloned().unwrap_or_else(|| field.zero());
coeffs.push(c);
j += 1;
}
FpPoly::from_coeffs(field, coeffs)
}
fn square_free_factorization_ff(f: &FpPoly) -> Vec<(FpPoly, usize)> {
if f.is_zero() || f.degree().unwrap_or(0) == 0 {
return Vec::new();
}
let field = f.domain().clone();
let p = field.prime().clone();
let mut output: Vec<(FpPoly, usize)> = Vec::new();
let fp = f.derivative();
if fp.is_zero() {
let g = pth_root_prime(f);
for (gi, ki) in square_free_factorization_ff(&g) {
output.push((gi, ki * p.to_u64().unwrap_or(1) as usize));
}
return output;
}
let mut c = f.gcd(&fp);
c = monic(&c);
let mut w = monic(&match f.div_rem(&c) {
Some((q, _)) => q,
None => f.clone(),
});
let mut i = 1usize;
while !w.is_one() && w.degree().unwrap_or(0) > 0 {
let y = monic(&w.gcd(&c));
let z = monic(&match w.div_rem(&y) {
Some((q, _)) => q,
None => w.clone(),
});
if z.degree().unwrap_or(0) > 0 && !z.is_one() {
output.push((z, i));
}
w = y;
if let Some((q, _)) = c.div_rem(&w) {
c = monic(&q);
}
i += 1;
}
if c.degree().unwrap_or(0) > 0 && !c.is_one() {
let g = pth_root_prime(&c);
for (gi, ki) in square_free_factorization_ff(&g) {
output.push((gi, ki * p.to_u64().unwrap_or(1) as usize));
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dense::DenseUnivariatePolynomial;
fn field(p: u64) -> FiniteField {
FiniteField::new(BigInt::from(p))
}
fn fpoly(f: &FiniteField, coeffs: &[i64]) -> FpPoly {
DenseUnivariatePolynomial::from_coeffs(
f.clone(),
coeffs.iter().map(|&c| f.element(BigInt::from(c))).collect(),
)
}
fn product(polys: &[FpPoly]) -> FpPoly {
let field = polys
.first()
.map(|p| p.domain().clone())
.unwrap_or_else(|| FiniteField::new(BigInt::from(2)));
let mut acc = FpPoly::from_coeffs(field.clone(), vec![field.one()]);
for p in polys {
acc = acc.mul(p);
}
acc
}
fn assert_factors_reconstruct(input: &FpPoly, factors: &[(FpPoly, usize)]) {
let field = input.domain().clone();
let mut acc = FpPoly::from_coeffs(field.clone(), vec![field.one()]);
for (g, m) in factors {
for _ in 0..*m {
acc = acc.mul(g);
}
}
let deg_acc = acc.degree().unwrap_or(0);
let deg_in = input.degree().unwrap_or(0);
assert_eq!(deg_acc, deg_in, "degree mismatch on reconstruction");
if let Some((q, r)) = input.div_rem(&acc) {
assert!(r.is_zero(), "nonzero remainder when reconstructing");
assert_eq!(q.degree(), Some(0), "quotient should be a constant");
}
}
#[test]
fn poly_pow_mod_basic() {
let f = field(7);
let m = fpoly(&f, &[1, 0, 1]); let x = fpoly(&f, &[0, 1]);
let r = poly_pow_mod(&x, &BigInt::from(2u32), &m);
assert_eq!(r.coeff(0).cloned(), Some(f.element(BigInt::from(6))));
let r = poly_pow_mod(&x, &BigInt::from(4u32), &m);
assert_eq!(r.coeff(0).cloned(), Some(f.element(BigInt::from(1))));
}
#[test]
fn factor_x_squared_minus_1_over_f5() {
let f = field(5);
let p = fpoly(&f, &[4, 0, 1]); let factors = factor_over_finite_field(&p);
let linear_count = factors
.iter()
.filter(|(g, _)| g.degree() == Some(1))
.count();
assert_eq!(linear_count, 2);
assert_factors_reconstruct(&p, &factors);
}
#[test]
fn factor_irreducible_degree_3_over_f2() {
let f = field(2);
let p = fpoly(&f, &[1, 1, 0, 1]);
let factors = factor_over_finite_field(&p);
let nontrivial: Vec<_> = factors
.iter()
.filter(|(g, _)| g.degree().unwrap_or(0) > 0)
.collect();
assert_eq!(nontrivial.len(), 1);
assert_eq!(nontrivial[0].0.degree(), Some(3));
assert_factors_reconstruct(&p, &factors);
}
#[test]
fn factor_cyclotomic_x4_plus_1_over_f3() {
let f = field(3);
let p = fpoly(&f, &[1, 0, 0, 0, 1]); let factors = factor_over_finite_field(&p);
assert_factors_reconstruct(&p, &factors);
for (g, _) in &factors {
if g.degree().unwrap_or(0) < 1 {
continue;
}
let re = cantor_zassenhaus(&monic(g));
assert_eq!(re.len(), 1, "factor {:?} not irreducible", g);
}
}
#[test]
fn factor_four_irreducible_quadratics_over_f5() {
let f = field(5);
let q1 = fpoly(&f, &[2, 0, 1]);
let q2 = fpoly(&f, &[3, 0, 1]);
let q3 = fpoly(&f, &[1, 1, 1]);
let q4 = fpoly(&f, &[2, 1, 1]);
let p = product(&[q1, q2, q3, q4]);
let factors = factor_over_finite_field(&p);
assert_factors_reconstruct(&p, &factors);
let quad_count = factors
.iter()
.filter(|(g, _)| g.degree() == Some(2))
.count();
assert_eq!(
quad_count, 4,
"expected four quadratic factors, got {:?}",
factors
);
for (g, _) in &factors {
if g.degree().unwrap_or(0) < 1 {
continue;
}
let re = cantor_zassenhaus(&monic(g));
assert_eq!(re.len(), 1, "factor {:?} not irreducible", g);
}
}
#[test]
fn factor_cyclotomic_matches_sympy() {
#[allow(clippy::type_complexity)]
let cases: &[(u64, usize, &[(usize, usize)])] = &[
(5, 10, &[(1, 10)]),
(5, 30, &[(1, 10), (2, 10)]),
(7, 10, &[(1, 2), (4, 2)]),
(7, 30, &[(1, 6), (4, 6)]),
(17, 20, &[(1, 4), (4, 4)]),
(17, 30, &[(1, 2), (2, 2), (4, 6)]),
(17, 100, &[(1, 4), (4, 4), (20, 4)]),
(2, 10, &[(1, 2), (4, 2)]),
(3, 30, &[(1, 6), (4, 6)]),
];
for &(p, n, expected) in cases {
let f = field(p);
let mut coeffs = vec![f.element(BigInt::from(-1i64))];
coeffs.resize(n + 1, f.element(BigInt::from(0)));
coeffs[n] = f.element(BigInt::from(1));
let poly = DenseUnivariatePolynomial::from_coeffs(f, coeffs);
let factors = factor_over_finite_field(&poly);
let mut obs: std::collections::BTreeMap<usize, usize> = Default::default();
for (g, m) in &factors {
if g.degree().unwrap_or(0) > 0 {
*obs.entry(g.degree().unwrap()).or_insert(0) += *m;
}
}
let observed: Vec<(usize, usize)> = obs.into_iter().collect();
assert_eq!(
observed.as_slice(),
expected,
"x^{n}-1 over F_{p}: degree histogram mismatch",
);
}
}
#[test]
fn factor_repeated_root_over_f7() {
let f = field(7);
let l1 = fpoly(&f, &[-2, 1]); let l2 = fpoly(&f, &[1, 1]); let mut p = product(&[l1.clone(), l1.clone(), l1.clone(), l2.clone()]);
let _ = &mut p;
let factors = factor_over_finite_field(&p);
assert_factors_reconstruct(&p, &factors);
let m2 = factors
.iter()
.filter(|(g, _)| {
g.degree() == Some(1) && g.coeff(0).map(|c| c == &f.element(-2)).unwrap_or(false)
})
.map(|(_, m)| *m)
.next();
assert_eq!(m2, Some(3));
}
#[test]
fn berlekamp_agrees_with_cantor_zassenhaus() {
let cases: &[(u64, &[i64])] = &[
(5, &[4, 0, 1]), (3, &[1, 0, 0, 0, 1]), (5, &[2, 0, 1]), (5, &[1, 0, 0, 0, 1]), (7, &[1, 1, 0, 1]), ];
for &(prime, coeffs) in cases {
let f = field(prime);
let poly = fpoly(&f, coeffs);
let b = berlekamp(&poly)
.into_iter()
.map(|g| monic(&g))
.collect::<Vec<_>>();
let c = cantor_zassenhaus(&poly)
.into_iter()
.map(|g| monic(&g))
.collect::<Vec<_>>();
assert_eq!(b.len(), c.len(), "p={prime}: factor count differs");
let mut b_sig: Vec<Vec<BigInt>> = b
.iter()
.map(|g| g.coeffs().iter().map(|e| e.value().clone()).collect())
.collect();
let mut c_sig: Vec<Vec<BigInt>> = c
.iter()
.map(|g| g.coeffs().iter().map(|e| e.value().clone()).collect())
.collect();
b_sig.sort();
c_sig.sort();
assert_eq!(b_sig, c_sig, "p={prime}: Berlekamp factors differ from CZ");
}
}
}