Skip to main content

oxiz_math/algebraic/
isolate.rs

1//! Root Isolation for Algebraic Numbers.
2//!
3//! Algorithms for isolating real roots of polynomials into disjoint intervals.
4//! Each interval contains exactly one root, enabling construction of algebraic numbers.
5//!
6//! ## Algorithms
7//!
8//! - **Sturm Sequences**: Count roots in intervals
9//! - **Bisection**: Refine intervals containing roots
10//! - **Descartes' Rule**: Upper bound on positive roots
11//!
12//! ## References
13//!
14//! - "Algorithms in Real Algebraic Geometry" (Basu et al., 2006)
15//! - Z3's `math/polynomial/algebraic_numbers.cpp`
16
17use crate::polynomial::root_counting::Polynomial;
18#[allow(unused_imports)]
19use crate::prelude::*;
20use num_bigint::BigInt;
21use num_rational::BigRational;
22use num_traits::{Signed, Zero};
23
24/// Isolating interval for a root.
25#[derive(Debug, Clone)]
26pub struct IsolatingInterval {
27    /// Lower bound.
28    pub lower: BigRational,
29    /// Upper bound.
30    pub upper: BigRational,
31    /// Sign at lower bound.
32    pub sign_lower: i32,
33    /// Sign at upper bound.
34    pub sign_upper: i32,
35}
36
37impl IsolatingInterval {
38    /// Create a new isolating interval.
39    pub fn new(lower: BigRational, upper: BigRational, sign_lower: i32, sign_upper: i32) -> Self {
40        Self {
41            lower,
42            upper,
43            sign_lower,
44            sign_upper,
45        }
46    }
47
48    /// Get interval width.
49    pub fn width(&self) -> BigRational {
50        &self.upper - &self.lower
51    }
52
53    /// Get midpoint.
54    pub fn midpoint(&self) -> BigRational {
55        (&self.lower + &self.upper) / BigRational::from(BigInt::from(2))
56    }
57}
58
59/// Configuration for root isolation.
60#[derive(Debug, Clone)]
61pub struct IsolationConfig {
62    /// Use Sturm sequences for root counting.
63    pub use_sturm: bool,
64    /// Use Descartes' rule for optimization.
65    pub use_descartes: bool,
66    /// Maximum refinement iterations.
67    pub max_iterations: usize,
68    /// Precision threshold.
69    pub precision: BigRational,
70}
71
72impl Default for IsolationConfig {
73    fn default() -> Self {
74        Self {
75            use_sturm: true,
76            use_descartes: true,
77            max_iterations: 1000,
78            precision: BigRational::new(BigInt::from(1), BigInt::from(1_000_000)),
79        }
80    }
81}
82
83/// Statistics for root isolation.
84#[derive(Debug, Clone, Default)]
85pub struct IsolationStats {
86    /// Sturm sequence computations.
87    pub sturm_computations: u64,
88    /// Bisection steps.
89    pub bisection_steps: u64,
90    /// Sign evaluations.
91    pub sign_evaluations: u64,
92    /// Roots isolated.
93    pub roots_isolated: u64,
94}
95
96/// Interval refinement method.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum IntervalRefinement {
99    /// Bisection method.
100    Bisection,
101    /// Newton's method.
102    Newton,
103    /// Hybrid (bisection + Newton).
104    Hybrid,
105}
106
107/// Root isolator using interval arithmetic.
108pub struct RootIsolator {
109    /// Polynomial to isolate roots for (univariate over Q).
110    poly: Polynomial,
111    /// Configuration.
112    config: IsolationConfig,
113    /// Statistics.
114    stats: IsolationStats,
115    /// Cached Sturm sequence.
116    sturm_sequence: Option<Vec<Polynomial>>,
117}
118
119impl RootIsolator {
120    /// Create a new root isolator.
121    pub fn new(poly: Polynomial, config: IsolationConfig) -> Self {
122        Self {
123            poly,
124            config,
125            stats: IsolationStats::default(),
126            sturm_sequence: None,
127        }
128    }
129
130    /// Create with default configuration.
131    pub fn default_config(poly: Polynomial) -> Self {
132        Self::new(poly, IsolationConfig::default())
133    }
134
135    /// Isolate all real roots of the polynomial.
136    pub fn isolate_roots(&mut self) -> Vec<IsolatingInterval> {
137        // Handle trivial cases: zero polynomial or non-zero constant
138        let is_zero = self.poly.degree() == 0 && self.poly.coeffs.first().is_none_or(Zero::is_zero);
139        if is_zero || self.poly.degree() == 0 {
140            return Vec::new();
141        }
142
143        // Compute Sturm sequence if enabled
144        if self.config.use_sturm {
145            self.compute_sturm_sequence();
146        }
147
148        // Find bounds for all real roots
149        let (lower_bound, upper_bound) = self.root_bounds();
150
151        // Isolate roots in [lower_bound, upper_bound]
152        self.isolate_in_interval(lower_bound, upper_bound)
153    }
154
155    /// Compute Sturm sequence for the polynomial.
156    fn compute_sturm_sequence(&mut self) {
157        let mut seq = vec![self.poly.clone(), self.poly.derivative()];
158
159        loop {
160            let len = seq.len();
161            if len < 2 {
162                break;
163            }
164
165            let last = &seq[len - 1];
166            // Stop when the tail is degree-0 (zero or non-zero constant): any polynomial
167            // mod a constant is 0, so the next negated remainder would be 0 anyway.
168            if last.degree() == 0 {
169                break;
170            }
171
172            let remainder = seq[len - 2].remainder(&seq[len - 1]);
173
174            // Negate remainder (Sturm sequence convention: p_{k+1} = -rem(p_{k-1}, p_k))
175            let negated = Polynomial::new(remainder.coeffs.iter().map(|c| -c).collect());
176
177            if negated.degree() == 0 && negated.coeffs.first().is_none_or(Zero::is_zero) {
178                break;
179            }
180
181            seq.push(negated);
182
183            // Guard against degenerate polynomials
184            if seq.len() > 1000 {
185                break;
186            }
187        }
188
189        self.sturm_sequence = Some(seq);
190        self.stats.sturm_computations += 1;
191    }
192
193    /// Count roots in an interval using Sturm's theorem.
194    fn count_roots(&mut self, lower: &BigRational, upper: &BigRational) -> usize {
195        if self.sturm_sequence.is_none() {
196            self.compute_sturm_sequence();
197        }
198
199        // Clone to avoid simultaneous immutable+mutable borrow of self.
200        let seq: Vec<Polynomial> = self.sturm_sequence.as_ref().cloned().unwrap_or_default();
201
202        let sign_changes_lower = self.count_sign_changes(&seq, lower);
203        let sign_changes_upper = self.count_sign_changes(&seq, upper);
204
205        (sign_changes_lower as isize - sign_changes_upper as isize).unsigned_abs()
206    }
207
208    /// Count sign changes in Sturm sequence at a point.
209    fn count_sign_changes(&mut self, seq: &[Polynomial], point: &BigRational) -> usize {
210        self.stats.sign_evaluations += seq.len() as u64;
211
212        let signs: Vec<i32> = seq
213            .iter()
214            .map(|p| {
215                let val = p.eval(point);
216                if val.is_positive() {
217                    1
218                } else if val.is_negative() {
219                    -1
220                } else {
221                    0
222                }
223            })
224            .filter(|&s| s != 0) // Skip zeros per Sturm's theorem
225            .collect();
226
227        // Count sign changes
228        let mut changes = 0;
229        for i in 0..signs.len().saturating_sub(1) {
230            if signs[i] != signs[i + 1] {
231                changes += 1;
232            }
233        }
234
235        changes
236    }
237
238    /// Compute bounds containing all real roots (Cauchy bound).
239    ///
240    /// Cauchy bound: |roots| <= 1 + max(|a_i| / |a_n|)
241    pub(crate) fn root_bounds(&self) -> (BigRational, BigRational) {
242        let coeffs = &self.poly.coeffs;
243        if coeffs.is_empty() {
244            return (BigRational::zero(), BigRational::zero());
245        }
246
247        let leading = match coeffs.last() {
248            Some(c) => c.abs(),
249            None => return (BigRational::zero(), BigRational::zero()),
250        };
251
252        if leading.is_zero() {
253            return (BigRational::zero(), BigRational::zero());
254        }
255
256        let mut max_ratio = BigRational::zero();
257        for coeff in coeffs.iter().take(coeffs.len() - 1) {
258            let ratio = coeff.abs() / &leading;
259            if ratio > max_ratio {
260                max_ratio = ratio;
261            }
262        }
263
264        let bound = BigRational::from(BigInt::from(1)) + max_ratio;
265
266        (-bound.clone(), bound)
267    }
268
269    /// Isolate roots in a given interval.
270    fn isolate_in_interval(
271        &mut self,
272        lower: BigRational,
273        upper: BigRational,
274    ) -> Vec<IsolatingInterval> {
275        let num_roots = self.count_roots(&lower, &upper);
276
277        if num_roots == 0 {
278            return Vec::new();
279        }
280
281        if num_roots == 1 {
282            // Single root in interval
283            let sign_lower = self.eval_sign(&lower);
284            let sign_upper = self.eval_sign(&upper);
285
286            self.stats.roots_isolated += 1;
287
288            return vec![IsolatingInterval::new(lower, upper, sign_lower, sign_upper)];
289        }
290
291        // Multiple roots: bisect
292        self.stats.bisection_steps += 1;
293
294        let mid = (&lower + &upper) / BigRational::from(BigInt::from(2));
295
296        let mut left = self.isolate_in_interval(lower, mid.clone());
297        let mut right = self.isolate_in_interval(mid, upper);
298
299        left.append(&mut right);
300        left
301    }
302
303    /// Evaluate sign of polynomial at a point (-1, 0, or 1).
304    fn eval_sign(&mut self, point: &BigRational) -> i32 {
305        self.stats.sign_evaluations += 1;
306
307        let val = self.poly.eval(point);
308
309        if val.is_positive() {
310            1
311        } else if val.is_negative() {
312            -1
313        } else {
314            0
315        }
316    }
317
318    /// Refine an isolating interval.
319    pub fn refine_interval(
320        &mut self,
321        interval: &IsolatingInterval,
322        method: IntervalRefinement,
323    ) -> IsolatingInterval {
324        match method {
325            IntervalRefinement::Bisection => self.refine_bisection(interval),
326            IntervalRefinement::Newton => self.refine_newton(interval),
327            IntervalRefinement::Hybrid => {
328                // Try Newton first, fall back to bisection if not contracting fast enough
329                let newton_result = self.refine_newton(interval);
330                if newton_result.width()
331                    < interval.width() * BigRational::new(BigInt::from(9), BigInt::from(10))
332                {
333                    newton_result
334                } else {
335                    self.refine_bisection(interval)
336                }
337            }
338        }
339    }
340
341    /// Refine interval using bisection.
342    fn refine_bisection(&mut self, interval: &IsolatingInterval) -> IsolatingInterval {
343        self.stats.bisection_steps += 1;
344
345        let mid = interval.midpoint();
346        let sign_mid = self.eval_sign(&mid);
347
348        if sign_mid == 0 {
349            // Exact root
350            return IsolatingInterval::new(mid.clone(), mid, 0, 0);
351        }
352
353        if sign_mid != interval.sign_lower {
354            // Root in [lower, mid]
355            IsolatingInterval::new(interval.lower.clone(), mid, interval.sign_lower, sign_mid)
356        } else {
357            // Root in [mid, upper]
358            IsolatingInterval::new(mid, interval.upper.clone(), sign_mid, interval.sign_upper)
359        }
360    }
361
362    /// Refine interval using Newton's method.
363    fn refine_newton(&mut self, interval: &IsolatingInterval) -> IsolatingInterval {
364        let mid = interval.midpoint();
365        let f_mid = self.poly.eval(&mid);
366        let df_mid = self.poly.derivative().eval(&mid);
367
368        if df_mid.is_zero() {
369            // Derivative is zero - fall back to bisection
370            return self.refine_bisection(interval);
371        }
372
373        // Newton step: x_new = x - f(x)/f'(x)
374        let x_new = &mid - (&f_mid / &df_mid);
375
376        // Ensure new point is strictly inside interval
377        if x_new > interval.lower && x_new < interval.upper {
378            let sign_new = self.eval_sign(&x_new);
379
380            if sign_new == 0 {
381                return IsolatingInterval::new(x_new.clone(), x_new, 0, 0);
382            }
383
384            if sign_new != interval.sign_lower {
385                IsolatingInterval::new(interval.lower.clone(), x_new, interval.sign_lower, sign_new)
386            } else {
387                IsolatingInterval::new(x_new, interval.upper.clone(), sign_new, interval.sign_upper)
388            }
389        } else {
390            // Newton step escaped the interval - use bisection
391            self.refine_bisection(interval)
392        }
393    }
394
395    /// Get statistics.
396    pub fn stats(&self) -> &IsolationStats {
397        &self.stats
398    }
399
400    /// Reset statistics.
401    pub fn reset_stats(&mut self) {
402        self.stats = IsolationStats::default();
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    fn rat(n: i64) -> BigRational {
411        BigRational::from(BigInt::from(n))
412    }
413
414    #[test]
415    fn test_isolator_creation() {
416        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]);
417
418        let isolator = RootIsolator::default_config(poly);
419        assert_eq!(isolator.stats().roots_isolated, 0);
420    }
421
422    #[test]
423    fn test_root_bounds() {
424        let poly = Polynomial::new(vec![rat(-6), rat(5), rat(1)]); // x^2 + 5x - 6
425
426        let isolator = RootIsolator::default_config(poly);
427        let (lower, upper) = isolator.root_bounds();
428
429        // Roots are -6 and 1, so bounds should contain both
430        assert!(lower < rat(-6));
431        assert!(upper > rat(1));
432    }
433
434    #[test]
435    fn test_isolate_linear() {
436        let poly = Polynomial::new(vec![rat(-5), rat(1)]); // x - 5
437
438        let mut isolator = RootIsolator::default_config(poly);
439        let intervals = isolator.isolate_roots();
440
441        assert_eq!(intervals.len(), 1);
442        assert!(intervals[0].lower <= rat(5));
443        assert!(intervals[0].upper >= rat(5));
444    }
445
446    #[test]
447    fn test_refine_bisection() {
448        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]); // x^2 - 2
449
450        let mut isolator = RootIsolator::default_config(poly);
451
452        let interval = IsolatingInterval::new(rat(1), rat(2), -1, 1);
453
454        let refined = isolator.refine_interval(&interval, IntervalRefinement::Bisection);
455
456        assert!(refined.width() < interval.width());
457    }
458
459    #[test]
460    fn test_stats() {
461        let poly = Polynomial::new(vec![rat(-5), rat(1)]);
462
463        let mut isolator = RootIsolator::default_config(poly);
464        isolator.isolate_roots();
465
466        assert!(isolator.stats().roots_isolated > 0);
467        assert!(isolator.stats().sign_evaluations > 0);
468    }
469}