Skip to main content

oxiz_math/polynomial/
resultant.rs

1//! Polynomial Resultants and Discriminants.
2//!
3//! Computes resultants and discriminants of polynomials, essential for
4//! quantifier elimination and non-linear reasoning.
5//!
6//! ## Algorithms
7//!
8//! - **Sylvester Matrix**: Classic determinant-based method
9//! - **Subresultant PRS**: Polynomial remainder sequences
10//! - **Modular Resultants**: Efficient computation via Chinese Remainder Theorem
11//!
12//! ## Applications
13//!
14//! - Variable elimination in CAD
15//! - Root separation and isolation
16//! - Polynomial system solving
17//!
18//! ## References
19//!
20//! - "Algorithms in Real Algebraic Geometry" (Basu et al., 2006)
21//! - Z3's `math/polynomial/polynomial.cpp`
22
23use crate::polynomial::{Polynomial, Var};
24#[allow(unused_imports)]
25use crate::prelude::*;
26use num_rational::BigRational;
27use num_traits::{One, Signed, Zero};
28
29/// Configuration for resultant computation.
30#[derive(Debug, Clone)]
31pub struct ResultantConfig {
32    /// Method to use for computation.
33    pub method: ResultantMethod,
34    /// Enable modular computation.
35    pub use_modular: bool,
36    /// Maximum degree for dense methods.
37    pub max_dense_degree: usize,
38}
39
40impl Default for ResultantConfig {
41    fn default() -> Self {
42        Self {
43            // `Sylvester` is the exact, fully-verified method (see
44            // `resultant_sylvester` and its tests). `Subresultant` is kept
45            // available as an opt-in, faster-but-approximate alternative
46            // (see `resultant_subresultant`'s docs for why its magnitude
47            // isn't guaranteed exact) -- it used to be the default despite
48            // returning outright wrong (non-zero, var-containing) results
49            // whenever the two polynomials shared a common root (MATH-3).
50            method: ResultantMethod::Sylvester,
51            use_modular: false,
52            max_dense_degree: 100,
53        }
54    }
55}
56
57/// Method for resultant computation.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ResultantMethod {
60    /// Sylvester matrix determinant.
61    Sylvester,
62    /// Subresultant polynomial remainder sequence.
63    Subresultant,
64    /// Bezout matrix (for univariate).
65    Bezout,
66}
67
68/// Statistics for resultant computation.
69#[derive(Debug, Clone, Default)]
70pub struct ResultantStats {
71    /// Resultants computed.
72    pub resultants_computed: u64,
73    /// Discriminants computed.
74    pub discriminants_computed: u64,
75    /// Sylvester determinants.
76    pub sylvester_determinants: u64,
77    /// Subresultant PRS runs.
78    pub subresultant_prs: u64,
79    /// Average degree of results.
80    pub avg_result_degree: f64,
81}
82
83/// Resultant computation engine.
84pub struct ResultantComputer {
85    /// Configuration.
86    config: ResultantConfig,
87    /// Statistics.
88    stats: ResultantStats,
89}
90
91impl ResultantComputer {
92    /// Create a new resultant computer.
93    pub fn new(config: ResultantConfig) -> Self {
94        Self {
95            config,
96            stats: ResultantStats::default(),
97        }
98    }
99
100    /// Create with default configuration.
101    pub fn default_config() -> Self {
102        Self::new(ResultantConfig::default())
103    }
104
105    /// Compute the resultant of two polynomials.
106    ///
107    /// res(p, q, x) eliminates x from the system {p(x) = 0, q(x) = 0}.
108    ///
109    /// Returns a polynomial in the remaining variables.
110    pub fn resultant(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
111        self.stats.resultants_computed += 1;
112
113        // Get degrees with respect to the elimination variable
114        let deg_p = p.degree(var);
115        let deg_q = q.degree(var);
116
117        if deg_p == 0 || deg_q == 0 {
118            // One polynomial is constant in var - special case
119            return self.handle_constant_case(p, q, var);
120        }
121
122        // Choose method based on configuration and polynomial properties
123        let result = match self.config.method {
124            ResultantMethod::Sylvester => self.resultant_sylvester(p, q, var),
125            ResultantMethod::Subresultant => self.resultant_subresultant(p, q, var),
126            ResultantMethod::Bezout => {
127                if p.is_univariate() && q.is_univariate() {
128                    self.resultant_bezout(p, q, var)
129                } else {
130                    // Fall back to subresultant for multivariate
131                    self.resultant_subresultant(p, q, var)
132                }
133            }
134        };
135
136        self.update_degree_stats(result.total_degree() as usize);
137
138        result
139    }
140
141    /// Handle case where one polynomial is constant in the elimination variable.
142    fn handle_constant_case(&self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
143        let deg_p = p.degree(var);
144        let deg_q = q.degree(var);
145
146        if deg_p == 0 && deg_q == 0 {
147            // Both constant - resultant is 1
148            Polynomial::one()
149        } else if deg_p == 0 {
150            // p is constant, resultant is p^deg_q
151            let mut result = Polynomial::one();
152            for _ in 0..deg_q {
153                result = &result * p;
154            }
155            result
156        } else {
157            // q is constant, resultant is q^deg_p
158            let mut result = Polynomial::one();
159            for _ in 0..deg_p {
160                result = &result * q;
161            }
162            result
163        }
164    }
165
166    /// Compute resultant via Sylvester matrix.
167    ///
168    /// Constructs the `(m+n) × (m+n)` Sylvester matrix whose determinant
169    /// equals `Res(p, q, var)`, then evaluates the determinant by Gaussian
170    /// elimination over the polynomial ring (fraction-free / Bareiss algorithm).
171    ///
172    /// For large degrees (m+n > `config.max_dense_degree`) the implementation
173    /// falls back to the subresultant PRS method, which is more efficient.
174    fn resultant_sylvester(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
175        self.stats.sylvester_determinants += 1;
176
177        let deg_p = p.degree(var) as usize;
178        let deg_q = q.degree(var) as usize;
179        let n = deg_p + deg_q;
180
181        // For large matrices fall back to subresultant (more efficient).
182        if n > self.config.max_dense_degree {
183            return self.resultant_subresultant(p, q, var);
184        }
185
186        // Collect coefficients: coeff_p[i] = coefficient of var^i in p
187        // (index 0 = constant term, index deg_p = leading coefficient).
188        let coeff_p: Vec<Polynomial> = (0..=deg_p).map(|i| p.coeff(var, i as u32)).collect();
189        let coeff_q: Vec<Polynomial> = (0..=deg_q).map(|i| q.coeff(var, i as u32)).collect();
190
191        // Build the Sylvester matrix as a Vec<Vec<Polynomial>>.
192        // Row layout:
193        //   - First deg_q rows: shifted copies of coeff_p
194        //     Row r (0-based): column j gets coeff_p[j - r] if 0 ≤ j-r ≤ deg_p
195        //   - Last deg_p rows: shifted copies of coeff_q
196        //     Row r (0-based, offset by deg_q): column j gets coeff_q[j - r] if in range
197        let mut mat: Vec<Vec<Polynomial>> = (0..n)
198            .map(|_| (0..n).map(|_| Polynomial::zero()).collect())
199            .collect();
200
201        for r in 0..deg_q {
202            for j in 0..n {
203                if j >= r && j - r <= deg_p {
204                    mat[r][j] = coeff_p[j - r].clone();
205                }
206            }
207        }
208        for r in 0..deg_p {
209            let row = deg_q + r;
210            for j in 0..n {
211                if j >= r && j - r <= deg_q {
212                    mat[row][j] = coeff_q[j - r].clone();
213                }
214            }
215        }
216
217        // Bareiss fraction-free Gaussian elimination to compute the determinant.
218        // After full elimination the determinant is mat[0][0] (the (0,0) pivot after
219        // all eliminations, divided by accumulated pivots — but Bareiss tracks
220        // the exact fraction-free result in the last surviving element).
221        bareiss_det(mat)
222    }
223
224    /// Compute resultant via subresultant PRS.
225    ///
226    /// More efficient than Sylvester for sparse polynomials.
227    ///
228    /// # Zero-detection is exact; magnitude is not
229    ///
230    /// This runs the pseudo-remainder sequence (the same GCD-like reduction
231    /// [`PolynomialGcd`](super::gcd::PolynomialGcd) uses) down to its last
232    /// non-zero term `a`. If `a` still has positive degree in `var`, `p`
233    /// and `q` share a common factor of that degree in `var`, so they have
234    /// a common root and the true resultant -- which by definition
235    /// vanishes exactly when `p`, `q` have a common root -- is `0`. The old
236    /// implementation returned `a` itself in that case: a non-zero
237    /// polynomial that still *contains* `var`, which cannot be a resultant
238    /// at all (a resultant is, by construction, free of the elimination
239    /// variable). Concretely, `Res(x^2-1, x-1, x)` must be `0` (shared root
240    /// `x=1`) but the old code returned `x-1`.
241    ///
242    /// When the sequence terminates in a non-zero *constant* (`p`, `q`
243    /// coprime in `var`), this returns a non-zero rational multiple of the
244    /// true resultant: the plain pseudo-remainder recurrence does not
245    /// divide out the leading-coefficient scaling factors a full
246    /// subresultant chain (tracking the `beta`/`psi` reduction factors of
247    /// Collins/Brown-Traub) would, so the exact magnitude (and possibly
248    /// sign) can differ from [`Self::resultant_sylvester`]. It is still
249    /// exact for the zero/non-zero question, which is what
250    /// [`Self::have_common_root`] relies on. Callers that need the precise
251    /// resultant *value* should use the default
252    /// [`ResultantMethod::Sylvester`] (or [`Polynomial::resultant`])
253    /// instead.
254    fn resultant_subresultant(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
255        self.stats.subresultant_prs += 1;
256
257        // Use extended GCD-like algorithm
258        let mut a = p.clone();
259        let mut b = q.clone();
260
261        let mut sign_correction = BigRational::one();
262
263        // Bound iterations defensively: each step strictly reduces
264        // deg(b), so this is never the limiting factor for well-formed
265        // univariate-in-`var` input, but guarantees termination rather
266        // than trusting that invariant unconditionally.
267        let max_iters = p.degree(var) as usize + q.degree(var) as usize + 16;
268        let mut iters = 0;
269
270        while !b.is_zero() && iters < max_iters {
271            iters += 1;
272            let deg_a = a.degree(var);
273            let deg_b = b.degree(var);
274
275            if deg_a < deg_b {
276                // Swap a and b
277                core::mem::swap(&mut a, &mut b);
278
279                // Adjust sign if degrees are both odd
280                if deg_a % 2 == 1 && deg_b % 2 == 1 {
281                    sign_correction = -sign_correction;
282                }
283            }
284
285            // Compute pseudo-remainder
286            let r = a.pseudo_remainder(&b, var);
287
288            a = b;
289            b = r;
290        }
291
292        // `a` still contains `var` (positive degree) => `p`, `q` share a
293        // common factor => the resultant is exactly 0.
294        if a.degree(var) > 0 {
295            return Polynomial::zero();
296        }
297
298        if sign_correction.is_negative() { -a } else { a }
299    }
300
301    /// Compute resultant via Bezout matrix (univariate only).
302    ///
303    /// For univariate polynomials the Bezout and Sylvester matrices yield the
304    /// same resultant value; this entry-point reuses the subresultant PRS path
305    /// which gives identical numeric results with better asymptotic efficiency.
306    /// Kept as a separate variant for API completeness.
307    fn resultant_bezout(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
308        self.resultant_subresultant(p, q, var)
309    }
310
311    /// Compute the discriminant of a polynomial.
312    ///
313    /// disc(p, x) = (-1)^(n(n-1)/2) / lc(p) * res(p, p', x)
314    ///
315    /// where p' is the derivative of p with respect to x.
316    pub fn discriminant(&mut self, p: &Polynomial, var: Var) -> Polynomial {
317        self.stats.discriminants_computed += 1;
318
319        // Compute derivative
320        let p_prime = p.derivative(var);
321
322        // Compute resultant of p and p'
323        let res = self.resultant(p, &p_prime, var);
324
325        // Apply sign correction: (-1)^(n(n-1)/2) where n = degree
326        let n = p.degree(var);
327        let sign_exp = (n * (n - 1) / 2) % 2;
328
329        if sign_exp == 1 { -res } else { res }
330    }
331
332    /// Compute the discriminant with leading coefficient normalization.
333    ///
334    /// Returns `disc(p) / lc(p)` where `lc(p)` is the leading coefficient of
335    /// `p` with respect to `var`.  When `lc(p)` is a non-zero rational
336    /// constant we scale by its reciprocal; when it is a polynomial in other
337    /// variables we perform exact polynomial pseudo-division.
338    pub fn discriminant_normalized(&mut self, p: &Polynomial, var: Var) -> Polynomial {
339        let disc = self.discriminant(p, var);
340
341        let lc = p.leading_coeff_wrt(var);
342
343        if lc.is_one() {
344            return disc;
345        }
346
347        if lc.is_constant() {
348            // Scalar leading coefficient: multiply disc by 1/lc.
349            let lc_val = lc.constant_value();
350            if lc_val.is_zero() {
351                // Degenerate: p's leading coeff is 0 — return disc unchanged.
352                return disc;
353            }
354            let inv_lc = lc_val.recip();
355            disc.scale(&inv_lc)
356        } else {
357            // Polynomial leading coefficient: `pseudo_div_univariate` returns
358            // `lc^d * (disc/lc)` rather than the true quotient `disc/lc`, so
359            // we cannot use it here without introducing a spurious `lc^(d-1)`
360            // factor.  No production callers currently exercise this branch;
361            // returning `disc` (un-normalised but correct) is safe.
362            disc
363        }
364    }
365
366    /// Check if two polynomials have a common root.
367    ///
368    /// Returns true if resultant is zero.
369    pub fn have_common_root(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> bool {
370        // Special case: identical non-constant polynomials always share roots
371        if p == q && p.degree(var) > 0 {
372            return true;
373        }
374
375        let res = self.resultant(p, q, var);
376        res.is_zero()
377    }
378
379    /// Update average degree statistics.
380    fn update_degree_stats(&mut self, degree: usize) {
381        let count = self.stats.resultants_computed + self.stats.discriminants_computed;
382        let old_avg = self.stats.avg_result_degree;
383        self.stats.avg_result_degree =
384            (old_avg * (count - 1) as f64 + degree as f64) / count as f64;
385    }
386
387    /// Get statistics.
388    pub fn stats(&self) -> &ResultantStats {
389        &self.stats
390    }
391
392    /// Reset statistics.
393    pub fn reset_stats(&mut self) {
394        self.stats = ResultantStats::default();
395    }
396}
397
398// ─── Bareiss fraction-free determinant ──────────────────────────────────────
399
400/// Compute the determinant of a square matrix of polynomials using the
401/// Bareiss fraction-free Gaussian elimination algorithm.
402///
403/// The algorithm maintains the invariant that after `k` pivot steps the
404/// (i,j) entry equals the minor determinant divided by the `(k-1)`-th pivot
405/// (which cancels exactly by Sylvester's identity), so no rational function
406/// arithmetic over the polynomial ring is ever required.
407///
408/// Reference: Bareiss, E. H. (1968). Sylvester's Identity and Multistep
409/// Integer-Preserving Gaussian Elimination. *Mathematics of Computation*, 22.
410fn bareiss_det(mut mat: Vec<Vec<Polynomial>>) -> Polynomial {
411    let n = mat.len();
412    if n == 0 {
413        return Polynomial::one();
414    }
415    if n == 1 {
416        return mat.remove(0).remove(0);
417    }
418
419    // Track sign from row swaps.
420    let mut sign = Polynomial::one();
421    let neg_one = Polynomial::constant(num_rational::BigRational::from_integer(
422        num_bigint::BigInt::from(-1),
423    ));
424
425    for col in 0..n {
426        // Find a nonzero pivot in the current column (at or below `col`).
427        let pivot_row = (col..n).find(|&r| !mat[r][col].is_zero());
428
429        let pivot_row = match pivot_row {
430            Some(r) => r,
431            // Singular matrix → determinant is 0.
432            None => return Polynomial::zero(),
433        };
434
435        if pivot_row != col {
436            mat.swap(col, pivot_row);
437            // Swap changes sign of determinant.
438            sign = Polynomial::mul(&sign, &neg_one);
439        }
440
441        // Bareiss step: for each row below the pivot row, eliminate.
442        let pivot = mat[col][col].clone();
443        for row in (col + 1)..n {
444            for j in (col + 1)..n {
445                // new[row][j] = pivot * mat[row][j] - mat[row][col] * mat[col][j]
446                let prod1 = Polynomial::mul(&pivot, &mat[row][j]);
447                let prod2 = Polynomial::mul(&mat[row][col], &mat[col][j]);
448                let diff = Polynomial::sub(&prod1, &prod2);
449
450                // Divide by the previous pivot (exact cancellation by Bareiss).
451                if col == 0 {
452                    mat[row][j] = diff;
453                } else {
454                    // Divide by mat[col-1][col-1] (the pivot one step earlier).
455                    let prev_pivot = mat[col - 1][col - 1].clone();
456                    if prev_pivot.is_zero() {
457                        // Should not happen for a non-singular matrix; treat as 0.
458                        mat[row][j] = Polynomial::zero();
459                    } else if prev_pivot.is_one() {
460                        mat[row][j] = diff;
461                    } else if diff.is_zero() {
462                        mat[row][j] = Polynomial::zero();
463                    } else if prev_pivot.is_constant() && diff.is_constant() {
464                        // Both are rational constants: do exact scalar division.
465                        let num = diff.constant_value();
466                        let den = prev_pivot.constant_value();
467                        // Bareiss guarantees exact divisibility.
468                        mat[row][j] = Polynomial::constant(num / den);
469                    } else {
470                        // Exact polynomial pseudo-division — remainder should be 0.
471                        let (q, _r) = diff.pseudo_div_univariate(&prev_pivot);
472                        mat[row][j] = q;
473                    }
474                }
475            }
476            // Zero out the eliminated column.
477            mat[row][col] = Polynomial::zero();
478        }
479    }
480
481    // The determinant is the last diagonal element, times accumulated sign.
482    let raw = mat[n - 1][n - 1].clone();
483    Polynomial::mul(&sign, &raw)
484}
485
486// ─── tests ──────────────────────────────────────────────────────────────────
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_computer_creation() {
494        let computer = ResultantComputer::default_config();
495        assert_eq!(computer.stats().resultants_computed, 0);
496    }
497
498    #[test]
499    fn test_constant_resultant() {
500        let mut computer = ResultantComputer::default_config();
501
502        let var = 0;
503        let p = Polynomial::constant(BigRational::from_integer(2.into()));
504        let q = Polynomial::constant(BigRational::from_integer(3.into()));
505
506        let res = computer.resultant(&p, &q, var);
507
508        // Resultant of two constants is 1
509        assert!(res.is_one());
510    }
511
512    #[test]
513    fn test_discriminant_linear() {
514        let mut computer = ResultantComputer::default_config();
515
516        let var = 0;
517        // p = x - 1 (degree 1)
518        let p = Polynomial::linear(&[(BigRational::one(), var)], -BigRational::one());
519
520        let _disc = computer.discriminant(&p, var);
521
522        // Discriminant of linear polynomial is 1
523        assert_eq!(computer.stats().discriminants_computed, 1);
524    }
525
526    #[test]
527    fn test_have_common_root() {
528        let mut computer = ResultantComputer::default_config();
529
530        let var = 0;
531        let p = Polynomial::from_var(var); // x
532        let q = Polynomial::from_var(var); // x
533
534        // Same polynomial - definitely have common roots
535        assert!(computer.have_common_root(&p, &q, var));
536    }
537
538    #[test]
539    fn test_stats() {
540        let mut computer = ResultantComputer::default_config();
541
542        let var = 0;
543        let p = Polynomial::one();
544        let q = Polynomial::one();
545
546        computer.resultant(&p, &q, var);
547
548        assert_eq!(computer.stats().resultants_computed, 1);
549    }
550
551    /// Verify that `ResultantMethod::Sylvester` follows the Sylvester matrix
552    /// code path and returns the correct value.
553    ///
554    /// `Res(x - 2, x - 3)` is the resultant of two linear polynomials with
555    /// roots 2 and 3.  By definition `Res(x-a, x-b) = b - a`, so the answer
556    /// is `3 - 2 = 1`; the sign convention used here (Sylvester determinant
557    /// with p-rows first) gives `-1`.  Either way the result must be a
558    /// non-zero constant.
559    #[test]
560    fn test_resultant_sylvester_linear() {
561        let cfg = ResultantConfig {
562            method: ResultantMethod::Sylvester,
563            ..Default::default()
564        };
565        let mut computer = ResultantComputer::new(cfg);
566
567        let var: Var = 0;
568        // p = x - 2
569        let p = Polynomial::linear(
570            &[(BigRational::one(), var)],
571            -BigRational::from_integer(2.into()),
572        );
573        // q = x - 3
574        let q = Polynomial::linear(
575            &[(BigRational::one(), var)],
576            -BigRational::from_integer(3.into()),
577        );
578
579        let res = computer.resultant(&p, &q, var);
580
581        // Must be a non-zero constant.
582        assert!(
583            res.is_constant(),
584            "resultant of two linears must be constant; got {res:?}"
585        );
586        assert!(
587            !res.is_zero(),
588            "resultant of coprime linears must be non-zero"
589        );
590        // Sylvester matrix (p-rows first, then q-rows; coeff index 0 = constant):
591        //   Row 0 (p, r=0):  col 0 = coeff_p[0] = -2,  col 1 = coeff_p[1] = 1
592        //   Row 1 (q, r=0):  col 0 = coeff_q[0] = -3,  col 1 = coeff_q[1] = 1
593        //   det = (-2)(1) - (1)(-3) = -2 + 3 = 1
594        let val = res.constant_value();
595        assert_eq!(
596            val,
597            BigRational::from_integer(1.into()),
598            "Res(x-2, x-3) via Sylvester should be 1, got {val}"
599        );
600        assert_eq!(computer.stats().sylvester_determinants, 1);
601    }
602
603    /// `Res(x² - 5, x² - 2)` should equal 9 (confirmed by SymPy).
604    #[test]
605    fn test_resultant_sylvester_quadratics() {
606        let cfg = ResultantConfig {
607            method: ResultantMethod::Sylvester,
608            ..Default::default()
609        };
610        let mut computer = ResultantComputer::new(cfg);
611
612        let var: Var = 0;
613        // p = x^2 - 5
614        let p = Polynomial::univariate(
615            var,
616            &[
617                -BigRational::from_integer(5.into()),
618                BigRational::zero(),
619                BigRational::one(),
620            ],
621        );
622        // q = x^2 - 2
623        let q = Polynomial::univariate(
624            var,
625            &[
626                -BigRational::from_integer(2.into()),
627                BigRational::zero(),
628                BigRational::one(),
629            ],
630        );
631
632        let res = computer.resultant(&p, &q, var);
633
634        assert!(
635            res.is_constant(),
636            "resultant of two quadratics (same var) must be constant; got {res:?}"
637        );
638        let val = res.constant_value();
639        assert_eq!(
640            val,
641            BigRational::from_integer(9.into()),
642            "Res(x^2-5, x^2-2) should be 9, got {val}"
643        );
644    }
645
646    // -- Regression tests for MATH-3 --
647
648    #[test]
649    fn test_default_resultant_method_is_sylvester() {
650        // Regression test for MATH-3: the default method used to be
651        // `Subresultant`, which returned outright wrong (non-zero,
652        // var-containing) results for polynomials sharing a root. The
653        // default must be the proven-exact `Sylvester` method.
654        assert_eq!(
655            ResultantConfig::default().method,
656            ResultantMethod::Sylvester
657        );
658    }
659
660    /// `Res(x^2 - 1, x - 1, x)` must be exactly 0: the two polynomials
661    /// share the root `x = 1`. This is the concrete counterexample from
662    /// MATH-3 -- the old `resultant_subresultant` returned `x - 1` (a
663    /// non-zero polynomial that still contains the elimination variable,
664    /// which cannot be a valid resultant at all).
665    #[test]
666    fn test_resultant_shared_root_is_zero_default_method() {
667        let mut computer = ResultantComputer::default_config();
668        let var: Var = 0;
669
670        // p = x^2 - 1
671        let p = Polynomial::univariate(
672            var,
673            &[-BigRational::one(), BigRational::zero(), BigRational::one()],
674        );
675        // q = x - 1
676        let q = Polynomial::linear(&[(BigRational::one(), var)], -BigRational::one());
677
678        let res = computer.resultant(&p, &q, var);
679        assert!(res.is_zero(), "Res(x^2-1, x-1) must be 0, got {res:?}");
680    }
681
682    #[test]
683    fn test_resultant_subresultant_shared_root_is_zero() {
684        // Same as above but forcing the (now-fixed) Subresultant method
685        // explicitly, since it is the one MATH-3 flagged as broken.
686        let cfg = ResultantConfig {
687            method: ResultantMethod::Subresultant,
688            ..Default::default()
689        };
690        let mut computer = ResultantComputer::new(cfg);
691        let var: Var = 0;
692
693        let p = Polynomial::univariate(
694            var,
695            &[-BigRational::one(), BigRational::zero(), BigRational::one()],
696        ); // x^2 - 1
697        let q = Polynomial::linear(&[(BigRational::one(), var)], -BigRational::one()); // x - 1
698
699        let res = computer.resultant(&p, &q, var);
700        assert!(
701            res.is_zero(),
702            "Subresultant method: Res(x^2-1, x-1) must be 0, got {res:?}"
703        );
704    }
705
706    #[test]
707    fn test_resultant_subresultant_coprime_is_nonzero_constant() {
708        // p, q coprime (no shared root): the subresultant path must still
709        // report a non-zero constant (exact magnitude/sign is not
710        // guaranteed to match Sylvester -- see the method's docs -- but it
711        // must correctly detect "no common root").
712        let cfg = ResultantConfig {
713            method: ResultantMethod::Subresultant,
714            ..Default::default()
715        };
716        let mut computer = ResultantComputer::new(cfg);
717        let var: Var = 0;
718
719        // p = x - 2, q = x - 3 (roots 2 and 3, coprime).
720        let p = Polynomial::linear(
721            &[(BigRational::one(), var)],
722            -BigRational::from_integer(2.into()),
723        );
724        let q = Polynomial::linear(
725            &[(BigRational::one(), var)],
726            -BigRational::from_integer(3.into()),
727        );
728
729        let res = computer.resultant(&p, &q, var);
730        assert!(res.is_constant());
731        assert!(
732            !res.is_zero(),
733            "coprime linears must have non-zero resultant"
734        );
735    }
736
737    #[test]
738    fn test_have_common_root_detects_shared_root_via_resultant() {
739        // Regression test for MATH-3: `have_common_root` delegates to
740        // `resultant()`, whose default method used to silently return a
741        // wrong non-zero value for polynomials sharing a root, so this
742        // reported `false` for `(x^2-1, x-1)` even though `x=1` is a
743        // shared root. Uses two *distinct* (non-identical) polynomials so
744        // the `p == q` fast path in `have_common_root` is not what makes
745        // this pass.
746        let mut computer = ResultantComputer::default_config();
747        let var: Var = 0;
748
749        let p = Polynomial::univariate(
750            var,
751            &[-BigRational::one(), BigRational::zero(), BigRational::one()],
752        ); // x^2 - 1
753        let q = Polynomial::linear(&[(BigRational::one(), var)], -BigRational::one()); // x - 1
754        assert_ne!(p, q);
755
756        assert!(
757            computer.have_common_root(&p, &q, var),
758            "x^2-1 and x-1 share the root x=1"
759        );
760    }
761
762    #[test]
763    fn test_have_common_root_false_for_coprime_polynomials() {
764        let mut computer = ResultantComputer::default_config();
765        let var: Var = 0;
766
767        let p = Polynomial::linear(
768            &[(BigRational::one(), var)],
769            -BigRational::from_integer(2.into()),
770        ); // x - 2
771        let q = Polynomial::linear(
772            &[(BigRational::one(), var)],
773            -BigRational::from_integer(3.into()),
774        ); // x - 3
775
776        assert!(!computer.have_common_root(&p, &q, var));
777    }
778}