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}