Skip to main content

oxiz_math/polynomial/
root_isolation.rs

1//! Root Isolation for Univariate Polynomials.
2//!
3//! Implements algorithms for isolating real roots of polynomials including:
4//! - Sturm sequences
5//! - Descartes' rule of signs
6//! - Continued fraction method
7//! - Bisection refinement
8
9#[allow(unused_imports)]
10use crate::prelude::*;
11use num_bigint::BigInt;
12use num_rational::BigRational;
13use num_traits::Zero;
14
15/// Root isolation engine for real polynomials.
16pub struct RootIsolator {
17    /// Precision threshold for root refinement
18    precision: BigRational,
19    /// Maximum refinement iterations
20    max_iterations: usize,
21    /// Statistics
22    stats: IsolationStats,
23}
24
25/// Isolated root interval.
26#[derive(Debug, Clone)]
27pub struct RootInterval {
28    /// Left endpoint
29    pub left: BigRational,
30    /// Right endpoint
31    pub right: BigRational,
32    /// Is left endpoint included
33    pub left_closed: bool,
34    /// Is right endpoint included
35    pub right_closed: bool,
36    /// Number of roots in this interval
37    pub multiplicity: usize,
38}
39
40/// Root isolation statistics.
41#[derive(Debug, Clone, Default)]
42pub struct IsolationStats {
43    /// Number of Sturm sequence evaluations
44    pub sturm_evaluations: usize,
45    /// Number of Descartes tests
46    pub descartes_tests: usize,
47    /// Number of bisection steps
48    pub bisection_steps: usize,
49    /// Total intervals generated
50    pub intervals_generated: usize,
51}
52
53impl RootIsolator {
54    /// Create a new root isolator.
55    pub fn new(precision: BigRational) -> Self {
56        Self {
57            precision,
58            max_iterations: 1000,
59            stats: IsolationStats::default(),
60        }
61    }
62
63    /// Isolate all real roots of a polynomial in an interval.
64    pub fn isolate_roots(
65        &mut self,
66        poly: &[BigRational],
67        interval: (BigRational, BigRational),
68    ) -> Vec<RootInterval> {
69        // Remove leading zeros
70        let poly = Self::normalize_polynomial(poly);
71
72        if poly.len() <= 1 {
73            return vec![];
74        }
75
76        // Build Sturm sequence
77        let sturm_seq = self.build_sturm_sequence(&poly);
78
79        // Count sign variations at endpoints. Normalize a reversed/degenerate
80        // interval (`left > right`) up front rather than let a Sturm
81        // sign-variation count for the (higher-x) `left` come out smaller
82        // than for `right` — sign variations are non-increasing as x
83        // increases, so a reversed pair would otherwise underflow the
84        // `usize` subtraction below.
85        let (left, right) = interval;
86        let (left, right) = if left <= right {
87            (left, right)
88        } else {
89            (right, left)
90        };
91        let left_variations = self.count_sign_variations(&sturm_seq, &left);
92        let right_variations = self.count_sign_variations(&sturm_seq, &right);
93        self.stats.sturm_evaluations += 2;
94
95        // Defense in depth: even for a well-ordered interval, use a
96        // saturating subtraction so a degenerate/edge-case count (e.g. an
97        // interval collapsing to a point) can never panic.
98        let num_roots = left_variations.saturating_sub(right_variations);
99
100        if num_roots == 0 {
101            return vec![];
102        } else if num_roots == 1 {
103            // Single root - refine to desired precision
104            let refined = self.refine_root_interval(&poly, left, right);
105            return vec![refined];
106        }
107
108        // Multiple roots - bisect and recurse
109        self.bisect_and_isolate(&poly, &sturm_seq, left, right)
110    }
111
112    /// Build Sturm sequence for a polynomial.
113    fn build_sturm_sequence(&self, poly: &[BigRational]) -> Vec<Vec<BigRational>> {
114        let mut sequence = Vec::new();
115
116        // f_0 = f(x)
117        sequence.push(poly.to_vec());
118
119        // f_1 = f'(x)
120        let derivative = Self::derivative(poly);
121        if derivative.is_empty() {
122            return sequence;
123        }
124        sequence.push(derivative);
125
126        // f_{i+1} = -remainder(f_{i-1}, f_i)
127        loop {
128            let len = sequence.len();
129            let f_prev = &sequence[len - 2];
130            let f_curr = &sequence[len - 1];
131
132            let remainder = Self::polynomial_remainder(f_prev, f_curr);
133
134            if remainder.is_empty() || Self::is_zero_poly(&remainder) {
135                break;
136            }
137
138            // Negate remainder
139            let neg_remainder: Vec<BigRational> = remainder.iter().map(|c| -c.clone()).collect();
140
141            sequence.push(neg_remainder);
142        }
143
144        sequence
145    }
146
147    /// Count sign variations in a Sturm sequence at a point.
148    fn count_sign_variations(&self, sturm_seq: &[Vec<BigRational>], x: &BigRational) -> usize {
149        let mut signs = Vec::new();
150
151        for poly in sturm_seq {
152            let value = Self::evaluate(poly, x);
153            if !value.is_zero() {
154                signs.push(value > BigRational::zero());
155            }
156        }
157
158        // Count sign changes
159        let mut variations = 0;
160        for i in 0..signs.len().saturating_sub(1) {
161            if signs[i] != signs[i + 1] {
162                variations += 1;
163            }
164        }
165
166        variations
167    }
168
169    /// Bisect interval and recursively isolate roots.
170    fn bisect_and_isolate(
171        &mut self,
172        poly: &[BigRational],
173        sturm_seq: &[Vec<BigRational>],
174        left: BigRational,
175        right: BigRational,
176    ) -> Vec<RootInterval> {
177        self.stats.bisection_steps += 1;
178
179        let mid = (&left + &right) / BigRational::from_integer(BigInt::from(2));
180
181        let left_vars = self.count_sign_variations(sturm_seq, &left);
182        let mid_vars = self.count_sign_variations(sturm_seq, &mid);
183        let right_vars = self.count_sign_variations(sturm_seq, &right);
184        self.stats.sturm_evaluations += 3;
185
186        let mut intervals = Vec::new();
187
188        // Roots in [left, mid]
189        let left_roots = left_vars.saturating_sub(mid_vars);
190        if left_roots > 0 {
191            intervals.extend(self.isolate_roots(poly, (left.clone(), mid.clone())));
192        }
193
194        // Roots in [mid, right]
195        let right_roots = mid_vars.saturating_sub(right_vars);
196        if right_roots > 0 {
197            intervals.extend(self.isolate_roots(poly, (mid, right)));
198        }
199
200        intervals
201    }
202
203    /// Refine a root interval to desired precision.
204    fn refine_root_interval(
205        &mut self,
206        poly: &[BigRational],
207        mut left: BigRational,
208        mut right: BigRational,
209    ) -> RootInterval {
210        let mut iterations = 0;
211
212        while &right - &left > self.precision && iterations < self.max_iterations {
213            let mid = (&left + &right) / BigRational::from_integer(BigInt::from(2));
214            let mid_val = Self::evaluate(poly, &mid);
215
216            if mid_val.is_zero() {
217                return RootInterval {
218                    left: mid.clone(),
219                    right: mid,
220                    left_closed: true,
221                    right_closed: true,
222                    multiplicity: 1,
223                };
224            }
225
226            let left_val = Self::evaluate(poly, &left);
227
228            if (left_val > BigRational::zero()) == (mid_val > BigRational::zero()) {
229                left = mid;
230            } else {
231                right = mid;
232            }
233
234            iterations += 1;
235        }
236
237        self.stats.intervals_generated += 1;
238
239        RootInterval {
240            left,
241            right,
242            left_closed: false,
243            right_closed: false,
244            multiplicity: 1,
245        }
246    }
247
248    /// Evaluate polynomial at a point using Horner's method.
249    fn evaluate(poly: &[BigRational], x: &BigRational) -> BigRational {
250        if poly.is_empty() {
251            return BigRational::zero();
252        }
253
254        let mut result = poly[0].clone();
255        for coeff in &poly[1..] {
256            result = result * x + coeff;
257        }
258        result
259    }
260
261    /// Compute polynomial derivative.
262    fn derivative(poly: &[BigRational]) -> Vec<BigRational> {
263        if poly.len() <= 1 {
264            return vec![];
265        }
266
267        let mut deriv = Vec::with_capacity(poly.len() - 1);
268        for (i, coeff) in poly.iter().enumerate().take(poly.len() - 1) {
269            let degree = (poly.len() - 1 - i) as i64;
270            deriv.push(coeff * BigRational::from_integer(BigInt::from(degree)));
271        }
272        deriv
273    }
274
275    /// Polynomial division - compute remainder.
276    fn polynomial_remainder(dividend: &[BigRational], divisor: &[BigRational]) -> Vec<BigRational> {
277        if divisor.is_empty() || Self::is_zero_poly(divisor) {
278            return vec![];
279        }
280
281        let mut remainder = dividend.to_vec();
282
283        while remainder.len() >= divisor.len() && !Self::is_zero_poly(&remainder) {
284            let lead_div = &divisor[0];
285            let lead_rem = &remainder[0];
286
287            if lead_div.is_zero() {
288                break;
289            }
290
291            let quotient_coeff = lead_rem / lead_div;
292
293            for i in 0..divisor.len() {
294                remainder[i] = &remainder[i] - &quotient_coeff * &divisor[i];
295            }
296
297            remainder.remove(0);
298        }
299
300        remainder
301    }
302
303    /// Normalize polynomial by removing leading zeros.
304    fn normalize_polynomial(poly: &[BigRational]) -> Vec<BigRational> {
305        let mut result = poly.to_vec();
306        while !result.is_empty() && result[0].is_zero() {
307            result.remove(0);
308        }
309        result
310    }
311
312    /// Check if polynomial is zero.
313    fn is_zero_poly(poly: &[BigRational]) -> bool {
314        poly.iter().all(|c| c.is_zero())
315    }
316
317    /// Get statistics.
318    pub fn stats(&self) -> &IsolationStats {
319        &self.stats
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use num_traits::{One, Zero};
327
328    #[test]
329    fn test_root_isolator() {
330        let precision = BigRational::new(BigInt::from(1), BigInt::from(1000));
331        let isolator = RootIsolator::new(precision);
332
333        assert_eq!(isolator.stats.sturm_evaluations, 0);
334    }
335
336    #[test]
337    fn test_sturm_sequence() {
338        let precision = BigRational::new(BigInt::from(1), BigInt::from(100));
339        let isolator = RootIsolator::new(precision);
340
341        // f(x) = x^2 - 2
342        let poly = vec![
343            BigRational::one(),
344            BigRational::zero(),
345            BigRational::from_integer(BigInt::from(-2)),
346        ];
347
348        let sturm = isolator.build_sturm_sequence(&poly);
349        assert!(!sturm.is_empty());
350    }
351}