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
80        let (left, right) = interval;
81        let left_variations = self.count_sign_variations(&sturm_seq, &left);
82        let right_variations = self.count_sign_variations(&sturm_seq, &right);
83        self.stats.sturm_evaluations += 2;
84
85        let num_roots = left_variations - right_variations;
86
87        if num_roots == 0 {
88            return vec![];
89        } else if num_roots == 1 {
90            // Single root - refine to desired precision
91            let refined = self.refine_root_interval(&poly, left, right);
92            return vec![refined];
93        }
94
95        // Multiple roots - bisect and recurse
96        self.bisect_and_isolate(&poly, &sturm_seq, left, right)
97    }
98
99    /// Build Sturm sequence for a polynomial.
100    fn build_sturm_sequence(&self, poly: &[BigRational]) -> Vec<Vec<BigRational>> {
101        let mut sequence = Vec::new();
102
103        // f_0 = f(x)
104        sequence.push(poly.to_vec());
105
106        // f_1 = f'(x)
107        let derivative = Self::derivative(poly);
108        if derivative.is_empty() {
109            return sequence;
110        }
111        sequence.push(derivative);
112
113        // f_{i+1} = -remainder(f_{i-1}, f_i)
114        loop {
115            let len = sequence.len();
116            let f_prev = &sequence[len - 2];
117            let f_curr = &sequence[len - 1];
118
119            let remainder = Self::polynomial_remainder(f_prev, f_curr);
120
121            if remainder.is_empty() || Self::is_zero_poly(&remainder) {
122                break;
123            }
124
125            // Negate remainder
126            let neg_remainder: Vec<BigRational> = remainder.iter().map(|c| -c.clone()).collect();
127
128            sequence.push(neg_remainder);
129        }
130
131        sequence
132    }
133
134    /// Count sign variations in a Sturm sequence at a point.
135    fn count_sign_variations(&self, sturm_seq: &[Vec<BigRational>], x: &BigRational) -> usize {
136        let mut signs = Vec::new();
137
138        for poly in sturm_seq {
139            let value = Self::evaluate(poly, x);
140            if !value.is_zero() {
141                signs.push(value > BigRational::zero());
142            }
143        }
144
145        // Count sign changes
146        let mut variations = 0;
147        for i in 0..signs.len().saturating_sub(1) {
148            if signs[i] != signs[i + 1] {
149                variations += 1;
150            }
151        }
152
153        variations
154    }
155
156    /// Bisect interval and recursively isolate roots.
157    fn bisect_and_isolate(
158        &mut self,
159        poly: &[BigRational],
160        sturm_seq: &[Vec<BigRational>],
161        left: BigRational,
162        right: BigRational,
163    ) -> Vec<RootInterval> {
164        self.stats.bisection_steps += 1;
165
166        let mid = (&left + &right) / BigRational::from_integer(BigInt::from(2));
167
168        let left_vars = self.count_sign_variations(sturm_seq, &left);
169        let mid_vars = self.count_sign_variations(sturm_seq, &mid);
170        let right_vars = self.count_sign_variations(sturm_seq, &right);
171        self.stats.sturm_evaluations += 3;
172
173        let mut intervals = Vec::new();
174
175        // Roots in [left, mid]
176        let left_roots = left_vars - mid_vars;
177        if left_roots > 0 {
178            intervals.extend(self.isolate_roots(poly, (left.clone(), mid.clone())));
179        }
180
181        // Roots in [mid, right]
182        let right_roots = mid_vars - right_vars;
183        if right_roots > 0 {
184            intervals.extend(self.isolate_roots(poly, (mid, right)));
185        }
186
187        intervals
188    }
189
190    /// Refine a root interval to desired precision.
191    fn refine_root_interval(
192        &mut self,
193        poly: &[BigRational],
194        mut left: BigRational,
195        mut right: BigRational,
196    ) -> RootInterval {
197        let mut iterations = 0;
198
199        while &right - &left > self.precision && iterations < self.max_iterations {
200            let mid = (&left + &right) / BigRational::from_integer(BigInt::from(2));
201            let mid_val = Self::evaluate(poly, &mid);
202
203            if mid_val.is_zero() {
204                return RootInterval {
205                    left: mid.clone(),
206                    right: mid,
207                    left_closed: true,
208                    right_closed: true,
209                    multiplicity: 1,
210                };
211            }
212
213            let left_val = Self::evaluate(poly, &left);
214
215            if (left_val > BigRational::zero()) == (mid_val > BigRational::zero()) {
216                left = mid;
217            } else {
218                right = mid;
219            }
220
221            iterations += 1;
222        }
223
224        self.stats.intervals_generated += 1;
225
226        RootInterval {
227            left,
228            right,
229            left_closed: false,
230            right_closed: false,
231            multiplicity: 1,
232        }
233    }
234
235    /// Evaluate polynomial at a point using Horner's method.
236    fn evaluate(poly: &[BigRational], x: &BigRational) -> BigRational {
237        if poly.is_empty() {
238            return BigRational::zero();
239        }
240
241        let mut result = poly[0].clone();
242        for coeff in &poly[1..] {
243            result = result * x + coeff;
244        }
245        result
246    }
247
248    /// Compute polynomial derivative.
249    fn derivative(poly: &[BigRational]) -> Vec<BigRational> {
250        if poly.len() <= 1 {
251            return vec![];
252        }
253
254        let mut deriv = Vec::with_capacity(poly.len() - 1);
255        for (i, coeff) in poly.iter().enumerate().take(poly.len() - 1) {
256            let degree = (poly.len() - 1 - i) as i64;
257            deriv.push(coeff * BigRational::from_integer(BigInt::from(degree)));
258        }
259        deriv
260    }
261
262    /// Polynomial division - compute remainder.
263    fn polynomial_remainder(dividend: &[BigRational], divisor: &[BigRational]) -> Vec<BigRational> {
264        if divisor.is_empty() || Self::is_zero_poly(divisor) {
265            return vec![];
266        }
267
268        let mut remainder = dividend.to_vec();
269
270        while remainder.len() >= divisor.len() && !Self::is_zero_poly(&remainder) {
271            let lead_div = &divisor[0];
272            let lead_rem = &remainder[0];
273
274            if lead_div.is_zero() {
275                break;
276            }
277
278            let quotient_coeff = lead_rem / lead_div;
279
280            for i in 0..divisor.len() {
281                remainder[i] = &remainder[i] - &quotient_coeff * &divisor[i];
282            }
283
284            remainder.remove(0);
285        }
286
287        remainder
288    }
289
290    /// Normalize polynomial by removing leading zeros.
291    fn normalize_polynomial(poly: &[BigRational]) -> Vec<BigRational> {
292        let mut result = poly.to_vec();
293        while !result.is_empty() && result[0].is_zero() {
294            result.remove(0);
295        }
296        result
297    }
298
299    /// Check if polynomial is zero.
300    fn is_zero_poly(poly: &[BigRational]) -> bool {
301        poly.iter().all(|c| c.is_zero())
302    }
303
304    /// Get statistics.
305    pub fn stats(&self) -> &IsolationStats {
306        &self.stats
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use num_traits::{One, Zero};
314
315    #[test]
316    fn test_root_isolator() {
317        let precision = BigRational::new(BigInt::from(1), BigInt::from(1000));
318        let isolator = RootIsolator::new(precision);
319
320        assert_eq!(isolator.stats.sturm_evaluations, 0);
321    }
322
323    #[test]
324    fn test_sturm_sequence() {
325        let precision = BigRational::new(BigInt::from(1), BigInt::from(100));
326        let isolator = RootIsolator::new(precision);
327
328        // f(x) = x^2 - 2
329        let poly = vec![
330            BigRational::one(),
331            BigRational::zero(),
332            BigRational::from_integer(BigInt::from(-2)),
333        ];
334
335        let sturm = isolator.build_sturm_sequence(&poly);
336        assert!(!sturm.is_empty());
337    }
338}