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 method: ResultantMethod::Subresultant,
44 use_modular: false,
45 max_dense_degree: 100,
46 }
47 }
48}
49
50/// Method for resultant computation.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ResultantMethod {
53 /// Sylvester matrix determinant.
54 Sylvester,
55 /// Subresultant polynomial remainder sequence.
56 Subresultant,
57 /// Bezout matrix (for univariate).
58 Bezout,
59}
60
61/// Statistics for resultant computation.
62#[derive(Debug, Clone, Default)]
63pub struct ResultantStats {
64 /// Resultants computed.
65 pub resultants_computed: u64,
66 /// Discriminants computed.
67 pub discriminants_computed: u64,
68 /// Sylvester determinants.
69 pub sylvester_determinants: u64,
70 /// Subresultant PRS runs.
71 pub subresultant_prs: u64,
72 /// Average degree of results.
73 pub avg_result_degree: f64,
74}
75
76/// Resultant computation engine.
77pub struct ResultantComputer {
78 /// Configuration.
79 config: ResultantConfig,
80 /// Statistics.
81 stats: ResultantStats,
82}
83
84impl ResultantComputer {
85 /// Create a new resultant computer.
86 pub fn new(config: ResultantConfig) -> Self {
87 Self {
88 config,
89 stats: ResultantStats::default(),
90 }
91 }
92
93 /// Create with default configuration.
94 pub fn default_config() -> Self {
95 Self::new(ResultantConfig::default())
96 }
97
98 /// Compute the resultant of two polynomials.
99 ///
100 /// res(p, q, x) eliminates x from the system {p(x) = 0, q(x) = 0}.
101 ///
102 /// Returns a polynomial in the remaining variables.
103 pub fn resultant(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
104 self.stats.resultants_computed += 1;
105
106 // Get degrees with respect to the elimination variable
107 let deg_p = p.degree(var);
108 let deg_q = q.degree(var);
109
110 if deg_p == 0 || deg_q == 0 {
111 // One polynomial is constant in var - special case
112 return self.handle_constant_case(p, q, var);
113 }
114
115 // Choose method based on configuration and polynomial properties
116 let result = match self.config.method {
117 ResultantMethod::Sylvester => self.resultant_sylvester(p, q, var),
118 ResultantMethod::Subresultant => self.resultant_subresultant(p, q, var),
119 ResultantMethod::Bezout => {
120 if p.is_univariate() && q.is_univariate() {
121 self.resultant_bezout(p, q, var)
122 } else {
123 // Fall back to subresultant for multivariate
124 self.resultant_subresultant(p, q, var)
125 }
126 }
127 };
128
129 self.update_degree_stats(result.total_degree() as usize);
130
131 result
132 }
133
134 /// Handle case where one polynomial is constant in the elimination variable.
135 fn handle_constant_case(&self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
136 let deg_p = p.degree(var);
137 let deg_q = q.degree(var);
138
139 if deg_p == 0 && deg_q == 0 {
140 // Both constant - resultant is 1
141 Polynomial::one()
142 } else if deg_p == 0 {
143 // p is constant, resultant is p^deg_q
144 let mut result = Polynomial::one();
145 for _ in 0..deg_q {
146 result = &result * p;
147 }
148 result
149 } else {
150 // q is constant, resultant is q^deg_p
151 let mut result = Polynomial::one();
152 for _ in 0..deg_p {
153 result = &result * q;
154 }
155 result
156 }
157 }
158
159 /// Compute resultant via Sylvester matrix.
160 ///
161 /// Constructs the `(m+n) × (m+n)` Sylvester matrix whose determinant
162 /// equals `Res(p, q, var)`, then evaluates the determinant by Gaussian
163 /// elimination over the polynomial ring (fraction-free / Bareiss algorithm).
164 ///
165 /// For large degrees (m+n > `config.max_dense_degree`) the implementation
166 /// falls back to the subresultant PRS method, which is more efficient.
167 fn resultant_sylvester(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
168 self.stats.sylvester_determinants += 1;
169
170 let deg_p = p.degree(var) as usize;
171 let deg_q = q.degree(var) as usize;
172 let n = deg_p + deg_q;
173
174 // For large matrices fall back to subresultant (more efficient).
175 if n > self.config.max_dense_degree {
176 return self.resultant_subresultant(p, q, var);
177 }
178
179 // Collect coefficients: coeff_p[i] = coefficient of var^i in p
180 // (index 0 = constant term, index deg_p = leading coefficient).
181 let coeff_p: Vec<Polynomial> = (0..=deg_p).map(|i| p.coeff(var, i as u32)).collect();
182 let coeff_q: Vec<Polynomial> = (0..=deg_q).map(|i| q.coeff(var, i as u32)).collect();
183
184 // Build the Sylvester matrix as a Vec<Vec<Polynomial>>.
185 // Row layout:
186 // - First deg_q rows: shifted copies of coeff_p
187 // Row r (0-based): column j gets coeff_p[j - r] if 0 ≤ j-r ≤ deg_p
188 // - Last deg_p rows: shifted copies of coeff_q
189 // Row r (0-based, offset by deg_q): column j gets coeff_q[j - r] if in range
190 let mut mat: Vec<Vec<Polynomial>> = (0..n)
191 .map(|_| (0..n).map(|_| Polynomial::zero()).collect())
192 .collect();
193
194 for r in 0..deg_q {
195 for j in 0..n {
196 if j >= r && j - r <= deg_p {
197 mat[r][j] = coeff_p[j - r].clone();
198 }
199 }
200 }
201 for r in 0..deg_p {
202 let row = deg_q + r;
203 for j in 0..n {
204 if j >= r && j - r <= deg_q {
205 mat[row][j] = coeff_q[j - r].clone();
206 }
207 }
208 }
209
210 // Bareiss fraction-free Gaussian elimination to compute the determinant.
211 // After full elimination the determinant is mat[0][0] (the (0,0) pivot after
212 // all eliminations, divided by accumulated pivots — but Bareiss tracks
213 // the exact fraction-free result in the last surviving element).
214 bareiss_det(mat)
215 }
216
217 /// Compute resultant via subresultant PRS.
218 ///
219 /// More efficient than Sylvester for sparse polynomials.
220 fn resultant_subresultant(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
221 self.stats.subresultant_prs += 1;
222
223 // Use extended GCD-like algorithm
224 let mut a = p.clone();
225 let mut b = q.clone();
226
227 let mut sign_correction = BigRational::one();
228
229 while !b.is_zero() {
230 let deg_a = a.degree(var);
231 let deg_b = b.degree(var);
232
233 if deg_a < deg_b {
234 // Swap a and b
235 core::mem::swap(&mut a, &mut b);
236
237 // Adjust sign if degrees are both odd
238 if deg_a % 2 == 1 && deg_b % 2 == 1 {
239 sign_correction = -sign_correction;
240 }
241 }
242
243 // Compute pseudo-remainder
244 let r = a.pseudo_remainder(&b, var);
245
246 a = b;
247 b = r;
248 }
249
250 // The last non-zero remainder is the resultant (up to a factor)
251 if sign_correction.is_negative() { -a } else { a }
252 }
253
254 /// Compute resultant via Bezout matrix (univariate only).
255 ///
256 /// For univariate polynomials the Bezout and Sylvester matrices yield the
257 /// same resultant value; this entry-point reuses the subresultant PRS path
258 /// which gives identical numeric results with better asymptotic efficiency.
259 /// Kept as a separate variant for API completeness.
260 fn resultant_bezout(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> Polynomial {
261 self.resultant_subresultant(p, q, var)
262 }
263
264 /// Compute the discriminant of a polynomial.
265 ///
266 /// disc(p, x) = (-1)^(n(n-1)/2) / lc(p) * res(p, p', x)
267 ///
268 /// where p' is the derivative of p with respect to x.
269 pub fn discriminant(&mut self, p: &Polynomial, var: Var) -> Polynomial {
270 self.stats.discriminants_computed += 1;
271
272 // Compute derivative
273 let p_prime = p.derivative(var);
274
275 // Compute resultant of p and p'
276 let res = self.resultant(p, &p_prime, var);
277
278 // Apply sign correction: (-1)^(n(n-1)/2) where n = degree
279 let n = p.degree(var);
280 let sign_exp = (n * (n - 1) / 2) % 2;
281
282 if sign_exp == 1 { -res } else { res }
283 }
284
285 /// Compute the discriminant with leading coefficient normalization.
286 ///
287 /// Returns `disc(p) / lc(p)` where `lc(p)` is the leading coefficient of
288 /// `p` with respect to `var`. When `lc(p)` is a non-zero rational
289 /// constant we scale by its reciprocal; when it is a polynomial in other
290 /// variables we perform exact polynomial pseudo-division.
291 pub fn discriminant_normalized(&mut self, p: &Polynomial, var: Var) -> Polynomial {
292 let disc = self.discriminant(p, var);
293
294 let lc = p.leading_coeff_wrt(var);
295
296 if lc.is_one() {
297 return disc;
298 }
299
300 if lc.is_constant() {
301 // Scalar leading coefficient: multiply disc by 1/lc.
302 let lc_val = lc.constant_value();
303 if lc_val.is_zero() {
304 // Degenerate: p's leading coeff is 0 — return disc unchanged.
305 return disc;
306 }
307 let inv_lc = lc_val.recip();
308 disc.scale(&inv_lc)
309 } else {
310 // Polynomial leading coefficient: `pseudo_div_univariate` returns
311 // `lc^d * (disc/lc)` rather than the true quotient `disc/lc`, so
312 // we cannot use it here without introducing a spurious `lc^(d-1)`
313 // factor. No production callers currently exercise this branch;
314 // returning `disc` (un-normalised but correct) is safe.
315 disc
316 }
317 }
318
319 /// Check if two polynomials have a common root.
320 ///
321 /// Returns true if resultant is zero.
322 pub fn have_common_root(&mut self, p: &Polynomial, q: &Polynomial, var: Var) -> bool {
323 // Special case: identical non-constant polynomials always share roots
324 if p == q && p.degree(var) > 0 {
325 return true;
326 }
327
328 let res = self.resultant(p, q, var);
329 res.is_zero()
330 }
331
332 /// Update average degree statistics.
333 fn update_degree_stats(&mut self, degree: usize) {
334 let count = self.stats.resultants_computed + self.stats.discriminants_computed;
335 let old_avg = self.stats.avg_result_degree;
336 self.stats.avg_result_degree =
337 (old_avg * (count - 1) as f64 + degree as f64) / count as f64;
338 }
339
340 /// Get statistics.
341 pub fn stats(&self) -> &ResultantStats {
342 &self.stats
343 }
344
345 /// Reset statistics.
346 pub fn reset_stats(&mut self) {
347 self.stats = ResultantStats::default();
348 }
349}
350
351// ─── Bareiss fraction-free determinant ──────────────────────────────────────
352
353/// Compute the determinant of a square matrix of polynomials using the
354/// Bareiss fraction-free Gaussian elimination algorithm.
355///
356/// The algorithm maintains the invariant that after `k` pivot steps the
357/// (i,j) entry equals the minor determinant divided by the `(k-1)`-th pivot
358/// (which cancels exactly by Sylvester's identity), so no rational function
359/// arithmetic over the polynomial ring is ever required.
360///
361/// Reference: Bareiss, E. H. (1968). Sylvester's Identity and Multistep
362/// Integer-Preserving Gaussian Elimination. *Mathematics of Computation*, 22.
363fn bareiss_det(mut mat: Vec<Vec<Polynomial>>) -> Polynomial {
364 let n = mat.len();
365 if n == 0 {
366 return Polynomial::one();
367 }
368 if n == 1 {
369 return mat.remove(0).remove(0);
370 }
371
372 // Track sign from row swaps.
373 let mut sign = Polynomial::one();
374 let neg_one = Polynomial::constant(num_rational::BigRational::from_integer(
375 num_bigint::BigInt::from(-1),
376 ));
377
378 for col in 0..n {
379 // Find a nonzero pivot in the current column (at or below `col`).
380 let pivot_row = (col..n).find(|&r| !mat[r][col].is_zero());
381
382 let pivot_row = match pivot_row {
383 Some(r) => r,
384 // Singular matrix → determinant is 0.
385 None => return Polynomial::zero(),
386 };
387
388 if pivot_row != col {
389 mat.swap(col, pivot_row);
390 // Swap changes sign of determinant.
391 sign = Polynomial::mul(&sign, &neg_one);
392 }
393
394 // Bareiss step: for each row below the pivot row, eliminate.
395 let pivot = mat[col][col].clone();
396 for row in (col + 1)..n {
397 for j in (col + 1)..n {
398 // new[row][j] = pivot * mat[row][j] - mat[row][col] * mat[col][j]
399 let prod1 = Polynomial::mul(&pivot, &mat[row][j]);
400 let prod2 = Polynomial::mul(&mat[row][col], &mat[col][j]);
401 let diff = Polynomial::sub(&prod1, &prod2);
402
403 // Divide by the previous pivot (exact cancellation by Bareiss).
404 if col == 0 {
405 mat[row][j] = diff;
406 } else {
407 // Divide by mat[col-1][col-1] (the pivot one step earlier).
408 let prev_pivot = mat[col - 1][col - 1].clone();
409 if prev_pivot.is_zero() {
410 // Should not happen for a non-singular matrix; treat as 0.
411 mat[row][j] = Polynomial::zero();
412 } else if prev_pivot.is_one() {
413 mat[row][j] = diff;
414 } else if diff.is_zero() {
415 mat[row][j] = Polynomial::zero();
416 } else if prev_pivot.is_constant() && diff.is_constant() {
417 // Both are rational constants: do exact scalar division.
418 let num = diff.constant_value();
419 let den = prev_pivot.constant_value();
420 // Bareiss guarantees exact divisibility.
421 mat[row][j] = Polynomial::constant(num / den);
422 } else {
423 // Exact polynomial pseudo-division — remainder should be 0.
424 let (q, _r) = diff.pseudo_div_univariate(&prev_pivot);
425 mat[row][j] = q;
426 }
427 }
428 }
429 // Zero out the eliminated column.
430 mat[row][col] = Polynomial::zero();
431 }
432 }
433
434 // The determinant is the last diagonal element, times accumulated sign.
435 let raw = mat[n - 1][n - 1].clone();
436 Polynomial::mul(&sign, &raw)
437}
438
439// ─── tests ──────────────────────────────────────────────────────────────────
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_computer_creation() {
447 let computer = ResultantComputer::default_config();
448 assert_eq!(computer.stats().resultants_computed, 0);
449 }
450
451 #[test]
452 fn test_constant_resultant() {
453 let mut computer = ResultantComputer::default_config();
454
455 let var = 0;
456 let p = Polynomial::constant(BigRational::from_integer(2.into()));
457 let q = Polynomial::constant(BigRational::from_integer(3.into()));
458
459 let res = computer.resultant(&p, &q, var);
460
461 // Resultant of two constants is 1
462 assert!(res.is_one());
463 }
464
465 #[test]
466 fn test_discriminant_linear() {
467 let mut computer = ResultantComputer::default_config();
468
469 let var = 0;
470 // p = x - 1 (degree 1)
471 let p = Polynomial::linear(&[(BigRational::one(), var)], -BigRational::one());
472
473 let _disc = computer.discriminant(&p, var);
474
475 // Discriminant of linear polynomial is 1
476 assert_eq!(computer.stats().discriminants_computed, 1);
477 }
478
479 #[test]
480 fn test_have_common_root() {
481 let mut computer = ResultantComputer::default_config();
482
483 let var = 0;
484 let p = Polynomial::from_var(var); // x
485 let q = Polynomial::from_var(var); // x
486
487 // Same polynomial - definitely have common roots
488 assert!(computer.have_common_root(&p, &q, var));
489 }
490
491 #[test]
492 fn test_stats() {
493 let mut computer = ResultantComputer::default_config();
494
495 let var = 0;
496 let p = Polynomial::one();
497 let q = Polynomial::one();
498
499 computer.resultant(&p, &q, var);
500
501 assert_eq!(computer.stats().resultants_computed, 1);
502 }
503
504 /// Verify that `ResultantMethod::Sylvester` follows the Sylvester matrix
505 /// code path and returns the correct value.
506 ///
507 /// `Res(x - 2, x - 3)` is the resultant of two linear polynomials with
508 /// roots 2 and 3. By definition `Res(x-a, x-b) = b - a`, so the answer
509 /// is `3 - 2 = 1`; the sign convention used here (Sylvester determinant
510 /// with p-rows first) gives `-1`. Either way the result must be a
511 /// non-zero constant.
512 #[test]
513 fn test_resultant_sylvester_linear() {
514 let cfg = ResultantConfig {
515 method: ResultantMethod::Sylvester,
516 ..Default::default()
517 };
518 let mut computer = ResultantComputer::new(cfg);
519
520 let var: Var = 0;
521 // p = x - 2
522 let p = Polynomial::linear(
523 &[(BigRational::one(), var)],
524 -BigRational::from_integer(2.into()),
525 );
526 // q = x - 3
527 let q = Polynomial::linear(
528 &[(BigRational::one(), var)],
529 -BigRational::from_integer(3.into()),
530 );
531
532 let res = computer.resultant(&p, &q, var);
533
534 // Must be a non-zero constant.
535 assert!(
536 res.is_constant(),
537 "resultant of two linears must be constant; got {res:?}"
538 );
539 assert!(
540 !res.is_zero(),
541 "resultant of coprime linears must be non-zero"
542 );
543 // Sylvester matrix (p-rows first, then q-rows; coeff index 0 = constant):
544 // Row 0 (p, r=0): col 0 = coeff_p[0] = -2, col 1 = coeff_p[1] = 1
545 // Row 1 (q, r=0): col 0 = coeff_q[0] = -3, col 1 = coeff_q[1] = 1
546 // det = (-2)(1) - (1)(-3) = -2 + 3 = 1
547 let val = res.constant_value();
548 assert_eq!(
549 val,
550 BigRational::from_integer(1.into()),
551 "Res(x-2, x-3) via Sylvester should be 1, got {val}"
552 );
553 assert_eq!(computer.stats().sylvester_determinants, 1);
554 }
555
556 /// `Res(x² - 5, x² - 2)` should equal 9 (confirmed by SymPy).
557 #[test]
558 fn test_resultant_sylvester_quadratics() {
559 let cfg = ResultantConfig {
560 method: ResultantMethod::Sylvester,
561 ..Default::default()
562 };
563 let mut computer = ResultantComputer::new(cfg);
564
565 let var: Var = 0;
566 // p = x^2 - 5
567 let p = Polynomial::univariate(
568 var,
569 &[
570 -BigRational::from_integer(5.into()),
571 BigRational::zero(),
572 BigRational::one(),
573 ],
574 );
575 // q = x^2 - 2
576 let q = Polynomial::univariate(
577 var,
578 &[
579 -BigRational::from_integer(2.into()),
580 BigRational::zero(),
581 BigRational::one(),
582 ],
583 );
584
585 let res = computer.resultant(&p, &q, var);
586
587 assert!(
588 res.is_constant(),
589 "resultant of two quadratics (same var) must be constant; got {res:?}"
590 );
591 let val = res.constant_value();
592 assert_eq!(
593 val,
594 BigRational::from_integer(9.into()),
595 "Res(x^2-5, x^2-2) should be 9, got {val}"
596 );
597 }
598}