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