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            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}