1use ocas_domain::{Domain, FiniteField, Integer, IntegerDomain, number_theory};
19
20use crate::dense::DenseUnivariatePolynomial;
21use crate::factor::finite_field::{self, FpPoly};
22
23pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
25
26fn to_finite_field(f: &ZPoly, p: &Integer) -> FpPoly {
29 let field = FiniteField::new(p.to_bigint());
30 let coeffs = f
31 .coeffs()
32 .iter()
33 .map(|c| field.element(c.to_bigint()))
34 .collect();
35 FpPoly::from_coeffs(field, coeffs)
36}
37
38fn from_finite_field_symmetric(g: &FpPoly) -> ZPoly {
41 let domain = IntegerDomain;
42 let p = g.domain().prime();
43 let p_int = Integer::from(p.clone());
44 let coeffs = g
45 .coeffs()
46 .iter()
47 .map(|c| {
48 let c_int = Integer::from(c.value().clone());
49 number_theory::symmetric_mod(&c_int, &p_int)
50 })
51 .collect();
52 ZPoly::from_coeffs(domain, coeffs)
53}
54
55fn monic_fp(f: &FpPoly) -> FpPoly {
57 if f.is_zero() {
58 return f.zero();
59 }
60 let lc = f.leading_coeff().unwrap();
61 if f.domain().is_one(lc) {
62 return f.clone();
63 }
64 let inv = f.domain().inv(lc).expect("nonzero lc");
65 f.mul_scalar(&inv)
66}
67
68pub(crate) fn mignotte_bound(f: &ZPoly) -> Integer {
74 let n = f.degree().unwrap_or(0);
75 let mut sum_sq = Integer::from(0);
76 for c in f.coeffs() {
77 let v = c.abs();
78 sum_sq += &(&v * &v);
79 }
80 let norm = sum_sq.sqrt() + &Integer::from(1);
81 &Integer::from(2).pow_u32(n as u32) * &norm
82}
83
84fn bezout_mod_p(g: &FpPoly, h: &FpPoly) -> (FpPoly, FpPoly) {
91 let field = g.domain().clone();
92 let one = FpPoly::from_coeffs(field.clone(), vec![field.one()]);
93 let zero = FpPoly::from_coeffs(field.clone(), vec![field.zero()]);
94 let (mut old_r, mut r) = (g.clone(), h.clone());
95 let (mut old_s, mut s) = (one.clone(), zero.clone());
96 let (mut old_t, mut t) = (zero, one);
97 while !r.is_zero() {
98 let (q, rem) = old_r.div_rem(&r).unwrap_or_else(|| (r.zero(), r.zero()));
99 old_r = r;
100 r = rem;
101 let new_s = old_s.sub(&q.mul(&s));
102 let new_t = old_t.sub(&q.mul(&t));
103 old_s = s;
104 s = new_s;
105 old_t = t;
106 t = new_t;
107 }
108 if let Some(c) = old_r.leading_coeff()
110 && !field.is_one(c)
111 {
112 let c_inv = field.inv(c).expect("gcd constant is nonzero");
113 old_s = old_s.mul_scalar(&c_inv);
114 old_t = old_t.mul_scalar(&c_inv);
115 }
116 (old_s, old_t)
117}
118
119fn hensel_lift_pair(
126 f: &ZPoly,
127 g0: &FpPoly,
128 h0: &FpPoly,
129 p: &Integer,
130 bound: &Integer,
131) -> Option<(ZPoly, ZPoly)> {
132 let (s, t) = bezout_mod_p(g0, h0);
134 debug_assert!(
135 {
136 let one = g0.one();
137 let id = s.mul(g0).add(&t.mul(h0));
138 id.sub(&one).is_zero()
139 },
140 "Bézout identity s·g0 + t·h0 = 1 failed"
141 );
142 let mut g = from_finite_field_symmetric(g0);
143 let mut h = from_finite_field_symmetric(h0);
144 let mut m = p.clone();
145
146 loop {
147 let gh = g.mul(&h);
148 let e = f.sub(&gh);
149 if e.is_zero() {
150 return Some((g, h));
151 }
152 let mut e_over_m = Vec::new();
154 for c in e.coeffs() {
155 let (q, r) = c.div_rem(&m);
156 if r.is_zero() {
157 e_over_m.push(q);
158 } else {
159 return None;
160 }
161 }
162 let e_over_m = ZPoly::from_coeffs(IntegerDomain, e_over_m);
163 if e_over_m.is_zero() {
164 return Some((g, h));
165 }
166 let e_bar = to_finite_field(&e_over_m, p);
167 let (_q1, dg) = t.mul(&e_bar).div_rem(g0)?;
169 let dividend = e_bar.sub(&dg.mul(h0));
173 let (dh, dh_rem) = dividend.div_rem(g0)?;
174 debug_assert!(
175 dh_rem.is_zero(),
176 "Δh division not exact; Bézout identity may be broken"
177 );
178 let dg_z = from_finite_field_symmetric(&dg);
179 let dh_z = from_finite_field_symmetric(&dh);
180 let m_int = m.clone();
181 g = g.add(&dg_z.mul_scalar(&m_int));
182 h = h.add(&dh_z.mul_scalar(&m_int));
183 m *= p;
184 if &m > bound {
185 return Some((g, h));
186 }
187 }
188}
189
190fn hensel_lift_multi(
196 f: &ZPoly,
197 factors: &[FpPoly],
198 p: &Integer,
199 bound: &Integer,
200) -> Option<Vec<ZPoly>> {
201 if factors.len() <= 1 {
202 return Some(vec![f.clone()]);
203 }
204 let mut lifted: Vec<ZPoly> = Vec::new();
205 let mut work = factors.to_vec();
206 let mut f_current = f.clone();
207 while work.len() > 1 {
208 let g0 = monic_fp(&work[0].clone());
209 let h0 = monic_fp(&work[1..].iter().cloned().reduce(|a, b| a.mul(&b)).unwrap());
210 let (g, h) = hensel_lift_pair(&f_current, &g0, &h0, p, bound)?;
211 lifted.push(reduce_symmetric(&g, bound));
214 f_current = h;
215 work = work[1..].to_vec();
216 }
217 lifted.push(reduce_symmetric(&f_current, bound));
218 Some(lifted)
219}
220
221fn reduce_symmetric(f: &ZPoly, modulus: &Integer) -> ZPoly {
225 let coeffs = f
226 .coeffs()
227 .iter()
228 .map(|c| number_theory::symmetric_mod(c, modulus))
229 .collect();
230 ZPoly::from_coeffs(IntegerDomain, coeffs)
231}
232
233fn combinations(n: usize, k: usize) -> Vec<Vec<usize>> {
235 let mut out = Vec::new();
236 let mut cur = Vec::new();
237 combos(0, n, k, &mut cur, &mut out);
238 out
239}
240
241fn combos(start: usize, n: usize, k: usize, cur: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
242 if cur.len() == k {
243 out.push(cur.clone());
244 return;
245 }
246 for i in start..n {
247 cur.push(i);
248 combos(i + 1, n, k, cur, out);
249 cur.pop();
250 }
251}
252
253fn zassenhaus_combine(f: &ZPoly, lifted: &[ZPoly], modulus: &Integer) -> Vec<ZPoly> {
262 if lifted.is_empty() {
263 return Vec::new();
264 }
265 let one = f.one();
266 let lc_f = f
267 .leading_coeff()
268 .cloned()
269 .unwrap_or_else(|| Integer::from(1));
270 let lc_f_abs = lc_f.abs();
271 let mut remaining: Vec<ZPoly> = lifted.to_vec();
272 let mut result = Vec::new();
273 let mut size = 1usize;
274 while size <= remaining.len() && !remaining.is_empty() {
275 let n = remaining.len();
276 let mut found = false;
277 for combo in combinations(n, size) {
278 let mut prod = one.clone();
279 for &idx in &combo {
280 prod = prod.mul(&remaining[idx]);
281 }
282 let candidate = reduce_symmetric(&prod, modulus);
285 for d in divisors(&lc_f_abs) {
288 let scaled = candidate.mul_scalar(&d);
289 if let Some((_, r)) = f.div_rem(&scaled)
290 && r.is_zero()
291 && !scaled.is_one()
292 {
293 result.push(scaled);
294 let mut nr = Vec::new();
295 for (i, fac) in remaining.iter().enumerate() {
296 if !combo.contains(&i) {
297 nr.push(fac.clone());
298 }
299 }
300 remaining = nr;
301 found = true;
302 size = 1;
303 break;
304 }
305 }
306 if found {
307 break;
308 }
309 }
310 if !found {
311 size += 1;
312 }
313 }
314 for fac in remaining {
315 if !fac.is_one() {
316 result.push(fac);
317 }
318 }
319 result
320}
321
322fn divisors(n: &Integer) -> Vec<Integer> {
324 if n <= &Integer::from(0) {
325 return Vec::new();
326 }
327 let mut divs = vec![Integer::from(1)];
328 let mut remaining = n.clone();
329 let mut p = Integer::from(2);
330 while &p * &p <= remaining {
331 let mut count = 0u32;
332 while (&remaining % &p).is_zero() {
333 remaining /= &p;
334 count += 1;
335 }
336 if count > 0 {
337 let current = divs.clone();
338 for d in current {
339 for e in 1..=count {
340 let factor = &d * &p.pow_u32(e);
341 divs.push(factor);
342 }
343 }
344 }
345 p += &Integer::from(1);
346 }
347 if remaining > Integer::from(1) {
348 let current = divs.clone();
349 for d in current {
350 divs.push(&d * &remaining);
351 }
352 }
353 divs.sort();
354 divs
355}
356
357pub fn factor_square_free_monic(f: &ZPoly) -> Vec<ZPoly> {
360 if f.degree().unwrap_or(0) == 0 {
361 return Vec::new();
362 }
363 if f.degree() == Some(1) {
364 return vec![f.clone()];
365 }
366 let bound = mignotte_bound(f);
367 let two_bound = &Integer::from(2) * &bound;
368 let lc = f.leading_coeff().unwrap().abs();
369
370 let mut prime_count = 0usize;
371 for p in number_theory::primes_from(&Integer::from(2)) {
372 prime_count += 1;
373 if prime_count > 30 {
374 break;
375 }
376 if (&lc % &p).is_zero() {
377 continue;
378 }
379 let fp = to_finite_field(f, &p);
380 let fpp = fp.derivative();
381 if fpp.is_zero() || fp.gcd(&fpp).degree().unwrap_or(0) > 0 {
382 continue; }
384 let factors_p = finite_field::cantor_zassenhaus(&monic_fp(&fp));
385 if factors_p.len() <= 1 {
386 return vec![f.clone()]; }
388 let mut lift_mod = p.clone();
389 while lift_mod <= two_bound {
390 lift_mod *= &p;
391 }
392 if let Some(lifted) = hensel_lift_multi(f, &factors_p, &p, &lift_mod) {
393 let irreducibles = zassenhaus_combine(f, &lifted, &lift_mod);
394 if !irreducibles.is_empty() {
395 return irreducibles;
396 }
397 }
398 }
399 vec![f.clone()]
400}
401
402pub fn factor_primitive(f: &ZPoly) -> Vec<(ZPoly, usize)> {
407 if f.degree().unwrap_or(0) == 0 {
408 return Vec::new();
409 }
410 let sqfree = f.square_free_factorization();
412 let mut result = Vec::new();
413 for (g, mult) in sqfree {
414 let lc = g.leading_coeff().cloned().unwrap();
417 let sign = if lc.is_negative() {
418 Integer::from(-1i64)
419 } else {
420 Integer::from(1i64)
421 };
422 let g_pos = g.mul_scalar(&sign);
423 for irr in factor_square_free_monic(&g_pos.primitive_part()) {
424 result.push((irr, mult));
425 }
426 }
427 result
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 fn zpoly(coeffs: &[i64]) -> ZPoly {
435 ZPoly::from_coeffs(
436 IntegerDomain,
437 coeffs.iter().map(|&c| Integer::from(c)).collect(),
438 )
439 }
440
441 fn reconstruct(f: &ZPoly, factors: &[ZPoly]) -> bool {
442 let mut acc = f.one();
443 for g in factors {
444 acc = acc.mul(g);
445 }
446 let (q, r) = f.div_rem(&acc).unwrap_or((f.zero(), f.clone()));
447 r.is_zero() && q.degree() == Some(0)
448 }
449
450 #[test]
451 fn factor_x100_minus_1_over_z() {
452 let mut coeffs = vec![Integer::from(-1i64)];
453 coeffs.resize(101, Integer::from(0));
454 coeffs[100] = Integer::from(1);
455 let f = ZPoly::from_coeffs(IntegerDomain, coeffs);
456 let factors = factor_square_free_monic(&f);
457 assert!(reconstruct(&f, &factors));
458 assert_eq!(factors.len(), 9);
460 }
461
462 #[test]
463 fn factor_quadratic_split() {
464 let f = zpoly(&[6, 5, 1]); let factors = factor_square_free_monic(&f);
466 assert!(reconstruct(&f, &factors));
467 assert_eq!(factors.len(), 2);
468 }
469
470 #[test]
471 fn factor_irreducible_quadratic() {
472 let f = zpoly(&[1, 0, 1]); let factors = factor_square_free_monic(&f);
474 assert!(reconstruct(&f, &factors));
475 assert_eq!(factors.len(), 1);
476 }
477
478 #[test]
479 fn factor_three_linear() {
480 let f = zpoly(&[-6, 11, -6, 1]); let factors = factor_square_free_monic(&f);
482 assert!(reconstruct(&f, &factors));
483 assert_eq!(factors.len(), 3);
484 }
485
486 #[test]
487 fn factor_three_mixed() {
488 let a = zpoly(&[1, 0, 1]);
490 let b = zpoly(&[1, 1, 1]);
491 let c = zpoly(&[1, 1]);
492 let f = a.mul(&b).mul(&c);
493 let factors = factor_square_free_monic(&f);
494 assert!(reconstruct(&f, &factors));
495 assert_eq!(factors.len(), 3);
496 }
497
498 #[test]
499 fn mignotte_bound_sanity() {
500 let f = zpoly(&[1, 0, 1]); let b = mignotte_bound(&f);
502 assert!(
505 b >= Integer::from(6) && b <= Integer::from(10),
506 "mignotte(x^2+1) = {b}, expected ~6-10"
507 );
508 }
509
510 #[test]
511 fn factor_cyclotomic_matches_sympy_over_z() {
512 let cases: &[(usize, &[(usize, usize)])] = &[
516 (12, &[(1, 2), (2, 3), (4, 1)]),
517 (30, &[(1, 2), (2, 2), (4, 2), (8, 2)]),
518 (60, &[(1, 2), (2, 3), (4, 3), (8, 3), (16, 1)]),
519 (100, &[(1, 2), (2, 1), (4, 2), (8, 1), (20, 2), (40, 1)]),
520 ];
521 for &(n, expected) in cases {
522 let mut coeffs = vec![Integer::from(-1i64)];
523 coeffs.resize(n + 1, Integer::from(0));
524 coeffs[n] = Integer::from(1);
525 let f = ZPoly::from_coeffs(IntegerDomain, coeffs);
526 let factors = factor_square_free_monic(&f);
527 assert!(
528 reconstruct(&f, &factors),
529 "x^{n}-1: factors do not reconstruct"
530 );
531 let mut obs: std::collections::BTreeMap<usize, usize> = Default::default();
532 for g in &factors {
533 *obs.entry(g.degree().unwrap()).or_insert(0) += 1;
534 }
535 let observed: Vec<(usize, usize)> = obs.into_iter().collect();
536 assert_eq!(
537 observed.as_slice(),
538 expected,
539 "x^{n}-1 over Z: degree histogram mismatch"
540 );
541 }
542 }
543
544 #[test]
545 fn factor_x30_minus_1_over_z() {
546 let mut coeffs = vec![Integer::from(-1i64)];
548 coeffs.resize(31, Integer::from(0));
549 coeffs[30] = Integer::from(1);
550 let f = ZPoly::from_coeffs(IntegerDomain, coeffs);
551 let factors = factor_square_free_monic(&f);
552 assert!(reconstruct(&f, &factors));
553 assert_eq!(factors.len(), 8, "expected 8 cyclotomic factors");
554 }
555}
556
557#[cfg(test)]
558mod proptests {
559 use super::*;
560 use proptest::prelude::*;
561
562 fn any_int_poly(max_degree: usize) -> impl Strategy<Value = ZPoly> {
563 (0..=max_degree)
564 .prop_flat_map(move |deg| {
565 let n = deg + 1;
566 prop::collection::vec(i64_range(), n)
567 })
568 .prop_map(|coeffs| {
569 let c: Vec<Integer> = coeffs.into_iter().map(Integer::from).collect();
570 ZPoly::from_coeffs(IntegerDomain, c)
571 })
572 }
573
574 fn i64_range() -> impl Strategy<Value = i64> {
575 -100i64..=100i64
576 }
577
578 proptest! {
579 #![proptest_config(ProptestConfig::with_cases(500))]
580
581 #[test]
582 fn factor_then_multiply_roundtrip(p in any_int_poly(6)) {
583 let f = p.primitive_part();
585 if f.degree().unwrap_or(0) == 0 {
586 return Ok(());
587 }
588 let factors = factor_primitive(&f);
589 let mut acc = f.one();
590 for (g, m) in &factors {
591 for _ in 0..*m {
592 acc = acc.mul(g);
593 }
594 }
595 if let Some((q, r)) = f.div_rem(&acc) {
597 prop_assert!(r.is_zero());
598 prop_assert_eq!(q.degree(), Some(0));
599 }
600 }
601 }
602}