use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{Signed, Zero};
use crate::base::interval::{Interval, IntervalKind};
use crate::base::numeric::Q;
use crate::poly::Poly;
use crate::poly::zpoly::{integer_scaled, powers, pseudo_rem_pos, z_primitive};
#[derive(Debug, Clone)]
pub(crate) struct SturmChain {
chain: Vec<Poly>,
int_chain: Vec<Vec<BigInt>>,
}
#[allow(dead_code)] impl SturmChain {
pub fn new(p: &Poly) -> Self {
if p.is_zero() {
return Self::from_chain(vec![Poly::zero()]);
}
let p0 = p.square_free_part();
let p1 = p0.derivative();
if p1.is_zero() {
return Self::from_chain(vec![p0]);
}
let mut chain = vec![p0.clone(), p1.clone()];
let mut int_chain = vec![integer_scaled(p0.coeffs()), integer_scaled(p1.coeffs())];
loop {
let n = int_chain.len();
let rem = pseudo_rem_pos(&int_chain[n - 2], &int_chain[n - 1]);
if rem.is_empty() {
break;
}
let next = z_primitive(&rem.into_iter().map(|c| -c).collect::<Vec<_>>());
chain.push(Poly::from_coeffs(
next.iter().cloned().map(Ratio::from_integer).collect(),
));
int_chain.push(next);
}
SturmChain { chain, int_chain }
}
fn from_chain(chain: Vec<Poly>) -> Self {
let int_chain = chain.iter().map(|p| integer_scaled(p.coeffs())).collect();
SturmChain { chain, int_chain }
}
fn max_degree(&self) -> usize {
self.int_chain
.iter()
.map(|c| c.len().saturating_sub(1))
.max()
.unwrap_or(0)
}
fn signs_at(&self, x: &Ratio<BigInt>) -> Vec<i8> {
let (a, b) = numer_denom(x);
let b_pows = powers(&b, self.max_degree());
self.int_chain
.iter()
.map(|c| int_sign_at(c, &a, &b, &b_pows))
.collect()
}
fn is_root(&self, x: &Ratio<BigInt>) -> bool {
let Some(p0) = self.int_chain.first() else {
return false;
};
let (a, b) = numer_denom(x);
let b_pows = powers(&b, p0.len().saturating_sub(1));
int_sign_at(p0, &a, &b, &b_pows) == 0
}
pub fn sign_variations_at(&self, x: &Ratio<BigInt>) -> usize {
count_sign_changes(&self.signs_at(x))
}
pub fn sign_variations_at_pos_inf(&self) -> usize {
let signs: Vec<i8> = self.chain.iter().map(leading_sign).collect();
count_sign_changes(&signs)
}
pub fn sign_variations_at_neg_inf(&self) -> usize {
let signs: Vec<i8> = self
.chain
.iter()
.map(|p| {
let ls = leading_sign(p);
if ls == 0 {
return 0;
}
let deg = p.degree().unwrap_or(0);
if deg % 2 == 0 { ls } else { -ls }
})
.collect();
count_sign_changes(&signs)
}
pub fn count_real_roots(&self) -> usize {
let neg = self.sign_variations_at_neg_inf();
let pos = self.sign_variations_at_pos_inf();
neg.saturating_sub(pos)
}
pub fn has_no_real_roots(&self) -> bool {
self.count_real_roots() == 0
}
pub fn count_roots_in(&self, a: &Ratio<BigInt>, b: &Ratio<BigInt>) -> usize {
let sa = self.sign_variations_at(a);
let sb = self.sign_variations_at(b);
sa.saturating_sub(sb)
}
pub fn isolate_roots_in(&self, a: &Q, b: &Q, max_depth: u32) -> Vec<Interval<Q>> {
let two = Ratio::from_integer(BigInt::from(2));
let mut result = Vec::new();
let mut stack: Vec<Cell> = vec![Cell {
lo: a.clone(),
hi: b.clone(),
depth: max_depth,
var_lo: self.sign_variations_at(a),
var_hi: self.sign_variations_at(b),
}];
while let Some(cell) = stack.pop() {
let n = cell.var_lo.saturating_sub(cell.var_hi);
if n == 0 {
continue;
}
if n == 1 || cell.depth == 0 {
result.push(Interval::left_open(cell.lo, cell.hi));
continue;
}
let mid = (&cell.lo + &cell.hi) / &two;
let var_mid = self.sign_variations_at(&mid);
stack.push(Cell {
lo: mid.clone(),
hi: cell.hi,
depth: cell.depth - 1,
var_lo: var_mid,
var_hi: cell.var_hi,
});
stack.push(Cell {
lo: cell.lo,
hi: mid,
depth: cell.depth - 1,
var_lo: cell.var_lo,
var_hi: var_mid,
});
}
result
}
pub fn isolate_all_real_roots(&self) -> Vec<Interval<Q>> {
let Some(p) = self.chain.first() else {
return vec![];
};
if p.degree().unwrap_or(0) == 0 {
return vec![];
}
let bound = cauchy_bound(p) + Ratio::from_integer(BigInt::from(1));
let neg_bound = -bound.clone();
let raw = self.isolate_roots_in(&neg_bound, &bound, 256);
raw.into_iter()
.map(|iv| {
if self.is_root(&iv.upper) {
Interval::point(iv.upper)
} else {
iv
}
})
.collect()
}
pub fn refine_interval(&self, iv: &Interval<Q>, max_width: &Q) -> Interval<Q> {
let two = Ratio::from_integer(BigInt::from(2));
let mut lo = iv.lower.clone();
let mut hi = iv.upper.clone();
let mut kind = iv.kind;
let mut var_lo: Option<usize> = None;
for _ in 0..512 {
if &hi - &lo <= *max_width || lo == hi {
break;
}
let mid = (&lo + &hi) / &two;
let signs_mid = self.signs_at(&mid);
if signs_mid.first() == Some(&0) {
return Interval::point(mid);
}
let var_mid = count_sign_changes(&signs_mid);
let at_lo = *var_lo.get_or_insert_with(|| self.sign_variations_at(&lo));
if at_lo.saturating_sub(var_mid) == 1 {
hi = mid;
} else {
lo = mid;
var_lo = Some(var_mid);
}
kind = IntervalKind::LeftOpen;
}
Interval {
lower: lo,
upper: hi,
kind,
}
}
pub fn count_roots_in_closed(&self, a: &Ratio<BigInt>, b: &Ratio<BigInt>) -> usize {
if a > b {
return 0;
}
let open_right = self.count_roots_in(a, b);
open_right + usize::from(self.is_root(a))
}
pub fn leading_sign_of_original(&self) -> i8 {
self.chain.first().map_or(0, leading_sign)
}
}
struct Cell {
lo: Q,
hi: Q,
depth: u32,
var_lo: usize,
var_hi: usize,
}
fn numer_denom(x: &Ratio<BigInt>) -> (BigInt, BigInt) {
if x.denom().is_negative() {
(-x.numer(), -x.denom())
} else {
(x.numer().clone(), x.denom().clone())
}
}
fn int_sign_at(c: &[BigInt], a: &BigInt, b: &BigInt, b_pows: &[BigInt]) -> i8 {
let Some((lead, rest)) = c.split_last() else {
return 0;
};
let n = rest.len();
let mut v = lead.clone();
for (i, ci) in rest.iter().enumerate().rev() {
v *= a;
if !ci.is_zero() {
match b_pows.get(n - i) {
Some(bp) => v += ci * bp,
None => v += ci * b.pow((n - i) as u32),
}
}
}
match v.sign() {
num_bigint::Sign::Plus => 1,
num_bigint::Sign::Minus => -1,
num_bigint::Sign::NoSign => 0,
}
}
pub(crate) fn cauchy_bound(p: &Poly) -> Ratio<BigInt> {
let one = Ratio::from_integer(BigInt::from(1));
let Some(n) = p.degree() else {
return one;
};
if n == 0 {
return one;
}
let lc = p.coeff(n);
let mut max = Ratio::from_integer(BigInt::from(0));
for i in 0..n {
let r = (p.coeff(i) / &lc).abs();
if r > max {
max = r;
}
}
max + one
}
fn leading_sign(p: &Poly) -> i8 {
match p.leading_coeff() {
None => 0,
Some(c) if c.is_positive() => 1,
Some(c) if c.is_negative() => -1,
Some(_) => 0, }
}
fn count_sign_changes(signs: &[i8]) -> usize {
let nonzero: Vec<i8> = signs.iter().copied().filter(|&s| s != 0).collect();
nonzero.windows(2).filter(|w| w[0] != w[1]).count()
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::ToPrimitive;
fn r(n: i64) -> Ratio<BigInt> {
Ratio::from_integer(BigInt::from(n))
}
fn binomial_tail(n: usize, k: usize, denom: i64) -> Poly {
let mut acc = Poly::zero();
let one_minus_x = Poly::from_coeffs(vec![r(1), r(-1)]);
let x = Poly::x();
for j in k..=n {
let mut c = r(1);
for i in 0..j {
c *= Ratio::new(BigInt::from(n - i), BigInt::from(i + 1));
}
let mut term = Poly::from_coeffs(vec![c]);
for _ in 0..j {
term = &term * &x;
}
for _ in 0..(n - j) {
term = &term * &one_minus_x;
}
acc = &acc + &term;
}
&acc - &Poly::from_coeffs(vec![Ratio::new(BigInt::from(1), BigInt::from(denom))])
}
#[test]
fn degree_40_tail_isolation_is_fast_and_correct() {
let f = binomial_tail(40, 12, 40);
assert_eq!(f.degree(), Some(40));
let start = std::time::Instant::now();
let chain = SturmChain::new(&f);
let ivs = chain.isolate_all_real_roots();
assert_eq!(ivs.len(), 2, "{ivs:?}");
let width = Ratio::new(BigInt::from(1), BigInt::from(1024));
let refined: Vec<Interval<Q>> = ivs
.iter()
.map(|iv| chain.refine_interval(iv, &width))
.collect();
assert!(start.elapsed().as_secs() < 5, "took {:?}", start.elapsed());
for (iv, fine) in ivs.iter().zip(&refined) {
assert!(&fine.upper - &fine.lower <= width);
assert!(iv.lower <= fine.lower && fine.upper <= iv.upper);
assert_eq!(chain.count_roots_in(&fine.lower, &fine.upper), 1);
}
let lo = refined[1].lower.to_f64().unwrap_or(f64::NAN);
let hi = refined[1].upper.to_f64().unwrap_or(f64::NAN);
assert!(
lo <= 0.1656272043932356 && 0.1656272043932356 <= hi,
"[{lo}, {hi}]"
);
}
fn pseudo_random_poly(seed: u64, degree: usize, square: bool) -> Poly {
let mut state = seed;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state % 19) as i64 - 9
};
let mut coeffs: Vec<Ratio<BigInt>> = (0..=degree).map(|_| r(next())).collect();
if coeffs[degree].is_zero() {
coeffs[degree] = r(1);
}
let p = Poly::from_coeffs(coeffs);
if square {
let q = Poly::from_coeffs(vec![r(next()), r(next()), r(1)]);
&(&p * &q) * &q
} else {
p
}
}
#[test]
fn integer_chain_matches_rational_definition() {
for seed in 1..=12u64 {
let p = pseudo_random_poly(seed, 8 + (seed as usize % 5), seed % 3 == 0);
let chain = SturmChain::new(&p);
let p0 = p.square_free_part();
assert_eq!(chain.chain[0], p0, "seed {seed}: square-free part");
let mut expected = vec![p0.clone(), p0.derivative()];
loop {
let n = expected.len();
let rem = expected[n - 2].rem(&expected[n - 1]);
if rem.is_zero() {
break;
}
expected.push((-&rem).primitive_part());
}
assert_eq!(chain.chain, expected, "seed {seed}");
for (q, z) in chain.chain.iter().zip(&chain.int_chain) {
let x = Ratio::new(BigInt::from(-7), BigInt::from(3));
let zq = Poly::from_coeffs(z.iter().cloned().map(Ratio::from_integer).collect());
assert_eq!(q.eval(&x).is_positive(), zq.eval(&x).is_positive());
assert_eq!(q.eval(&x).is_zero(), zq.eval(&x).is_zero());
}
}
}
#[test]
fn integer_sign_evaluation_matches_rational() {
let p = pseudo_random_poly(5, 11, true);
let chain = SturmChain::new(&p);
let xs = [
r(0),
r(3),
r(-2),
Ratio::new(BigInt::from(5), BigInt::from(7)),
Ratio::new(BigInt::from(-1234567), BigInt::from(89)),
Ratio::new(BigInt::from(1), BigInt::from(1) << 40),
];
for x in &xs {
let expected: Vec<i8> = chain
.chain
.iter()
.map(|q| {
let v = q.eval(x);
if v.is_positive() {
1
} else if v.is_negative() {
-1
} else {
0
}
})
.collect();
assert_eq!(chain.signs_at(x), expected, "at {x}");
}
let root = Ratio::new(BigInt::from(2), BigInt::from(1));
let q = &p * &Poly::from_coeffs(vec![r(-2), r(1)]);
assert!(SturmChain::new(&q).is_root(&root));
assert!(!SturmChain::new(&q).is_root(&r(1)) || q.eval(&r(1)).is_zero());
}
#[test]
fn integer_square_free_part_matches_generic() {
fn euclid_square_free_part(p: &Poly) -> Poly {
let dp = p.derivative();
if dp.is_zero() {
return p.clone();
}
p.div(&Poly::gcd_euclid(p, &dp))
}
for seed in 1..=10u64 {
let p = pseudo_random_poly(seed, 6 + seed as usize % 4, true);
assert_eq!(
euclid_square_free_part(&p),
p.square_free_part(),
"seed {seed}"
);
assert_eq!(SturmChain::new(&p).chain[0], p.square_free_part());
let q = pseudo_random_poly(seed + 100, 9, false);
assert_eq!(
euclid_square_free_part(&q),
q.square_free_part(),
"seed {seed}"
);
}
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
let p = Poly::from_coeffs(vec![half.clone(), r(3), half, r(1)]);
let p2 = &p * &p;
assert_eq!(euclid_square_free_part(&p2), p2.square_free_part());
}
#[test]
fn sturm_x2_minus_1() {
let p = Poly::from_coeffs(vec![r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 2);
assert!(!chain.has_no_real_roots());
}
#[test]
fn sturm_x2_plus_1() {
let p = Poly::from_coeffs(vec![r(1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 0);
assert!(chain.has_no_real_roots());
}
#[test]
fn sturm_x3_minus_x() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 3);
}
#[test]
fn sturm_x() {
let p = Poly::x();
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 1);
}
#[test]
fn count_roots_in_subintervals() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_roots_in(&r(-2), &r(2)), 3);
let neg_half = Ratio::new(BigInt::from(-1), BigInt::from(2));
assert_eq!(chain.count_roots_in(&r(-2), &neg_half), 1);
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
assert_eq!(chain.count_roots_in(&neg_half, &half), 1);
assert_eq!(chain.count_roots_in(&half, &r(2)), 1);
assert_eq!(chain.count_roots_in(&r(2), &r(10)), 0);
}
#[test]
fn isolate_roots_x3_minus_x() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
let intervals = chain.isolate_roots_in(&r(-10), &r(10), 50);
assert_eq!(intervals.len(), 3, "should isolate 3 roots");
for iv in &intervals {
assert_eq!(iv.kind, IntervalKind::LeftOpen);
assert_eq!(chain.count_roots_in(&iv.lower, &iv.upper), 1);
}
}
#[test]
fn isolate_roots_no_real() {
let p = Poly::from_coeffs(vec![r(1), r(0), r(1)]);
let chain = SturmChain::new(&p);
let intervals = chain.isolate_roots_in(&r(-100), &r(100), 50);
assert!(intervals.is_empty());
}
#[test]
fn sturm_constant() {
let p = Poly::from_int(5);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 0);
assert!(chain.has_no_real_roots());
}
#[test]
fn sturm_zero() {
let p = Poly::zero();
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 0);
}
#[test]
fn sturm_x2_minus_2() {
let p = Poly::from_coeffs(vec![r(-2), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 2);
}
#[test]
fn sturm_repeated_root() {
let p = Poly::from_coeffs(vec![r(1), r(-2), r(1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 1);
}
#[test]
fn isolate_all_x3_minus_x() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
let iv = chain.isolate_all_real_roots();
assert_eq!(iv.len(), 3);
for w in iv.windows(2) {
assert!(w[0].upper <= w[1].lower);
}
for i in &iv {
if i.is_point() {
assert!(p.eval(&i.lower).is_zero());
} else {
assert_eq!(i.kind, IntervalKind::LeftOpen);
assert_eq!(chain.count_roots_in(&i.lower, &i.upper), 1);
assert!(!p.eval(&i.upper).is_zero());
}
}
}
#[test]
fn isolate_all_x2_minus_2() {
let p = Poly::from_coeffs(vec![r(-2), r(0), r(1)]);
let chain = SturmChain::new(&p);
let iv = chain.isolate_all_real_roots();
assert_eq!(iv.len(), 2);
assert!(iv[0].upper <= r(0) && iv[1].lower >= r(0));
let width = Ratio::new(BigInt::from(1), BigInt::from(100));
let refined = chain.refine_interval(&iv[1], &width);
assert_eq!(refined.kind, IntervalKind::LeftOpen);
assert!(refined.width() <= width);
assert!(refined.lower < Ratio::new(BigInt::from(1415), BigInt::from(1000)));
assert!(refined.upper > Ratio::new(BigInt::from(1414), BigInt::from(1000)));
let zero = Interval::point(r(0));
assert_eq!(chain.refine_interval(&zero, &width), zero);
}
#[test]
fn closed_interval_count() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.count_roots_in_closed(&r(-1), &r(1)), 3);
assert_eq!(chain.count_roots_in(&r(-1), &r(1)), 2);
assert_eq!(chain.count_roots_in_closed(&r(0), &r(0)), 1);
assert_eq!(chain.count_roots_in_closed(&r(2), &r(1)), 0);
}
#[test]
fn leading_sign_positive() {
let p = Poly::from_coeffs(vec![r(1), r(0), r(1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.leading_sign_of_original(), 1);
}
#[test]
fn leading_sign_negative() {
let p = Poly::from_coeffs(vec![r(-1), r(0), r(-1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.leading_sign_of_original(), -1);
}
}