Skip to main content

ocas_poly/
resultant.rs

1//! Polynomial resultant computation.
2//!
3//! Implements Brown's Polynomial Remainder Sequence (PRS) algorithm for
4//! computing the resultant of two univariate polynomials over any
5//! [`EuclideanDomain`].
6//!
7//! The resultant of two polynomials $a$ and $b$ is zero if and only if
8//! they share a common root (or equivalently, a non-trivial GCD).
9
10use ocas_domain::EuclideanDomain;
11
12use crate::dense::DenseUnivariatePolynomial;
13
14impl<D: EuclideanDomain> DenseUnivariatePolynomial<D> {
15    /// Compute the resultant of `self` and `other` using Brown's PRS algorithm.
16    ///
17    /// The resultant $\operatorname{Res}(a, b)$ is a scalar in the coefficient
18    /// domain. It is zero if and only if $\gcd(a, b)$ is non-constant.
19    ///
20    /// Ported from Symbolica's `resultant_prs` (`src/poly/resultant.rs`):
21    /// subresultant PRS with exact division by `beta` at every step (the
22    /// division is exact in any UFD by the subresultant theorem).
23    ///
24    /// # Example
25    ///
26    /// ```
27    /// use ocas_domain::{IntegerDomain, Integer};
28    /// use ocas_poly::DenseUnivariatePolynomial;
29    ///
30    /// let d = IntegerDomain;
31    /// // Res(x - 1, x - 2) = 1 - 2 = -1
32    /// let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
33    ///     Integer::from(-1), Integer::from(1),
34    /// ]);
35    /// let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
36    ///     Integer::from(-2), Integer::from(1),
37    /// ]);
38    /// assert_eq!(a.resultant(&b), Integer::from(-1));
39    /// ```
40    pub fn resultant(&self, other: &Self) -> D::Element {
41        let d = self.domain();
42
43        // Ensure deg(self) >= deg(other); swap with the sign
44        // Res(a, b) = (-1)^(deg a · deg b) Res(b, a).
45        match (self.degree(), other.degree()) {
46            (None, _) | (_, None) => return d.zero(),
47            (Some(ds), Some(do_)) if ds < do_ => {
48                let r = other.resultant(self);
49                if ds % 2 == 1 && do_ % 2 == 1 {
50                    return d.neg(&r);
51                }
52                return r;
53            }
54            _ => {}
55        }
56
57        let deg_a = self.degree().expect("nonzero polynomial");
58        let deg_b = other.degree().expect("nonzero polynomial");
59
60        // If the smaller polynomial is constant, the resultant is
61        // `constant^(deg of the larger)`.
62        if deg_b == 0 {
63            return d.pow(&other.constant(), deg_a as u64);
64        }
65
66        let mut a = self.clone();
67        let mut a_new = other.clone();
68
69        let mut deg = (a.degree().expect("nonzero") - a_new.degree().expect("nonzero")) as u64;
70        let mut neg_lc = d.one(); // set before use
71        let mut init = false;
72        let mut beta = d.pow(&d.neg(&d.one()), deg + 1);
73        let mut psi = d.neg(&d.one());
74
75        // Collect (leading_coeff, degree) at each step.
76        let mut lcs: Vec<(D::Element, u64)> =
77            vec![(a.lcoeff(), a.degree().expect("nonzero") as u64)];
78
79        while a_new.degree().unwrap_or(0) > 0 {
80            if init {
81                // Update psi and beta.
82                psi = if deg == 0 {
83                    // Can only happen on the first iteration.
84                    psi
85                } else if deg == 1 {
86                    neg_lc.clone()
87                } else {
88                    let num = d.pow(&neg_lc, deg);
89                    let den = d.pow(&psi, deg - 1);
90                    let (q, r) = d
91                        .div_rem(&num, &den)
92                        .expect("subresultant psi division is exact");
93                    debug_assert!(d.is_zero(&r));
94                    q
95                };
96                deg = (a.degree().expect("nonzero") - a_new.degree().expect("nonzero")) as u64;
97                beta = d.mul(&neg_lc, &d.pow(&psi, deg));
98            } else {
99                init = true;
100            }
101
102            neg_lc = d.neg(a_new.leading_coeff().expect("nonzero"));
103
104            // Pseudo-remainder: a · (−lc(b))^(deg+1) mod b, with sign.
105            let factor = d.pow(&neg_lc, deg + 1);
106            let (_, mut r) = a
107                .mul_scalar(&factor)
108                .div_rem(&a_new)
109                .expect("pseudo-division succeeds after scaling");
110            if (deg + 1) % 2 == 1 {
111                r = r.neg();
112            }
113
114            lcs.push((a_new.lcoeff(), a_new.degree().expect("nonzero") as u64));
115
116            // Exact scalar division by beta (subresultant theorem).
117            let r_reduced = Self::from_coeffs(
118                d.clone(),
119                r.coeffs()
120                    .iter()
121                    .map(|c| {
122                        d.div(c, &beta)
123                            .expect("subresultant beta division is exact")
124                    })
125                    .collect(),
126            );
127            a = a_new;
128            a_new = r_reduced;
129        }
130
131        // A zero remainder before reaching a constant means a common factor.
132        if a_new.is_zero() {
133            return d.zero();
134        }
135        lcs.push((a_new.lcoeff(), 0));
136
137        // Compute the resultant from the PRS using the fundamental theorem.
138        let mut rho = d.one();
139        let mut den = d.one();
140
141        for k in 1..lcs.len() {
142            let mut exponent: i64 = lcs[k - 1].1 as i64 - lcs[k].1 as i64;
143            // Multiply by (deg differences from remaining steps).
144            for l in k..lcs.len() - 1 {
145                let dl = lcs[l].1 as i64;
146                let dl1 = lcs[l + 1].1 as i64;
147                exponent *= 1 - (dl - dl1);
148            }
149
150            if exponent > 0 {
151                let pow_val = d.pow(&lcs[k].0, exponent as u64);
152                rho = d.mul(&rho, &pow_val);
153            } else if exponent < 0 {
154                let pow_val = d.pow(&lcs[k].0, (-exponent) as u64);
155                den = d.mul(&den, &pow_val);
156            }
157        }
158
159        d.div_rem(&rho, &den)
160            .expect("resultant reconstruction is exact")
161            .0
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use ocas_domain::{Integer, IntegerDomain};
169
170    fn int(i: i64) -> Integer {
171        Integer::from(i)
172    }
173
174    fn poly(coeffs: &[i64]) -> DenseUnivariatePolynomial<IntegerDomain> {
175        DenseUnivariatePolynomial::from_coeffs(
176            IntegerDomain,
177            coeffs.iter().map(|&c| int(c)).collect(),
178        )
179    }
180
181    #[test]
182    fn resultant_linear_different_roots() {
183        // Res(x - 1, x - 2) = 1 - 2 = -1 (product of (α_i - β_j))
184        let a = poly(&[-1, 1]); // x - 1
185        let b = poly(&[-2, 1]); // x - 2
186        assert_eq!(a.resultant(&b), int(-1));
187    }
188
189    #[test]
190    fn resultant_common_root() {
191        // Res(x^2 - 1, x - 1) = 0 (share root x=1)
192        let a = poly(&[-1, 0, 1]); // x^2 - 1
193        let b = poly(&[-1, 1]); // x - 1
194        assert_eq!(a.resultant(&b), int(0));
195    }
196
197    #[test]
198    fn resultant_no_common_root() {
199        // Res(x^2 + 1, (x+1)^2) = 4
200        let a = poly(&[1, 0, 1]); // x^2 + 1
201        let b = poly(&[1, 2, 1]); // x^2 + 2x + 1
202        assert_eq!(a.resultant(&b), int(4));
203    }
204
205    #[test]
206    fn resultant_shared_factor() {
207        // Res((x-1)(x-2), (x-1)(x-3)) = 0
208        let a = poly(&[2, -3, 1]); // x^2 - 3x + 2
209        let b = poly(&[3, -4, 1]); // x^2 - 4x + 3
210        assert_eq!(a.resultant(&b), int(0));
211    }
212
213    #[test]
214    fn resultant_constant_poly() {
215        // Res(x^2 + 1, 3) = 3^2 = 9
216        let a = poly(&[1, 0, 1]);
217        let b = poly(&[3]);
218        assert_eq!(a.resultant(&b), int(9));
219    }
220
221    #[test]
222    fn resultant_constant_constant() {
223        // Res(2, 3): deg_a=0, deg_b=0, b^deg_a = 3^0 = 1
224        let a = poly(&[2]);
225        let b = poly(&[3]);
226        assert_eq!(a.resultant(&b), int(1));
227    }
228
229    #[test]
230    fn resultant_symmetric_up_to_sign() {
231        // Res(a, b) = (-1)^(deg_a * deg_b) * Res(b, a)
232        let a = poly(&[-1, 0, 1]); // x^2 - 1, deg=2
233        let b = poly(&[-2, 1]); // x - 2, deg=1
234        // deg_a * deg_b = 2, so Res(a,b) = Res(b,a)
235        let r1 = a.resultant(&b);
236        let r2 = b.resultant(&a);
237        assert_eq!(r1, r2);
238    }
239
240    #[test]
241    fn resultant_zero_poly() {
242        let a = poly(&[0]); // zero polynomial
243        let b = poly(&[1, 1]); // x + 1
244        assert_eq!(a.resultant(&b), int(0));
245    }
246
247    #[test]
248    fn resultant_quartic_cubic() {
249        // SymPy: resultant(x^4 - 3, 3x^3 - x^2 + 2x + 1, x) == -2243.
250        // Regression: the previous implementation skipped the beta division
251        // unless beta was a unit, which is not a valid resultant algorithm
252        // beyond trivial degrees.
253        let a = poly(&[-3, 0, 0, 0, 1]);
254        let b = poly(&[1, 2, -1, 3]);
255        assert_eq!(a.resultant(&b), int(-2243));
256        // And swapped (both degrees odd? 4 and 3 — no sign flip).
257        assert_eq!(b.resultant(&a), int(-2243));
258    }
259}