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    /// # Example
21    ///
22    /// ```
23    /// use ocas_domain::{IntegerDomain, Integer};
24    /// use ocas_poly::DenseUnivariatePolynomial;
25    ///
26    /// let d = IntegerDomain;
27    /// // Res(x - 1, x - 2) = 1 - 2 = -1
28    /// let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
29    ///     Integer::from(-1), Integer::from(1),
30    /// ]);
31    /// let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
32    ///     Integer::from(-2), Integer::from(1),
33    /// ]);
34    /// assert_eq!(a.resultant(&b), Integer::from(-1));
35    /// ```
36    pub fn resultant(&self, other: &Self) -> D::Element {
37        let d = self.domain();
38
39        // Ensure deg(a) >= deg(b).
40        let (a, b, swapped) = match (self.degree(), other.degree()) {
41            (None, _) | (_, None) => return d.zero(),
42            (Some(da), Some(db)) => {
43                if da >= db {
44                    (self.clone(), other.clone(), false)
45                } else {
46                    (other.clone(), self.clone(), true)
47                }
48            }
49        };
50
51        let deg_a = a.degree().unwrap();
52        let deg_b = b.degree().unwrap();
53
54        // If b is constant, return b^(deg a).
55        if deg_b == 0 {
56            let val = b.constant();
57            let res = d.pow(&val, deg_a as u64);
58            // Sign correction: (-1)^(deg_a * deg_b) when swapped.
59            if swapped && (deg_a * deg_b) % 2 == 1 {
60                return d.neg(&res);
61            }
62            return res;
63        }
64
65        // Run Brown's PRS.
66        let mut a_cur = a;
67        let mut b_cur = b;
68
69        let deg_diff = a_cur.degree().unwrap() - b_cur.degree().unwrap();
70        let neg_lc = d.neg(b_cur.leading_coeff().unwrap());
71        let mut beta = d.pow(&d.neg(&d.one()), (deg_diff + 1) as u64);
72        let mut psi = d.neg(&d.one());
73
74        // Collect (leading_coeff, degree) at each step.
75        let mut lcs: Vec<(D::Element, usize)> = Vec::new();
76        lcs.push((a_cur.lcoeff(), a_cur.degree().unwrap()));
77
78        let mut first = true;
79
80        while !b_cur.is_zero() {
81            let b_deg = b_cur.degree().unwrap();
82
83            if !first {
84                // Update psi and beta.
85                let cur_deg_diff = a_cur.degree().unwrap() - b_deg;
86                psi = if cur_deg_diff == 0 {
87                    psi
88                } else if cur_deg_diff == 1 {
89                    neg_lc.clone()
90                } else {
91                    let a_part = d.pow(&neg_lc, cur_deg_diff as u64);
92                    let psi_old = d.pow(&psi, (cur_deg_diff - 1) as u64);
93                    d.div_rem(&a_part, &psi_old).unwrap().0
94                };
95
96                let new_deg_diff = a_cur.degree().unwrap() - b_deg;
97                beta = d.mul(&neg_lc, &d.pow(&psi, new_deg_diff as u64));
98            }
99            first = false;
100
101            let neg_lc_new = d.neg(b_cur.leading_coeff().unwrap());
102            let deg_diff_now = a_cur.degree().unwrap() - b_deg;
103
104            // Compute pseudo-remainder: a * (-lc)^(deg_diff+1) mod b.
105            let factor = d.pow(&neg_lc_new, (deg_diff_now + 1) as u64);
106            let scaled = a_cur.mul_scalar(&factor);
107            let (_, mut r) = scaled.div_rem(&b_cur).unwrap();
108
109            // Sign correction: (-1)^(deg_diff + 1).
110            if (deg_diff_now + 1) % 2 == 1 {
111                r = r.neg();
112            }
113
114            // Normalize by beta.
115            if !d.is_zero(&beta) {
116                let inv_beta = d.div_rem(&d.one(), &beta);
117                if let Some((q, rem)) = inv_beta
118                    && d.is_zero(&rem)
119                {
120                    r = r.mul_scalar(&q);
121                }
122            }
123
124            lcs.push((b_cur.lcoeff(), b_deg));
125
126            a_cur = b_cur;
127            b_cur = r;
128        }
129
130        // If the last non-zero polynomial is not constant, the GCD is
131        // non-trivial and the resultant is zero.
132        if let Some(last_deg) = b_cur.degree()
133            && last_deg > 0
134        {
135            return d.zero();
136        }
137        // b_cur is now zero; check if a_cur is a non-constant GCD.
138        if a_cur.degree().unwrap_or(0) > 0 {
139            return d.zero();
140        }
141
142        // Compute resultant from PRS using the fundamental theorem.
143        lcs.push((a_cur.lcoeff(), 0));
144
145        let mut rho = d.one();
146        let mut den = d.one();
147
148        for k in 1..lcs.len() {
149            let deg_k_prev = lcs[k - 1].1 as i64;
150            let deg_k = lcs[k].1 as i64;
151            #[allow(unused_variables)]
152            let deg_k_next = if k + 1 < lcs.len() {
153                lcs[k + 1].1 as i64
154            } else {
155                0
156            };
157
158            let mut exponent: i64 = deg_k_prev - deg_k;
159            // Multiply by (deg differences from remaining steps).
160            for l in k..lcs.len() - 1 {
161                let dl = lcs[l].1 as i64;
162                let dl1 = lcs[l + 1].1 as i64;
163                exponent *= 1 - (dl - dl1);
164            }
165
166            if exponent > 0 {
167                let pow_val = d.pow(&lcs[k].0, exponent as u64);
168                rho = d.mul(&rho, &pow_val);
169            } else if exponent < 0 {
170                let pow_val = d.pow(&lcs[k].0, (-exponent) as u64);
171                den = d.mul(&den, &pow_val);
172            }
173        }
174
175        let result = d.div_rem(&rho, &den).unwrap().0;
176
177        // Sign correction for swapping.
178        if swapped && (deg_a * deg_b) % 2 == 1 {
179            d.neg(&result)
180        } else {
181            result
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use ocas_domain::{Integer, IntegerDomain};
190
191    fn int(i: i64) -> Integer {
192        Integer::from(i)
193    }
194
195    fn poly(coeffs: &[i64]) -> DenseUnivariatePolynomial<IntegerDomain> {
196        DenseUnivariatePolynomial::from_coeffs(
197            IntegerDomain,
198            coeffs.iter().map(|&c| int(c)).collect(),
199        )
200    }
201
202    #[test]
203    fn resultant_linear_different_roots() {
204        // Res(x - 1, x - 2) = 1 - 2 = -1 (product of (α_i - β_j))
205        let a = poly(&[-1, 1]); // x - 1
206        let b = poly(&[-2, 1]); // x - 2
207        assert_eq!(a.resultant(&b), int(-1));
208    }
209
210    #[test]
211    fn resultant_common_root() {
212        // Res(x^2 - 1, x - 1) = 0 (share root x=1)
213        let a = poly(&[-1, 0, 1]); // x^2 - 1
214        let b = poly(&[-1, 1]); // x - 1
215        assert_eq!(a.resultant(&b), int(0));
216    }
217
218    #[test]
219    fn resultant_no_common_root() {
220        // Res(x^2 + 1, (x+1)^2) = 4
221        let a = poly(&[1, 0, 1]); // x^2 + 1
222        let b = poly(&[1, 2, 1]); // x^2 + 2x + 1
223        assert_eq!(a.resultant(&b), int(4));
224    }
225
226    #[test]
227    fn resultant_shared_factor() {
228        // Res((x-1)(x-2), (x-1)(x-3)) = 0
229        let a = poly(&[2, -3, 1]); // x^2 - 3x + 2
230        let b = poly(&[3, -4, 1]); // x^2 - 4x + 3
231        assert_eq!(a.resultant(&b), int(0));
232    }
233
234    #[test]
235    fn resultant_constant_poly() {
236        // Res(x^2 + 1, 3) = 3^2 = 9
237        let a = poly(&[1, 0, 1]);
238        let b = poly(&[3]);
239        assert_eq!(a.resultant(&b), int(9));
240    }
241
242    #[test]
243    fn resultant_constant_constant() {
244        // Res(2, 3): deg_a=0, deg_b=0, b^deg_a = 3^0 = 1
245        let a = poly(&[2]);
246        let b = poly(&[3]);
247        assert_eq!(a.resultant(&b), int(1));
248    }
249
250    #[test]
251    fn resultant_symmetric_up_to_sign() {
252        // Res(a, b) = (-1)^(deg_a * deg_b) * Res(b, a)
253        let a = poly(&[-1, 0, 1]); // x^2 - 1, deg=2
254        let b = poly(&[-2, 1]); // x - 2, deg=1
255        // deg_a * deg_b = 2, so Res(a,b) = Res(b,a)
256        let r1 = a.resultant(&b);
257        let r2 = b.resultant(&a);
258        assert_eq!(r1, r2);
259    }
260
261    #[test]
262    fn resultant_zero_poly() {
263        let a = poly(&[0]); // zero polynomial
264        let b = poly(&[1, 1]); // x + 1
265        assert_eq!(a.resultant(&b), int(0));
266    }
267}