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}