Skip to main content

oxiz_math/polynomial/
root_counting.rs

1//! Polynomial Root Counting.
2//!
3//! Efficient algorithms for counting real roots of polynomials in intervals,
4//! using Descartes' rule of signs, Sturm sequences, and Budan's theorem.
5//!
6//! ## Algorithms
7//!
8//! - **Descartes' Rule**: Count sign variations in coefficients
9//! - **Sturm Sequence**: Exact root counting with polynomial remainder sequence
10//! - **Budan's Theorem**: Efficient root counting via derivative evaluations
11//!
12//! ## References
13//!
14//! - "Algorithms in Real Algebraic Geometry" (Basu, Pollack, Roy, 2006)
15//! - Z3's `math/polynomial/polynomial.cpp`
16
17#[allow(unused_imports)]
18use crate::prelude::*;
19use num_bigint::BigInt;
20use num_rational::BigRational;
21use num_traits::{Signed, Zero};
22
23/// A univariate polynomial over Q.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Polynomial {
26    /// Coefficients in increasing degree order: a_0 + a_1*x + a_2*x^2 + ...
27    pub coeffs: Vec<BigRational>,
28}
29
30impl Polynomial {
31    /// Create a polynomial from coefficients.
32    pub fn new(coeffs: Vec<BigRational>) -> Self {
33        let mut poly = Self { coeffs };
34        poly.normalize();
35        poly
36    }
37
38    /// Remove trailing zeros.
39    fn normalize(&mut self) {
40        // Safety: len() > 1 ensures last() is Some, but use map_or for no-unwrap policy
41        while self.coeffs.len() > 1 && self.coeffs.last().is_some_and(|c| c.is_zero()) {
42            self.coeffs.pop();
43        }
44        if self.coeffs.is_empty() {
45            self.coeffs.push(BigRational::zero());
46        }
47    }
48
49    /// Get the degree of the polynomial.
50    pub fn degree(&self) -> usize {
51        if self.coeffs.len() == 1 && self.coeffs[0].is_zero() {
52            0
53        } else {
54            self.coeffs.len() - 1
55        }
56    }
57
58    /// Evaluate the polynomial at a point.
59    pub fn eval(&self, x: &BigRational) -> BigRational {
60        // Horner's method
61        let mut result = BigRational::zero();
62        for coeff in self.coeffs.iter().rev() {
63            result = result * x + coeff;
64        }
65        result
66    }
67
68    /// Compute the derivative.
69    pub fn derivative(&self) -> Polynomial {
70        if self.degree() == 0 {
71            return Polynomial::new(vec![BigRational::zero()]);
72        }
73
74        let coeffs: Vec<_> = self
75            .coeffs
76            .iter()
77            .enumerate()
78            .skip(1)
79            .map(|(i, c)| c * BigRational::from_integer(BigInt::from(i)))
80            .collect();
81
82        Polynomial::new(coeffs)
83    }
84
85    /// Compute polynomial remainder: self mod other.
86    pub fn remainder(&self, other: &Polynomial) -> Polynomial {
87        let mut rem = self.clone();
88        let divisor_deg = other.degree();
89        let divisor_lead = &other.coeffs[divisor_deg];
90
91        loop {
92            // Terminate when rem is the zero polynomial or degree drops below divisor.
93            // degree() returns 0 for both the zero polynomial and non-zero constants, so we
94            // must check for zero explicitly to avoid an infinite loop when divisor_deg == 0.
95            if rem.coeffs.len() == 1 && rem.coeffs[0].is_zero() {
96                break;
97            }
98            if rem.degree() < divisor_deg || rem.coeffs.is_empty() {
99                break;
100            }
101
102            let rem_deg = rem.degree();
103            let rem_lead = &rem.coeffs[rem_deg];
104            let factor = rem_lead / divisor_lead;
105
106            // Subtract factor * other from rem
107            for i in 0..=divisor_deg {
108                rem.coeffs[rem_deg - divisor_deg + i] =
109                    &rem.coeffs[rem_deg - divisor_deg + i] - &factor * &other.coeffs[i];
110            }
111
112            rem.normalize();
113        }
114
115        rem
116    }
117}
118
119/// Configuration for root counting.
120#[derive(Debug, Clone)]
121pub struct RootCountConfig {
122    /// Use Sturm sequences (exact but slower).
123    pub use_sturm: bool,
124    /// Use Descartes' rule (fast but gives upper bound).
125    pub use_descartes: bool,
126    /// Use Budan's theorem.
127    pub use_budan: bool,
128}
129
130impl Default for RootCountConfig {
131    fn default() -> Self {
132        Self {
133            use_sturm: true,
134            use_descartes: true,
135            use_budan: false,
136        }
137    }
138}
139
140/// Statistics for root counting.
141#[derive(Debug, Clone, Default)]
142pub struct RootCountStats {
143    /// Roots counted.
144    pub roots_counted: u64,
145    /// Sturm sequences computed.
146    pub sturm_sequences: u64,
147    /// Descartes sign variations computed.
148    pub descartes_variations: u64,
149}
150
151/// Root counting engine.
152#[derive(Debug)]
153pub struct RootCounter {
154    /// Configuration.
155    config: RootCountConfig,
156    /// Statistics.
157    stats: RootCountStats,
158}
159
160impl RootCounter {
161    /// Create a new root counter.
162    pub fn new(config: RootCountConfig) -> Self {
163        Self {
164            config,
165            stats: RootCountStats::default(),
166        }
167    }
168
169    /// Create with default configuration.
170    pub fn default_config() -> Self {
171        Self::new(RootCountConfig::default())
172    }
173
174    /// Count real roots in interval (a, b) using Sturm's theorem.
175    pub fn count_roots_sturm(
176        &mut self,
177        poly: &Polynomial,
178        a: &BigRational,
179        b: &BigRational,
180    ) -> usize {
181        if !self.config.use_sturm {
182            return 0;
183        }
184
185        self.stats.sturm_sequences += 1;
186
187        let sturm_seq = self.sturm_sequence(poly);
188
189        let sign_changes_a = self.count_sign_changes(&sturm_seq, a);
190        let sign_changes_b = self.count_sign_changes(&sturm_seq, b);
191
192        (sign_changes_a as isize - sign_changes_b as isize).unsigned_abs()
193    }
194
195    /// Compute Sturm sequence for a polynomial.
196    fn sturm_sequence(&self, poly: &Polynomial) -> Vec<Polynomial> {
197        let mut seq = vec![poly.clone(), poly.derivative()];
198
199        loop {
200            let n = seq.len();
201            // A degree-0 tail (zero or non-zero constant) means the sequence is complete.
202            // Any polynomial mod a constant is 0, so the next remainder would be 0.
203            if seq[n - 1].degree() == 0 {
204                break;
205            }
206            let remainder = seq[n - 2].remainder(&seq[n - 1]);
207
208            // Negate remainder (Sturm sequence convention)
209            let negated = Polynomial::new(remainder.coeffs.iter().map(|c| -c).collect());
210
211            if negated.degree() == 0 && negated.coeffs[0].is_zero() {
212                break;
213            }
214
215            seq.push(negated);
216
217            // Prevent infinite loops
218            if seq.len() > 1000 {
219                break;
220            }
221        }
222
223        seq
224    }
225
226    /// Count sign changes in sequence at point x.
227    fn count_sign_changes(&self, seq: &[Polynomial], x: &BigRational) -> usize {
228        let mut last_sign = 0i8;
229        let mut changes = 0;
230
231        for poly in seq {
232            let val = poly.eval(x);
233            let sign = if val.is_positive() {
234                1
235            } else if val.is_negative() {
236                -1
237            } else {
238                0
239            };
240
241            if sign != 0 && last_sign != 0 && sign != last_sign {
242                changes += 1;
243            }
244
245            if sign != 0 {
246                last_sign = sign;
247            }
248        }
249
250        changes
251    }
252
253    /// Count positive roots using Descartes' rule of signs.
254    pub fn count_positive_roots_descartes(&mut self, poly: &Polynomial) -> usize {
255        if !self.config.use_descartes {
256            return 0;
257        }
258
259        self.stats.descartes_variations += 1;
260
261        let mut last_sign = 0i8;
262        let mut variations = 0;
263
264        for coeff in &poly.coeffs {
265            let sign = if coeff.is_positive() {
266                1
267            } else if coeff.is_negative() {
268                -1
269            } else {
270                0
271            };
272
273            if sign != 0 && last_sign != 0 && sign != last_sign {
274                variations += 1;
275            }
276
277            if sign != 0 {
278                last_sign = sign;
279            }
280        }
281
282        variations
283    }
284
285    /// Get statistics.
286    pub fn stats(&self) -> &RootCountStats {
287        &self.stats
288    }
289
290    /// Reset statistics.
291    pub fn reset_stats(&mut self) {
292        self.stats = RootCountStats::default();
293    }
294}
295
296impl Default for RootCounter {
297    fn default() -> Self {
298        Self::default_config()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use num_traits::{One, Zero};
306
307    #[test]
308    fn test_polynomial_creation() {
309        let poly = Polynomial::new(vec![
310            BigRational::from_integer(1.into()),
311            BigRational::from_integer(2.into()),
312            BigRational::from_integer(3.into()),
313        ]);
314        assert_eq!(poly.degree(), 2);
315    }
316
317    #[test]
318    fn test_polynomial_eval() {
319        // p(x) = 1 + 2x + 3x^2
320        let poly = Polynomial::new(vec![
321            BigRational::from_integer(1.into()),
322            BigRational::from_integer(2.into()),
323            BigRational::from_integer(3.into()),
324        ]);
325
326        // p(0) = 1
327        let val = poly.eval(&BigRational::zero());
328        assert_eq!(val, BigRational::from_integer(1.into()));
329
330        // p(1) = 1 + 2 + 3 = 6
331        let val = poly.eval(&BigRational::one());
332        assert_eq!(val, BigRational::from_integer(6.into()));
333    }
334
335    #[test]
336    fn test_polynomial_derivative() {
337        // p(x) = 1 + 2x + 3x^2
338        let poly = Polynomial::new(vec![
339            BigRational::from_integer(1.into()),
340            BigRational::from_integer(2.into()),
341            BigRational::from_integer(3.into()),
342        ]);
343
344        // p'(x) = 2 + 6x
345        let deriv = poly.derivative();
346        assert_eq!(deriv.degree(), 1);
347        assert_eq!(deriv.coeffs[0], BigRational::from_integer(2.into()));
348        assert_eq!(deriv.coeffs[1], BigRational::from_integer(6.into()));
349    }
350
351    #[test]
352    fn test_descartes_rule() {
353        let mut counter = RootCounter::default_config();
354
355        // p(x) = x^2 - 1 (roots at ±1, one positive root)
356        let poly = Polynomial::new(vec![
357            BigRational::from_integer((-1).into()),
358            BigRational::zero(),
359            BigRational::from_integer(1.into()),
360        ]);
361
362        let count = counter.count_positive_roots_descartes(&poly);
363        assert!(count >= 1); // Upper bound
364    }
365
366    #[test]
367    fn test_counter_creation() {
368        let counter = RootCounter::default_config();
369        assert_eq!(counter.stats().sturm_sequences, 0);
370    }
371
372    #[test]
373    fn test_stats() {
374        let mut counter = RootCounter::default_config();
375        counter.stats.sturm_sequences = 5;
376
377        assert_eq!(counter.stats().sturm_sequences, 5);
378
379        counter.reset_stats();
380        assert_eq!(counter.stats().sturm_sequences, 0);
381    }
382}