Skip to main content

ocas_poly/factor/
multivariate.rs

1//! Multivariate polynomial factorization.
2//!
3//! Currently implements bivariate factorization over $\mathbb{Z}$ and
4//! $\mathbb{F}_p$ via evaluation and Hensel lifting (Wang's algorithm).
5//!
6//! The bivariate polynomial is treated as a univariate polynomial in the main
7//! variable $x$ with coefficients in $\mathbb{Z}[y]$ (or $\mathbb{F}_p[y]$).
8//! It is evaluated at $y = \alpha$ to obtain a univariate image over the base
9//! domain, factored there, and the factors are lifted back to bivariate
10//! polynomials by linear Hensel lifting in the ideal $(y - \alpha)$.
11//!
12//! References: Wang (1978), "An Improved Multivariate Polynomial Factoring
13//! Algorithm"; Geddes, Czapor, Labahn, *Algorithms for Computer Algebra*.
14
15use num_bigint::BigInt;
16use num_traits::One;
17use ocas_domain::{
18    Domain, FiniteField, FiniteFieldElement, Integer, IntegerDomain, Rational, RationalDomain,
19};
20
21use crate::dense::DenseUnivariatePolynomial;
22use crate::factor::hensel;
23use crate::sparse::{Lex, MonomialOrder, SparseMultivariatePolynomial};
24
25/// Bivariate polynomial over the integers with lexicographic order.
26pub type ZMPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;
27
28/// Univariate polynomial over the rationals.
29pub type QPoly = DenseUnivariatePolynomial<RationalDomain>;
30
31/// Univariate polynomial over the integers.
32pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
33
34/// Univariate polynomial over a prime finite field.
35pub type FpPoly = DenseUnivariatePolynomial<FiniteField>;
36
37/// Bivariate polynomial over a prime finite field with lexicographic order.
38pub type FpMPoly = SparseMultivariatePolynomial<FiniteField, Lex>;
39
40/// Return the maximum degree of `poly` in variable `var_index`, or `0` for the
41/// zero polynomial.
42fn degree_in_var<D: Domain, O: MonomialOrder>(
43    poly: &SparseMultivariatePolynomial<D, O>,
44    var_index: usize,
45) -> usize {
46    poly.terms_ref()
47        .keys()
48        .map(|e| e.get(var_index).copied().unwrap_or(0))
49        .max()
50        .unwrap_or(0)
51}
52
53/// Evaluate `poly` at `y = value` and interpret the result as a univariate
54/// polynomial in the main variable `x` (variable 0).
55fn eval_to_univariate(poly: &ZMPoly, y_var: usize, value: &Integer) -> ZPoly {
56    let evaluated = poly.eval(y_var, value);
57    let mut coeffs = Vec::new();
58    for (exp, c) in evaluated.terms_ref() {
59        let idx = exp.first().copied().unwrap_or(0);
60        if idx >= coeffs.len() {
61            coeffs.resize(idx + 1, IntegerDomain.zero());
62        }
63        coeffs[idx] = c.clone();
64    }
65    ZPoly::from_coeffs(IntegerDomain, coeffs)
66}
67
68/// Lift a univariate polynomial in `x` back to a bivariate polynomial with
69/// no dependence on the secondary variable `y`.
70fn univariate_to_bivariate(g: &ZPoly, n_vars: usize, x_var: usize) -> ZMPoly {
71    let mut terms = Vec::new();
72    for (i, c) in g.coeffs().iter().enumerate() {
73        if !IntegerDomain.is_zero(c) {
74            let mut exp = vec![0usize; n_vars];
75            exp[x_var] = i;
76            terms.push((exp, c.clone()));
77        }
78    }
79    ZMPoly::from_terms(IntegerDomain, n_vars, terms)
80}
81
82/// Multiply a univariate polynomial in `x` by $(y - \alpha)^k$ and return the
83/// bivariate result. This is used to add a Hensel correction term.
84fn univariate_times_y_minus_alpha_k(
85    g: &ZPoly,
86    k: usize,
87    alpha: &Integer,
88    n_vars: usize,
89    x_var: usize,
90    y_var: usize,
91) -> ZMPoly {
92    let mut terms = Vec::new();
93    for (i, c) in g.coeffs().iter().enumerate() {
94        if IntegerDomain.is_zero(c) {
95            continue;
96        }
97        for j in 0..=k {
98            let mut exp = vec![0usize; n_vars];
99            exp[x_var] = i;
100            exp[y_var] = j;
101            let sign = if (k - j).is_multiple_of(2) {
102                1i64
103            } else {
104                -1i64
105            };
106            let binom = Integer::from(binomial(k, j) as i64);
107            let alpha_pow = alpha.pow_u32((k - j) as u32);
108            let sign_int = Integer::from(sign);
109            let coeff = IntegerDomain.mul(&IntegerDomain.mul(&binom, &sign_int), &alpha_pow);
110            let prod = IntegerDomain.mul(c, &coeff);
111            terms.push((exp, prod));
112        }
113    }
114    ZMPoly::from_terms(IntegerDomain, n_vars, terms)
115}
116
117/// Binomial coefficient $\binom{n}{k}$.
118fn binomial(n: usize, k: usize) -> u64 {
119    if k > n {
120        return 0;
121    }
122    if k == 0 || k == n {
123        return 1;
124    }
125    let k = k.min(n - k);
126    let mut num = 1u64;
127    let mut den = 1u64;
128    for i in 0..k {
129        num *= (n - i) as u64;
130        den *= (i + 1) as u64;
131    }
132    num / den
133}
134
135/// Take the partial derivative of `poly` with respect to `var_index`.
136fn derivative_in_var(poly: &ZMPoly, var_index: usize) -> ZMPoly {
137    let mut result = ZMPoly::new(IntegerDomain, poly.n_vars());
138    for (exp, coeff) in poly.terms_ref() {
139        let power = exp.get(var_index).copied().unwrap_or(0);
140        if power == 0 {
141            continue;
142        }
143        let mut new_exp = exp.to_vec();
144        new_exp[var_index] = power - 1;
145        let scalar = IntegerDomain.cast_u64(power as u64);
146        let new_coeff = IntegerDomain.mul(coeff, &scalar);
147        result.set_term_external(new_exp, new_coeff);
148    }
149    result
150}
151
152/// Compute the Taylor coefficients of `poly` viewed as a polynomial in
153/// $(y - \alpha)$, up to degree `max_k`. The coefficients are univariate
154/// polynomials in `x`.
155fn taylor_coeffs_in_y(poly: &ZMPoly, y_var: usize, alpha: &Integer, max_k: usize) -> Vec<ZPoly> {
156    let mut coeffs = Vec::with_capacity(max_k + 1);
157    let mut current = poly.clone();
158    for k in 0..=max_k {
159        let value = eval_to_univariate(&current, y_var, alpha);
160        coeffs.push(divide_by_k_factorial(value, k));
161        current = derivative_in_var(&current, y_var);
162    }
163    coeffs
164}
165
166/// Divide every coefficient of a univariate integer polynomial by `k!`.
167fn divide_by_k_factorial(poly: ZPoly, k: usize) -> ZPoly {
168    let mut fact = BigInt::one();
169    for i in 1..=k {
170        fact *= BigInt::from(i);
171    }
172    let fact_int = Integer::from(fact);
173    let coeffs = poly
174        .coeffs()
175        .iter()
176        .map(|c| IntegerDomain.div(c, &fact_int).unwrap_or_else(|| c.clone()))
177        .collect();
178    ZPoly::from_coeffs(IntegerDomain, coeffs)
179}
180
181/// Normalize a univariate integer polynomial to be monic by adjusting the sign
182/// if necessary. The input is assumed to be primitive.
183fn monic_zpoly(f: &ZPoly) -> ZPoly {
184    if f.is_zero() {
185        return f.clone();
186    }
187    let lc = f.leading_coeff().cloned().unwrap();
188    if lc.is_negative() {
189        f.mul_scalar(&Integer::from(-1))
190    } else {
191        f.clone()
192    }
193}
194
195/// Factor a primitive univariate integer polynomial into monic irreducible
196/// factors.
197fn factor_univariate_z(f: &ZPoly) -> Vec<ZPoly> {
198    hensel::factor_primitive(f)
199        .into_iter()
200        .map(|(g, _)| monic_zpoly(&g))
201        .collect()
202}
203
204/// Lift a square-free bivariate integer polynomial $f$ from its univariate
205/// factorization at $y = \alpha$ back to a bivariate factorization, assuming
206/// $f$ is monic in $x$ (its leading coefficient in $x$ is an integer constant).
207fn hensel_lift_bivariate(
208    f: &ZMPoly,
209    alpha: &Integer,
210    univariate_factors: &[ZPoly],
211    x_var: usize,
212    y_var: usize,
213) -> Option<Vec<ZMPoly>> {
214    let n_vars = f.n_vars();
215    let d_y = degree_in_var(f, y_var);
216
217    let c_f = taylor_coeffs_in_y(f, y_var, alpha, d_y);
218    let q_factors: Vec<QPoly> = univariate_factors.iter().map(zpoly_to_qpoly).collect();
219    let bezout_q = bezout_coefficients_q(&q_factors);
220
221    let mut lifted: Vec<ZMPoly> = univariate_factors
222        .iter()
223        .map(|g| univariate_to_bivariate(g, n_vars, x_var))
224        .collect();
225
226    for k in 1..=d_y {
227        let mut product = ZMPoly::from_terms(
228            IntegerDomain,
229            n_vars,
230            vec![(vec![0; n_vars], Integer::from(1))],
231        );
232        for g in &lifted {
233            product = product.mul(g);
234        }
235
236        let c_product = taylor_coeffs_in_y(&product, y_var, alpha, d_y);
237        let error = c_f[k].sub(&c_product[k]);
238        let error_q = zpoly_to_qpoly(&error);
239
240        for i in 0..lifted.len() {
241            let delta_q = error_q.mul(&bezout_q[i]);
242            let (_q, remainder_q) = delta_q.div_rem(&q_factors[i]).unwrap();
243            let remainder_z = qpoly_to_zpoly(&remainder_q)?;
244            let correction =
245                univariate_times_y_minus_alpha_k(&remainder_z, k, alpha, n_vars, x_var, y_var);
246            lifted[i] = lifted[i].add(&correction);
247        }
248    }
249
250    Some(lifted)
251}
252
253/// Choose evaluation points $y = \alpha$ such that the univariate image
254/// $f(x, \alpha)$ is square-free and has the fewest irreducible factors.
255/// A "lucky" point for Wang's Hensel lifting must preserve the bivariate
256/// factorization pattern, so fewer factors are preferred over more.
257///
258/// Returns all candidates with at least two factors, ordered from fewest to
259/// most factors, so the caller can retry on unlucky points.
260fn choose_evaluation_points(f: &ZMPoly, y_var: usize) -> Vec<(Integer, Vec<ZPoly>)> {
261    let candidates: [i64; 11] = [0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5];
262    let mut best: Vec<(Integer, Vec<ZPoly>)> = Vec::new();
263    for alpha in candidates {
264        let alpha_int = Integer::from(alpha);
265        let image = eval_to_univariate(f, y_var, &alpha_int);
266        if image.degree().unwrap_or(0) < 1 || !image.is_square_free() {
267            continue;
268        }
269        let factors = factor_univariate_z(&image);
270        if factors.len() < 2 {
271            continue;
272        }
273        // Keep candidates ordered by increasing number of factors.
274        let insert_pos = best
275            .binary_search_by(|(_, b)| b.len().cmp(&factors.len()))
276            .unwrap_or_else(|e| e);
277        best.insert(insert_pos, (alpha_int, factors));
278    }
279    best
280}
281
282/// Check whether a bivariate polynomial is the constant 1.
283fn is_one_mpoly(f: &ZMPoly) -> bool {
284    f.terms_ref().len() == 1
285        && f.terms_ref()
286            .iter()
287            .next()
288            .map(|(e, c)| e.iter().all(|&p| p == 0) && IntegerDomain.is_one(c))
289            .unwrap_or(false)
290}
291
292/// Factor a square-free bivariate integer polynomial that is monic in $x$.
293fn bivariate_factor_square_free_monic(f: &ZMPoly, x_var: usize, y_var: usize) -> Vec<ZMPoly> {
294    if degree_in_var(f, x_var) == 0 || degree_in_var(f, y_var) == 0 {
295        return vec![f.clone()];
296    }
297
298    let candidates = choose_evaluation_points(f, y_var);
299    if candidates.is_empty() {
300        return vec![f.clone()];
301    }
302
303    for (alpha, mut univariate_factors) in candidates {
304        if univariate_factors.len() <= 1 {
305            continue;
306        }
307
308        univariate_factors.sort_by_key(|b| std::cmp::Reverse(b.degree().unwrap_or(0)));
309
310        let lifted = match hensel_lift_bivariate(f, &alpha, &univariate_factors, x_var, y_var) {
311            Some(v) => v,
312            None => continue,
313        };
314
315        let mut product = ZMPoly::from_terms(
316            IntegerDomain,
317            f.n_vars(),
318            vec![(vec![0; f.n_vars()], Integer::from(1))],
319        );
320        for g in &lifted {
321            product = product.mul(g);
322        }
323        if product == f.clone() || product == f.neg() {
324            return lifted;
325        }
326    }
327
328    vec![f.clone()]
329}
330
331/// Check whether the leading coefficient of `f` in variable `x_var` is a
332/// nonzero integer constant (i.e., independent of all other variables).
333fn lc_x_is_constant(f: &ZMPoly, x_var: usize) -> bool {
334    let deg_x = degree_in_var(f, x_var);
335    if deg_x == 0 {
336        return true;
337    }
338    for exp in f.terms_ref().keys() {
339        if exp.get(x_var).copied().unwrap_or(0) == deg_x {
340            for (i, &e) in exp.iter().enumerate() {
341                if i != x_var && e != 0 {
342                    return false;
343                }
344            }
345        }
346    }
347    true
348}
349
350/// Factor a primitive, square-free bivariate integer polynomial into irreducible
351/// factors. Currently requires the leading coefficient in `x` to be a constant.
352fn bivariate_factor_square_free(f: &ZMPoly, x_var: usize, y_var: usize) -> Vec<ZMPoly> {
353    if !lc_x_is_constant(f, x_var) {
354        return vec![f.clone()];
355    }
356    bivariate_factor_square_free_monic(f, x_var, y_var)
357}
358
359/// Factor a bivariate polynomial over the integers into irreducible factors with
360/// multiplicities.
361///
362/// The input is treated as a polynomial in variable `x` (index `x_var`) with
363/// coefficients in $\mathbb{Z}[y]$ (variable index `y_var`). The current
364/// implementation handles the case where the leading coefficient in $x$ is a
365/// nonzero integer constant.
366pub fn bivariate_factor_z(f: &ZMPoly, x_var: usize, y_var: usize) -> Vec<(ZMPoly, usize)> {
367    if f.is_zero() || f.total_degree() == Some(0) {
368        return Vec::new();
369    }
370
371    let content = f.content();
372    let mut result = Vec::new();
373    if !IntegerDomain.is_one(&content) {
374        result.push((
375            ZMPoly::from_terms(
376                IntegerDomain,
377                f.n_vars(),
378                vec![(vec![0; f.n_vars()], content)],
379            ),
380            1,
381        ));
382    }
383
384    let primitive = f.primitive_part();
385    if primitive.total_degree() == Some(0) {
386        return result;
387    }
388
389    let sqfree = square_free_factorization_bivariate(&primitive, x_var, y_var);
390    for (g, m) in sqfree {
391        if is_one_mpoly(&g) {
392            continue;
393        }
394        for irr in bivariate_factor_square_free(&g, x_var, y_var) {
395            result.push((irr, m));
396        }
397    }
398
399    result
400}
401
402/// Square-free factorization of a bivariate integer polynomial using the
403/// heuristic bivariate GCD.
404fn square_free_factorization_bivariate(
405    f: &ZMPoly,
406    x_var: usize,
407    y_var: usize,
408) -> Vec<(ZMPoly, usize)> {
409    let f_deriv = derivative_in_var(f, x_var);
410    let mut g = crate::multivariate_gcd::bivariate_gcd(f, &f_deriv)
411        .unwrap_or_else(|| one_mpoly(f.n_vars()));
412    let mut w = divide_bivariate_by_gcd(f, &g, x_var, y_var);
413
414    let mut result = Vec::new();
415    let mut k = 1usize;
416    while !is_one_mpoly(&w) && w.total_degree() != Some(0) {
417        let h =
418            crate::multivariate_gcd::bivariate_gcd(&w, &g).unwrap_or_else(|| one_mpoly(f.n_vars()));
419        let z = divide_bivariate_by_gcd(&w, &h, x_var, y_var);
420        if !is_one_mpoly(&z) && z.total_degree() != Some(0) {
421            result.push((z, k));
422        }
423        w = h;
424        g = divide_bivariate_by_gcd(&g, &w, x_var, y_var);
425        k += 1;
426    }
427    result
428}
429
430/// The constant polynomial 1 in `n_vars` variables.
431fn one_mpoly(n_vars: usize) -> ZMPoly {
432    ZMPoly::from_terms(
433        IntegerDomain,
434        n_vars,
435        vec![(vec![0; n_vars], Integer::from(1))],
436    )
437}
438
439/// Divide bivariate polynomial `a` by `b` assuming `b` divides `a` exactly in
440/// $(\mathbb{Z}[y])[x]$. Returns `a` if the division fails.
441fn divide_bivariate_by_gcd(a: &ZMPoly, b: &ZMPoly, x_var: usize, y_var: usize) -> ZMPoly {
442    if b.is_zero() || is_one_mpoly(b) {
443        return a.clone();
444    }
445    if a.is_zero() {
446        return a.clone();
447    }
448    let deg_y_a = degree_in_var(a, y_var);
449    let deg_y_b = degree_in_var(b, y_var);
450    let n_points = deg_y_a.max(deg_y_b) + 2;
451
452    let mut images: Vec<(Integer, ZPoly)> = Vec::new();
453    let mut eval_point = Integer::from(0);
454    for _ in 0..n_points + 10 {
455        if images.len() >= n_points {
456            break;
457        }
458        let a_eval = eval_to_univariate(a, y_var, &eval_point);
459        let b_eval = eval_to_univariate(b, y_var, &eval_point);
460        if b_eval.is_zero() || a_eval.is_zero() {
461            eval_point = IntegerDomain.add(&eval_point, &Integer::from(1));
462            continue;
463        }
464        let (q, r) = a_eval.div_rem(&b_eval).unwrap();
465        if !r.is_zero() {
466            eval_point = IntegerDomain.add(&eval_point, &Integer::from(1));
467            continue;
468        }
469        images.push((eval_point.clone(), q));
470        eval_point = IntegerDomain.add(&eval_point, &Integer::from(1));
471    }
472
473    if images.len() < n_points {
474        return a.clone();
475    }
476
477    interpolate_bivariate_quotient(&images, a.n_vars(), x_var, y_var)
478}
479
480/// Interpolate a bivariate quotient from univariate images at distinct y-values.
481fn interpolate_bivariate_quotient(
482    images: &[(Integer, ZPoly)],
483    n_vars: usize,
484    x_var: usize,
485    y_var: usize,
486) -> ZMPoly {
487    let mut result = ZMPoly::new(IntegerDomain, n_vars);
488    if images.is_empty() {
489        return result;
490    }
491    let max_x_deg = images
492        .iter()
493        .map(|(_, g)| g.degree().unwrap_or(0))
494        .max()
495        .unwrap_or(0);
496    for x_pow in 0..=max_x_deg {
497        let mut y_points: Vec<(Integer, Integer)> = Vec::new();
498        for (y_val, g) in images {
499            if let Some(c) = g.coeff(x_pow) {
500                y_points.push((y_val.clone(), c.clone()));
501            }
502        }
503        if y_points.len() < 2 {
504            continue;
505        }
506        let y_poly = lagrange_interpolate(&y_points);
507        for (y_pow, c) in y_poly.coeffs().iter().enumerate() {
508            if !IntegerDomain.is_zero(c) {
509                let mut exp = vec![0; n_vars];
510                exp[x_var] = x_pow;
511                exp[y_var] = y_pow;
512                result.set_term_external(exp, c.clone());
513            }
514        }
515    }
516    result
517}
518
519/// Lagrange interpolation of an integer polynomial from points
520/// $(y_i, v_i)$. Returns a dense univariate polynomial in $y$.
521fn lagrange_interpolate(points: &[(Integer, Integer)]) -> ZPoly {
522    let n = points.len();
523    let mut result = ZPoly::from_coeffs(IntegerDomain, Vec::new());
524    for i in 0..n {
525        let (y_i, v_i) = &points[i];
526        let mut numerator = ZPoly::from_coeffs(IntegerDomain, vec![Integer::from(1)]);
527        let mut denom = Integer::from(1);
528        for (j, (y_j, _v_j)) in points.iter().enumerate().take(n) {
529            if i == j {
530                continue;
531            }
532            let factor = ZPoly::from_coeffs(
533                IntegerDomain,
534                vec![IntegerDomain.neg(y_j), Integer::from(1)],
535            );
536            numerator = numerator.mul(&factor);
537            denom = IntegerDomain.mul(&denom, &IntegerDomain.sub(y_i, y_j));
538        }
539        let q = IntegerDomain
540            .div(v_i, &denom)
541            .expect("lagrange_interpolate: non-exact division");
542        result = result.add(&numerator.mul_scalar(&q));
543    }
544    result
545}
546
547fn zpoly_to_qpoly(f: &ZPoly) -> QPoly {
548    QPoly::from_coeffs(
549        RationalDomain,
550        f.coeffs()
551            .iter()
552            .map(|c| Rational::from_integer(c.clone()))
553            .collect(),
554    )
555}
556
557fn qpoly_to_zpoly(f: &QPoly) -> Option<ZPoly> {
558    let mut coeffs = Vec::new();
559    for r in f.coeffs() {
560        let d = r.denom();
561        if !IntegerDomain.is_one(&d) {
562            return None;
563        }
564        coeffs.push(r.numer());
565    }
566    Some(ZPoly::from_coeffs(IntegerDomain, coeffs))
567}
568
569fn monic_qpoly(f: &QPoly) -> QPoly {
570    if f.is_zero() {
571        return f.clone();
572    }
573    let lc = f.leading_coeff().unwrap();
574    let inv = RationalDomain.inv(lc).unwrap();
575    f.mul_scalar(&inv)
576}
577
578fn extended_gcd_qpoly(a: &QPoly, b: &QPoly) -> (QPoly, QPoly, QPoly) {
579    if b.is_zero() {
580        let monic_a = monic_qpoly(a);
581        let lc = a.leading_coeff().unwrap();
582        let inv = RationalDomain.inv(lc).unwrap();
583        let s = QPoly::from_coeffs(RationalDomain, vec![inv]);
584        return (monic_a, s, QPoly::new(RationalDomain));
585    }
586    if a.degree().unwrap_or(0) < b.degree().unwrap_or(0) {
587        let (g, s, t) = extended_gcd_qpoly(b, a);
588        return (g, t, s);
589    }
590    let (q, r) = a.div_rem(b).expect("Q is a field");
591    let (g, s1, t1) = extended_gcd_qpoly(b, &r);
592    let s = t1.clone();
593    let t = s1.sub(&q.mul(&t1));
594    (g, s, t)
595}
596
597fn bezout_coefficients_q(factors: &[QPoly]) -> Vec<QPoly> {
598    let n = factors.len();
599    if n == 1 {
600        return vec![factors[0].one()];
601    }
602    let mut result = vec![factors[0].zero(); n];
603    result[0] = factors[0].one();
604    let mut accum = factors[0].clone();
605    for i in 1..n {
606        let (_g, s, t) = extended_gcd_qpoly(&accum, &factors[i]);
607        for res in result.iter_mut().take(i) {
608            *res = res.mul(&t);
609        }
610        result[i] = s;
611        accum = accum.mul(&factors[i]);
612    }
613    result
614}
615
616fn eval_to_univariate_fp(poly: &FpMPoly, y_var: usize, value: &FiniteFieldElement) -> FpPoly {
617    let evaluated = poly.eval(y_var, value);
618    let mut coeffs = Vec::new();
619    for (exp, c) in evaluated.terms_ref() {
620        let idx = exp.first().copied().unwrap_or(0);
621        if idx >= coeffs.len() {
622            coeffs.resize(idx + 1, poly.domain().zero());
623        }
624        coeffs[idx] = c.clone();
625    }
626    FpPoly::from_coeffs(poly.domain().clone(), coeffs)
627}
628
629fn univariate_to_bivariate_fp(g: &FpPoly, n_vars: usize, x_var: usize) -> FpMPoly {
630    let mut terms = Vec::new();
631    for (i, c) in g.coeffs().iter().enumerate() {
632        if !g.domain().is_zero(c) {
633            let mut exp = vec![0usize; n_vars];
634            exp[x_var] = i;
635            terms.push((exp, c.clone()));
636        }
637    }
638    FpMPoly::from_terms(g.domain().clone(), n_vars, terms)
639}
640
641fn derivative_in_var_fp(poly: &FpMPoly, var_index: usize) -> FpMPoly {
642    let mut result = FpMPoly::new(poly.domain().clone(), poly.n_vars());
643    for (exp, coeff) in poly.terms_ref() {
644        let power = exp.get(var_index).copied().unwrap_or(0);
645        if power == 0 {
646            continue;
647        }
648        let mut new_exp = exp.to_vec();
649        new_exp[var_index] = power - 1;
650        let scalar = poly.domain().cast_u64(power as u64);
651        let new_coeff = poly.domain().mul(coeff, &scalar);
652        result.set_term_external(new_exp, new_coeff);
653    }
654    result
655}
656
657fn divide_by_k_factorial_fp(poly: FpPoly, k: usize) -> FpPoly {
658    let domain = poly.domain().clone();
659    let mut fact = domain.one();
660    for i in 1..=k {
661        fact = domain.mul(&fact, &domain.cast_u64(i as u64));
662    }
663    let fact_inv = domain.inv(&fact).expect("k! must be invertible mod p");
664    let coeffs = poly
665        .coeffs()
666        .iter()
667        .map(|c| domain.mul(c, &fact_inv))
668        .collect();
669    FpPoly::from_coeffs(domain, coeffs)
670}
671
672fn taylor_coeffs_in_y_fp(
673    poly: &FpMPoly,
674    y_var: usize,
675    alpha: &FiniteFieldElement,
676    max_k: usize,
677) -> Vec<FpPoly> {
678    let mut coeffs = Vec::with_capacity(max_k + 1);
679    let mut current = poly.clone();
680    for k in 0..=max_k {
681        let value = eval_to_univariate_fp(&current, y_var, alpha);
682        coeffs.push(divide_by_k_factorial_fp(value, k));
683        current = derivative_in_var_fp(&current, y_var);
684    }
685    coeffs
686}
687
688fn monic_fppoly(f: &FpPoly) -> FpPoly {
689    if f.is_zero() {
690        return f.clone();
691    }
692    let lc = f.leading_coeff().cloned().unwrap();
693    let inv = f.domain().inv(&lc).expect("nonzero leading coefficient");
694    f.mul_scalar(&inv)
695}
696
697fn extended_gcd_fppoly(a: &FpPoly, b: &FpPoly) -> (FpPoly, FpPoly, FpPoly) {
698    if b.is_zero() {
699        return (a.clone(), a.one(), a.zero());
700    }
701    if a.degree().unwrap_or(0) < b.degree().unwrap_or(0) {
702        let (g, s, t) = extended_gcd_fppoly(b, a);
703        return (g, t, s);
704    }
705    let (q, r) = a.div_rem(b).expect("field division");
706    let (g, s1, t1) = extended_gcd_fppoly(b, &r);
707    let s = t1.clone();
708    let t = s1.sub(&q.mul(&t1));
709    (g, s, t)
710}
711
712fn bezout_coefficients_fp(factors: &[FpPoly]) -> Vec<FpPoly> {
713    let n = factors.len();
714    if n == 1 {
715        return vec![factors[0].one()];
716    }
717    let mut result = vec![factors[0].zero(); n];
718    result[0] = factors[0].one();
719    let mut accum = factors[0].clone();
720    for i in 1..n {
721        let (_g, s, t) = extended_gcd_fppoly(&accum, &factors[i]);
722        for res in result.iter_mut().take(i) {
723            *res = res.mul(&t);
724        }
725        result[i] = s;
726        accum = accum.mul(&factors[i]);
727    }
728    result
729}
730
731fn hensel_lift_bivariate_fp(
732    f: &FpMPoly,
733    alpha: &FiniteFieldElement,
734    univariate_factors: &[FpPoly],
735    x_var: usize,
736    y_var: usize,
737) -> Vec<FpMPoly> {
738    let n_vars = f.n_vars();
739    let d_y = degree_in_var(f, y_var);
740
741    let c_f = taylor_coeffs_in_y_fp(f, y_var, alpha, d_y);
742    let bezout = bezout_coefficients_fp(univariate_factors);
743
744    let mut lifted: Vec<FpMPoly> = univariate_factors
745        .iter()
746        .map(|g| univariate_to_bivariate_fp(g, n_vars, x_var))
747        .collect();
748
749    for k in 1..=d_y {
750        let mut product = FpMPoly::from_terms(
751            f.domain().clone(),
752            n_vars,
753            vec![(vec![0; n_vars], f.domain().one())],
754        );
755        for g in &lifted {
756            product = product.mul(g);
757        }
758
759        let c_product = taylor_coeffs_in_y_fp(&product, y_var, alpha, d_y);
760        let error = c_f[k].sub(&c_product[k]);
761
762        for i in 0..lifted.len() {
763            let delta = error.mul(&bezout[i]);
764            let (_q, remainder) = delta.div_rem(&univariate_factors[i]).unwrap();
765            let correction =
766                univariate_times_y_minus_alpha_k_fp(&remainder, k, alpha, n_vars, x_var, y_var);
767            lifted[i] = lifted[i].add(&correction);
768        }
769    }
770
771    lifted
772}
773
774fn univariate_times_y_minus_alpha_k_fp(
775    g: &FpPoly,
776    k: usize,
777    alpha: &FiniteFieldElement,
778    n_vars: usize,
779    x_var: usize,
780    y_var: usize,
781) -> FpMPoly {
782    let domain = g.domain().clone();
783    let mut terms = Vec::new();
784    for (i, c) in g.coeffs().iter().enumerate() {
785        if domain.is_zero(c) {
786            continue;
787        }
788        for j in 0..=k {
789            let mut exp = vec![0usize; n_vars];
790            exp[x_var] = i;
791            exp[y_var] = j;
792            let alpha_pow = domain.pow(alpha, (k - j) as u64);
793            let binom = domain.cast_u64(binomial(k, j));
794            let sign = if (k - j).is_multiple_of(2) {
795                domain.one()
796            } else {
797                domain.neg(&domain.one())
798            };
799            let coeff = domain.mul(c, &domain.mul(&binom, &domain.mul(&sign, &alpha_pow)));
800            terms.push((exp, coeff));
801        }
802    }
803    FpMPoly::from_terms(domain, n_vars, terms)
804}
805
806fn choose_evaluation_point_fp(
807    f: &FpMPoly,
808    y_var: usize,
809) -> Option<(FiniteFieldElement, Vec<FpPoly>)> {
810    let domain = f.domain().clone();
811    let p = domain.prime().clone();
812    let mut best: Option<(FiniteFieldElement, Vec<FpPoly>)> = None;
813    for a in 0i64..20 {
814        if BigInt::from(a) >= p {
815            break;
816        }
817        let alpha = domain.element(a);
818        let image = eval_to_univariate_fp(f, y_var, &alpha);
819        if image.degree().unwrap_or(0) < 1 || !image.is_square_free() {
820            continue;
821        }
822        let mut factors = image.factor();
823        factors.sort_by_key(|a| std::cmp::Reverse(a.0.degree().unwrap_or(0)));
824        let factors: Vec<FpPoly> = factors.into_iter().map(|(g, _)| monic_fppoly(&g)).collect();
825        if factors.len() < 2 {
826            continue;
827        }
828        match &best {
829            None => best = Some((alpha.clone(), factors)),
830            Some((_, best_factors)) => {
831                if factors.len() < best_factors.len() {
832                    best = Some((alpha.clone(), factors));
833                }
834            }
835        }
836    }
837    best
838}
839
840fn lc_x_is_constant_fp(f: &FpMPoly, x_var: usize) -> bool {
841    let deg_x = degree_in_var(f, x_var);
842    if deg_x == 0 {
843        return true;
844    }
845    for exp in f.terms_ref().keys() {
846        if exp.get(x_var).copied().unwrap_or(0) == deg_x {
847            for (i, &e) in exp.iter().enumerate() {
848                if i != x_var && e != 0 {
849                    return false;
850                }
851            }
852        }
853    }
854    true
855}
856
857fn bivariate_factor_square_free_monic_fp(f: &FpMPoly, x_var: usize, y_var: usize) -> Vec<FpMPoly> {
858    if degree_in_var(f, x_var) == 0 || degree_in_var(f, y_var) == 0 {
859        return vec![f.clone()];
860    }
861
862    let (alpha, univariate_factors) = match choose_evaluation_point_fp(f, y_var) {
863        Some(v) => v,
864        None => return vec![f.clone()],
865    };
866
867    if univariate_factors.len() <= 1 {
868        return vec![f.clone()];
869    }
870
871    let lifted = hensel_lift_bivariate_fp(f, &alpha, &univariate_factors, x_var, y_var);
872
873    let mut product = FpMPoly::from_terms(
874        f.domain().clone(),
875        f.n_vars(),
876        vec![(vec![0; f.n_vars()], f.domain().one())],
877    );
878    for g in &lifted {
879        product = product.mul(g);
880    }
881    if product == f.clone() {
882        return lifted;
883    }
884
885    vec![f.clone()]
886}
887
888/// Factor a square-free bivariate polynomial over a prime finite field into
889/// irreducible factors. The current implementation requires the leading
890/// coefficient in `x` to be a nonzero field constant.
891fn bivariate_factor_square_free_fp(f: &FpMPoly, x_var: usize, y_var: usize) -> Vec<FpMPoly> {
892    if !lc_x_is_constant_fp(f, x_var) {
893        return vec![f.clone()];
894    }
895    bivariate_factor_square_free_monic_fp(f, x_var, y_var)
896}
897
898/// Factor a bivariate polynomial over a prime finite field into irreducible
899/// factors with multiplicities.
900///
901/// The current implementation handles square-free polynomials whose leading
902/// coefficient in `x` is a field constant. Non-square-free inputs are returned
903/// as a single factor (a conservative fallback).
904pub fn bivariate_factor_fp(f: &FpMPoly, x_var: usize, y_var: usize) -> Vec<(FpMPoly, usize)> {
905    if f.is_zero() || f.total_degree() == Some(0) {
906        return Vec::new();
907    }
908
909    let content = f.content();
910    let mut result = Vec::new();
911    if !f.domain().is_one(&content) {
912        result.push((
913            FpMPoly::from_terms(
914                f.domain().clone(),
915                f.n_vars(),
916                vec![(vec![0; f.n_vars()], content)],
917            ),
918            1,
919        ));
920    }
921
922    let primitive = f.primitive_part();
923    if primitive.total_degree() == Some(0) {
924        return result;
925    }
926
927    // Conservative square-free check: derivative in x must be non-zero.
928    let deriv = derivative_in_var_fp(&primitive, x_var);
929    if !deriv.is_zero() {
930        for irr in bivariate_factor_square_free_fp(&primitive, x_var, y_var) {
931            result.push((irr, 1));
932        }
933    } else {
934        result.push((primitive, 1));
935    }
936
937    result
938}
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943    use num_bigint::BigInt;
944    use ocas_domain::{FiniteField, Integer};
945
946    fn mpoly_from_str(coeffs: &[((usize, usize), i64)]) -> ZMPoly {
947        let terms: Vec<(Vec<usize>, Integer)> = coeffs
948            .iter()
949            .map(|((x, y), c)| (vec![*x, *y], Integer::from(*c)))
950            .collect();
951        ZMPoly::from_terms(IntegerDomain, 2, terms)
952    }
953
954    fn fpoly_from_str(coeffs: &[((usize, usize), i64)], p: i64) -> FpMPoly {
955        let domain = FiniteField::new(BigInt::from(p));
956        let terms: Vec<(Vec<usize>, FiniteFieldElement)> = coeffs
957            .iter()
958            .map(|((x, y), c)| (vec![*x, *y], domain.element(*c)))
959            .collect();
960        FpMPoly::from_terms(domain, 2, terms)
961    }
962
963    fn one_mpoly_fp(n_vars: usize, domain: &FiniteField) -> FpMPoly {
964        FpMPoly::from_terms(
965            domain.clone(),
966            n_vars,
967            vec![(vec![0; n_vars], domain.one())],
968        )
969    }
970
971    #[test]
972    fn factor_monic_bivariate() {
973        // (x^2 + y + 1)(x + y + 2)
974        // = x^3 + x^2*y + 2*x^2 + x*y + y^2 + 3*y + x + 2
975        let f = mpoly_from_str(&[
976            ((3, 0), 1),
977            ((2, 1), 1),
978            ((2, 0), 2),
979            ((1, 1), 1),
980            ((1, 0), 1),
981            ((0, 2), 1),
982            ((0, 1), 3),
983            ((0, 0), 2),
984        ]);
985        let factors = bivariate_factor_z(&f, 0, 1);
986        let mut product = one_mpoly(2);
987        for (g, m) in &factors {
988            for _ in 0..*m {
989                product = product.mul(g);
990            }
991        }
992        assert!(
993            product == f || product == f.neg(),
994            "product did not reconstruct f"
995        );
996        assert!(factors.len() >= 2, "expected at least two factors");
997    }
998
999    #[test]
1000    #[ignore = "non-monic leading coefficient requires Wang LC handling"]
1001    fn factor_textbook_bivariate_non_monic() {
1002        // (x^2 + y + x + 1)(3x + y^2 + 4)
1003        // Leading coefficient in x is 3, so this case requires the full
1004        // Wang leading-coefficient algorithm (currently not implemented).
1005        let f = mpoly_from_str(&[
1006            ((3, 0), 3),
1007            ((2, 2), 1),
1008            ((2, 0), 7),
1009            ((1, 2), 1),
1010            ((1, 1), 3),
1011            ((1, 0), 7),
1012            ((0, 3), 1),
1013            ((0, 2), 1),
1014            ((0, 1), 4),
1015            ((0, 0), 4),
1016        ]);
1017        let factors = bivariate_factor_z(&f, 0, 1);
1018        let mut product = one_mpoly(2);
1019        for (g, m) in &factors {
1020            for _ in 0..*m {
1021                product = product.mul(g);
1022            }
1023        }
1024        assert!(
1025            product == f || product == f.neg(),
1026            "product did not reconstruct f"
1027        );
1028        assert!(factors.len() >= 2, "expected at least two factors");
1029    }
1030
1031    #[test]
1032    fn factor_monic_bivariate_over_finite_field() {
1033        // (x^2 + y + 1)(x + y + 2) over F_5
1034        // = x^3 + x^2*y + 2*x^2 + x*y + y^2 + 3*y + x + 2  (mod 5)
1035        let f = fpoly_from_str(
1036            &[
1037                ((3, 0), 1),
1038                ((2, 1), 1),
1039                ((2, 0), 2),
1040                ((1, 1), 1),
1041                ((1, 0), 1),
1042                ((0, 2), 1),
1043                ((0, 1), 3),
1044                ((0, 0), 2),
1045            ],
1046            5,
1047        );
1048        let factors = bivariate_factor_fp(&f, 0, 1);
1049        let mut product = one_mpoly_fp(2, f.domain());
1050        for (g, m) in &factors {
1051            for _ in 0..*m {
1052                product = product.mul(g);
1053            }
1054        }
1055        assert_eq!(product, f, "product did not reconstruct f");
1056        assert!(factors.len() >= 2, "expected at least two factors");
1057    }
1058}