#[allow(unused_imports)]
use crate::prelude::*;
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::Zero;
const MAX_ROOT_ISOLATION_DEPTH: u32 = 4096;
pub struct RootIsolator {
precision: BigRational,
max_iterations: usize,
stats: IsolationStats,
}
#[derive(Debug, Clone)]
pub struct RootInterval {
pub left: BigRational,
pub right: BigRational,
pub left_closed: bool,
pub right_closed: bool,
pub multiplicity: usize,
}
#[derive(Debug, Clone, Default)]
pub struct IsolationStats {
pub sturm_evaluations: usize,
pub descartes_tests: usize,
pub bisection_steps: usize,
pub intervals_generated: usize,
pub incomplete: bool,
}
struct BisectItem {
lo: BigRational,
hi: BigRational,
lo_vars: usize,
hi_vars: usize,
depth: u32,
}
impl RootIsolator {
pub fn new(precision: BigRational) -> Self {
Self {
precision,
max_iterations: 1000,
stats: IsolationStats::default(),
}
}
pub fn isolate_roots(
&mut self,
poly: &[BigRational],
interval: (BigRational, BigRational),
) -> Vec<RootInterval> {
self.isolate_roots_bounded(poly, interval, MAX_ROOT_ISOLATION_DEPTH)
}
fn isolate_roots_bounded(
&mut self,
poly: &[BigRational],
interval: (BigRational, BigRational),
max_depth: u32,
) -> Vec<RootInterval> {
let poly = Self::normalize_polynomial(poly);
if poly.len() <= 1 {
return vec![];
}
let sturm_seq = self.build_sturm_sequence(&poly);
let (left, right) = interval;
let (left, right) = if left <= right {
(left, right)
} else {
(right, left)
};
let left_variations = self.count_sign_variations(&sturm_seq, &left);
let right_variations = self.count_sign_variations(&sturm_seq, &right);
self.stats.sturm_evaluations += 2;
let num_roots = left_variations.saturating_sub(right_variations);
if num_roots == 0 {
return vec![];
} else if num_roots == 1 {
let refined = self.refine_root_interval(&poly, left, right);
return vec![refined];
}
self.bisect_and_isolate(
&poly,
&sturm_seq,
BisectItem {
lo: left,
hi: right,
lo_vars: left_variations,
hi_vars: right_variations,
depth: max_depth,
},
)
}
fn build_sturm_sequence(&self, poly: &[BigRational]) -> Vec<Vec<BigRational>> {
let mut sequence = Vec::new();
sequence.push(poly.to_vec());
let derivative = Self::derivative(poly);
if derivative.is_empty() {
return sequence;
}
sequence.push(derivative);
loop {
let len = sequence.len();
let f_prev = &sequence[len - 2];
let f_curr = &sequence[len - 1];
let remainder = Self::polynomial_remainder(f_prev, f_curr);
if remainder.is_empty() || Self::is_zero_poly(&remainder) {
break;
}
let neg_remainder: Vec<BigRational> = remainder.iter().map(|c| -c.clone()).collect();
sequence.push(neg_remainder);
}
sequence
}
fn count_sign_variations(&self, sturm_seq: &[Vec<BigRational>], x: &BigRational) -> usize {
let mut signs = Vec::new();
for poly in sturm_seq {
let value = Self::evaluate(poly, x);
if !value.is_zero() {
signs.push(value > BigRational::zero());
}
}
let mut variations = 0;
for i in 0..signs.len().saturating_sub(1) {
if signs[i] != signs[i + 1] {
variations += 1;
}
}
variations
}
fn bisect_and_isolate(
&mut self,
poly: &[BigRational],
sturm_seq: &[Vec<BigRational>],
initial: BisectItem,
) -> Vec<RootInterval> {
let mut intervals = Vec::new();
let mut work = vec![initial];
while let Some(BisectItem {
lo,
hi,
lo_vars,
hi_vars,
depth,
}) = work.pop()
{
let num_roots = lo_vars.saturating_sub(hi_vars);
if num_roots == 0 {
continue;
}
if num_roots == 1 {
intervals.push(self.refine_root_interval(poly, lo, hi));
continue;
}
if depth == 0 {
self.stats.incomplete = true;
continue;
}
self.stats.bisection_steps += 1;
let mid = (&lo + &hi) / BigRational::from_integer(BigInt::from(2));
let mid_vars = self.count_sign_variations(sturm_seq, &mid);
self.stats.sturm_evaluations += 1;
let left_roots = lo_vars.saturating_sub(mid_vars);
let right_roots = mid_vars.saturating_sub(hi_vars);
if right_roots > 0 {
work.push(BisectItem {
lo: mid.clone(),
hi,
lo_vars: mid_vars,
hi_vars,
depth: depth - 1,
});
}
if left_roots > 0 {
work.push(BisectItem {
lo,
hi: mid,
lo_vars,
hi_vars: mid_vars,
depth: depth - 1,
});
}
}
intervals
}
fn refine_root_interval(
&mut self,
poly: &[BigRational],
mut left: BigRational,
mut right: BigRational,
) -> RootInterval {
let mut iterations = 0;
while &right - &left > self.precision && iterations < self.max_iterations {
let mid = (&left + &right) / BigRational::from_integer(BigInt::from(2));
let mid_val = Self::evaluate(poly, &mid);
if mid_val.is_zero() {
return RootInterval {
left: mid.clone(),
right: mid,
left_closed: true,
right_closed: true,
multiplicity: 1,
};
}
let left_val = Self::evaluate(poly, &left);
if (left_val > BigRational::zero()) == (mid_val > BigRational::zero()) {
left = mid;
} else {
right = mid;
}
iterations += 1;
}
self.stats.intervals_generated += 1;
RootInterval {
left,
right,
left_closed: false,
right_closed: false,
multiplicity: 1,
}
}
fn evaluate(poly: &[BigRational], x: &BigRational) -> BigRational {
if poly.is_empty() {
return BigRational::zero();
}
let mut result = poly[0].clone();
for coeff in &poly[1..] {
result = result * x + coeff;
}
result
}
fn derivative(poly: &[BigRational]) -> Vec<BigRational> {
if poly.len() <= 1 {
return vec![];
}
let mut deriv = Vec::with_capacity(poly.len() - 1);
for (i, coeff) in poly.iter().enumerate().take(poly.len() - 1) {
let degree = (poly.len() - 1 - i) as i64;
deriv.push(coeff * BigRational::from_integer(BigInt::from(degree)));
}
deriv
}
fn polynomial_remainder(dividend: &[BigRational], divisor: &[BigRational]) -> Vec<BigRational> {
if divisor.is_empty() || Self::is_zero_poly(divisor) {
return vec![];
}
let mut remainder = dividend.to_vec();
while remainder.len() >= divisor.len() && !Self::is_zero_poly(&remainder) {
let lead_div = &divisor[0];
let lead_rem = &remainder[0];
if lead_div.is_zero() {
break;
}
let quotient_coeff = lead_rem / lead_div;
for i in 0..divisor.len() {
remainder[i] = &remainder[i] - "ient_coeff * &divisor[i];
}
remainder.remove(0);
}
remainder
}
fn normalize_polynomial(poly: &[BigRational]) -> Vec<BigRational> {
let mut result = poly.to_vec();
while !result.is_empty() && result[0].is_zero() {
result.remove(0);
}
result
}
fn is_zero_poly(poly: &[BigRational]) -> bool {
poly.iter().all(|c| c.is_zero())
}
pub fn stats(&self) -> &IsolationStats {
&self.stats
}
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::{One, Zero};
#[test]
fn test_root_isolator() {
let precision = BigRational::new(BigInt::from(1), BigInt::from(1000));
let isolator = RootIsolator::new(precision);
assert_eq!(isolator.stats.sturm_evaluations, 0);
}
#[test]
fn test_sturm_sequence() {
let precision = BigRational::new(BigInt::from(1), BigInt::from(100));
let isolator = RootIsolator::new(precision);
let poly = vec![
BigRational::one(),
BigRational::zero(),
BigRational::from_integer(BigInt::from(-2)),
];
let sturm = isolator.build_sturm_sequence(&poly);
assert!(!sturm.is_empty());
}
fn rat(n: i64) -> BigRational {
BigRational::from_integer(BigInt::from(n))
}
#[test]
fn test_isolate_roots_multiple_roots_via_bisection_behaviour_preserved() {
let poly = vec![rat(1), rat(0), rat(-2)];
let precision = BigRational::new(BigInt::from(1), BigInt::from(1_000_000));
let mut isolator = RootIsolator::new(precision);
let intervals = isolator.isolate_roots(&poly, (rat(-10), rat(10)));
assert_eq!(intervals.len(), 2, "x^2 - 2 has exactly two real roots");
for iv in &intervals {
assert!(iv.left <= iv.right);
}
assert!(
intervals[0].right <= intervals[1].left || intervals[1].right <= intervals[0].left,
"isolating intervals must not overlap: {:?}",
intervals
);
assert!(
!isolator.stats().incomplete,
"a normal, well-separated polynomial must never hit the depth cap"
);
}
#[test]
fn test_isolate_roots_bounded_depth_cap_is_visible_not_silent() {
let poly = vec![rat(1), rat(0), rat(-1), rat(0)];
let precision = BigRational::new(BigInt::from(1), BigInt::from(1_000_000));
let mut isolator = RootIsolator::new(precision);
let results = isolator.isolate_roots_bounded(&poly, (rat(-10), rat(10)), 1);
assert!(
isolator.stats().incomplete,
"an insufficient depth budget must be recorded as incomplete"
);
assert!(
results.len() < 3,
"a truncated search must not fabricate all three roots, got {results:?}"
);
}
#[test]
fn test_isolate_roots_bounded_sufficient_depth_never_marks_incomplete() {
let poly = vec![rat(1), rat(0), rat(-1), rat(0)];
let precision = BigRational::new(BigInt::from(1), BigInt::from(1_000_000));
let mut isolator = RootIsolator::new(precision);
let intervals = isolator.isolate_roots(&poly, (rat(-10), rat(10)));
assert_eq!(intervals.len(), 3);
assert!(!isolator.stats().incomplete);
}
#[test]
fn test_isolate_roots_deep_bisection_small_stack() {
let handle = std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(|| {
let mut den = BigInt::from(1u32);
for _ in 0..2000 {
den *= 2;
}
let eps = BigRational::new(BigInt::from(1), den);
let poly = vec![rat(1), -eps, rat(0)];
let precision = BigRational::new(BigInt::from(1), BigInt::from(1_000_000));
let mut isolator = RootIsolator::new(precision);
let intervals = isolator.isolate_roots(&poly, (rat(-1), rat(1)));
assert_eq!(intervals.len(), 2);
assert!(!isolator.stats().incomplete);
})
.expect("spawning a thread with an explicit stack size must succeed");
handle
.join()
.expect("a deep-but-finite bisection must not overflow a 1 MiB stack");
}
}