use crate::ball::ArbBall;
use crate::kernel::{ExprId, ExprPool};
use crate::poly::error::ConversionError;
use crate::poly::unipoly::UniPoly;
use rug::Integer;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RealRootError {
NotAPolynomial(ConversionError),
ZeroPolynomial,
}
impl From<ConversionError> for RealRootError {
fn from(e: ConversionError) -> Self {
RealRootError::NotAPolynomial(e)
}
}
impl fmt::Display for RealRootError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RealRootError::NotAPolynomial(e) => write!(f, "not a polynomial: {e}"),
RealRootError::ZeroPolynomial => {
write!(f, "zero polynomial has infinitely many roots (E-ROOT-002)")
}
}
}
}
impl std::error::Error for RealRootError {}
impl crate::errors::AlkahestError for RealRootError {
fn code(&self) -> &'static str {
match self {
RealRootError::NotAPolynomial(_) => "E-ROOT-001",
RealRootError::ZeroPolynomial => "E-ROOT-002",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
RealRootError::NotAPolynomial(_) => Some(
"ensure the input is a polynomial expression with integer coefficients \
in a single variable",
),
RealRootError::ZeroPolynomial => {
Some("real_roots is only defined for non-zero polynomials")
}
}
}
}
#[derive(Debug, Clone)]
pub struct RootInterval {
pub lo: rug::Rational,
pub hi: rug::Rational,
}
impl RootInterval {
pub fn new(lo: rug::Rational, hi: rug::Rational) -> Self {
debug_assert!(lo <= hi, "RootInterval requires lo ≤ hi");
RootInterval { lo, hi }
}
pub fn lo_f64(&self) -> f64 {
self.lo.to_f64()
}
pub fn hi_f64(&self) -> f64 {
self.hi.to_f64()
}
pub fn width(&self) -> rug::Rational {
self.hi.clone() - self.lo.clone()
}
pub fn lo_exact(&self) -> (String, String) {
(self.lo.numer().to_string(), self.lo.denom().to_string())
}
pub fn hi_exact(&self) -> (String, String) {
(self.hi.numer().to_string(), self.hi.denom().to_string())
}
}
impl fmt::Display for RootInterval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.lo, self.hi)
}
}
fn sign_variations(coeffs: &[Integer]) -> usize {
let nonzero: Vec<&Integer> = coeffs.iter().filter(|c| **c != 0).collect();
if nonzero.len() < 2 {
return 0;
}
let mut count = 0;
for w in nonzero.windows(2) {
let pos0 = *w[0] > 0;
let pos1 = *w[1] > 0;
if pos0 != pos1 {
count += 1;
}
}
count
}
fn taylor_shift_by_1(coeffs: &[Integer]) -> Vec<Integer> {
let mut c: Vec<Integer> = coeffs.to_vec();
let n = c.len();
for i in 0..n.saturating_sub(1) {
for j in (i..n - 1).rev() {
let cjp1 = c[j + 1].clone();
c[j] += cjp1;
}
}
c
}
fn taylor_shift_by(coeffs: &[Integer], k: u64) -> Vec<Integer> {
if k == 0 {
return coeffs.to_vec();
}
let mut c = coeffs.to_vec();
let n = c.len();
for i in 0..n.saturating_sub(1) {
for j in (i..n - 1).rev() {
let delta = c[j + 1].clone() * k;
c[j] += delta;
}
}
c
}
fn reverse_coeffs(coeffs: &[Integer]) -> Vec<Integer> {
coeffs.iter().cloned().rev().collect()
}
fn trim_trailing_zeros(c: &mut Vec<Integer>) {
while c.last().is_some_and(|v| *v == 0) {
c.pop();
}
}
fn evaluate_at_1(coeffs: &[Integer]) -> Integer {
coeffs.iter().fold(Integer::from(0), |acc, c| acc + c)
}
fn divide_by_t(coeffs: &[Integer]) -> Vec<Integer> {
debug_assert_eq!(coeffs[0], 0, "divide_by_t: constant term must be zero");
coeffs[1..].to_vec()
}
fn divide_by_t_minus_1(coeffs: &[Integer]) -> Vec<Integer> {
let n = coeffs.len() - 1;
if n == 0 {
return vec![];
}
let mut q = vec![Integer::from(0); n];
q[n - 1] = coeffs[n].clone();
for k in (1..n).rev() {
let qk = q[k].clone();
q[k - 1] = coeffs[k].clone() + qk;
}
q
}
fn remove_content(coeffs: &[Integer]) -> Vec<Integer> {
if coeffs.is_empty() {
return vec![];
}
let g = coeffs.iter().fold(Integer::from(0), |acc, c| {
let ca = c.clone().abs();
acc.gcd(&ca)
});
if g == 0 || g == 1 {
return coeffs.to_vec();
}
coeffs.iter().map(|c| c.clone() / g.clone()).collect()
}
fn derivative_coeffs(coeffs: &[Integer]) -> Vec<Integer> {
if coeffs.len() <= 1 {
return vec![];
}
coeffs[1..]
.iter()
.enumerate()
.map(|(i, c)| c.clone() * (i as u64 + 1))
.collect()
}
fn poly_pseudo_rem(a: &[Integer], b: &[Integer]) -> Vec<Integer> {
let db = b.len().saturating_sub(1);
if db == 0 {
if b.iter().any(|c| *c != 0) {
return vec![];
}
return a.to_vec();
}
let lc_b = b.last().unwrap().clone();
let mut r = a.to_vec();
while r.len().saturating_sub(1) >= db {
let dr = r.len() - 1;
let shift = dr - db;
let lc_r = r.last().unwrap().clone();
for coeff in r[..shift].iter_mut() {
*coeff = lc_b.clone() * coeff.clone();
}
for i in 0..b.len() {
let old = r[i + shift].clone();
r[i + shift] = lc_b.clone() * old - lc_r.clone() * b[i].clone();
}
r.pop();
trim_trailing_zeros(&mut r);
}
r
}
fn poly_gcd(a: &[Integer], b: &[Integer]) -> Vec<Integer> {
let b_zero = b.iter().all(|c| *c == 0);
if b_zero {
let mut g = remove_content(a);
trim_trailing_zeros(&mut g);
if g.last().is_some_and(|v| *v < 0) {
for c in g.iter_mut() {
*c = Integer::from(0) - c.clone();
}
}
return g;
}
let prem = poly_pseudo_rem(a, b);
let prem_zero = prem.iter().all(|c| *c == 0);
if prem_zero {
return poly_gcd(b, &[]);
}
let mut r = remove_content(&prem);
trim_trailing_zeros(&mut r);
poly_gcd(b, &r)
}
fn poly_exact_div(a: &[Integer], b: &[Integer]) -> Vec<Integer> {
let da = a.len() as i64 - 1;
let db = b.len() as i64 - 1;
if da < db || b.iter().all(|c| *c == 0) {
return vec![Integer::from(0)];
}
let deg_q = (da - db) as usize;
let mut q = vec![Integer::from(0); deg_q + 1];
let mut r = a.to_vec();
let lc_b = b.last().unwrap().clone();
for i in (0..=deg_q).rev() {
let lc_r = r[i + b.len() - 1].clone();
let qi = lc_r / lc_b.clone();
q[i] = qi.clone();
for (j, bj) in b.iter().enumerate() {
let old = r[i + j].clone();
r[i + j] = old - qi.clone() * bj.clone();
}
}
q
}
fn squarefree_part(coeffs: &[Integer]) -> Vec<Integer> {
if coeffs.len() <= 1 {
return coeffs.to_vec();
}
let dp = derivative_coeffs(coeffs);
if dp.iter().all(|c| *c == 0) {
return coeffs.to_vec();
}
let g = poly_gcd(coeffs, &dp);
if g.len() <= 1 {
return coeffs.to_vec();
}
let result = poly_exact_div(coeffs, &g);
let mut r = remove_content(&result);
trim_trailing_zeros(&mut r);
if r.last().is_some_and(|v| *v < 0) {
for c in r.iter_mut() {
*c = Integer::from(0) - c.clone();
}
}
r
}
fn cf_lower_bound_floor(coeffs: &[Integer]) -> u64 {
if coeffs.is_empty() {
return 0;
}
let n = coeffs.len() - 1;
if n == 0 {
return 0;
}
let p0 = &coeffs[0];
if *p0 == 0 {
return 0;
}
let sign = *p0 > 0;
let eval_at = |k: u64| -> Integer {
let k_int = Integer::from(k);
coeffs
.iter()
.rev()
.fold(Integer::from(0), |acc, c| acc * k_int.clone() + c.clone())
};
let p1 = evaluate_at_1(coeffs);
if p1 == 0 || (p1 > 0) != sign {
return 0;
}
let mut lo: u64 = 1;
let mut hi: u64 = 2;
let mut found_sign_change = false;
loop {
if hi > 1_000_000 {
break;
}
let pval = eval_at(hi);
if pval == 0 || (pval > 0) != sign {
found_sign_change = true;
break;
}
lo = hi;
hi = hi.saturating_mul(2);
}
if !found_sign_change {
return 0;
}
while hi - lo > 1 {
let mid = lo + (hi - lo) / 2;
let pval = eval_at(mid);
if pval == 0 || (pval > 0) != sign {
hi = mid;
} else {
lo = mid;
}
}
lo
}
struct Frame {
poly: Vec<Integer>,
a: Integer,
b: Integer,
c: Integer,
d: Integer,
just_deflated: bool,
}
fn mobius_interval(
a: &Integer,
b: &Integer,
c: &Integer,
d: &Integer,
) -> (rug::Rational, Option<rug::Rational>) {
let at_zero = rug::Rational::from((b.clone(), d.clone()));
let at_inf = if *c == 0 {
None
} else {
Some(rug::Rational::from((a.clone(), c.clone())))
};
match at_inf {
None => (at_zero, None),
Some(ai) => {
if at_zero <= ai {
(at_zero, Some(ai))
} else {
(ai, Some(at_zero))
}
}
}
}
fn isolate_positive_roots(coeffs: Vec<Integer>) -> Vec<RootInterval> {
if coeffs.is_empty() || coeffs.iter().all(|c| *c == 0) {
return vec![];
}
let mut result = Vec::new();
let mut stack: Vec<Frame> = vec![Frame {
poly: coeffs,
a: Integer::from(1),
b: Integer::from(0),
c: Integer::from(0),
d: Integer::from(1),
just_deflated: false,
}];
let max_iters: usize = 500_000;
let mut iters = 0usize;
while let Some(mut frame) = stack.pop() {
iters += 1;
if iters > max_iters {
break;
}
trim_trailing_zeros(&mut frame.poly);
if frame.poly.is_empty() || frame.poly.iter().all(|c| *c == 0) {
continue;
}
if frame.poly[0] == 0 {
let root_x = rug::Rational::from((frame.b.clone(), frame.d.clone()));
result.push(RootInterval::new(root_x.clone(), root_x));
frame.poly = divide_by_t(&frame.poly);
trim_trailing_zeros(&mut frame.poly);
if frame.poly.is_empty() {
continue;
}
frame.just_deflated = true;
stack.push(frame);
continue;
}
let val_at_1 = evaluate_at_1(&frame.poly);
if val_at_1 == 0 {
let a_plus_b = frame.a.clone() + frame.b.clone();
let c_plus_d = frame.c.clone() + frame.d.clone();
if c_plus_d != 0 {
let root_x = rug::Rational::from((a_plus_b, c_plus_d));
result.push(RootInterval::new(root_x.clone(), root_x));
}
frame.poly = divide_by_t_minus_1(&frame.poly);
trim_trailing_zeros(&mut frame.poly);
if frame.poly.is_empty() {
continue;
}
frame.just_deflated = true;
stack.push(frame);
continue;
}
let v = sign_variations(&frame.poly);
match v {
0 => continue,
1 if !frame.just_deflated => {
let (lo, hi_opt) = mobius_interval(&frame.a, &frame.b, &frame.c, &frame.d);
if let Some(hi) = hi_opt {
result.push(RootInterval::new(lo, hi));
continue;
}
}
_ => {
}
}
frame.just_deflated = false;
let k = cf_lower_bound_floor(&frame.poly);
if k >= 1 {
let new_p = taylor_shift_by(&frame.poly, k);
let ki = Integer::from(k);
let new_b = frame.a.clone() * ki.clone() + frame.b.clone();
let new_d = frame.c.clone() * ki + frame.d.clone();
frame.b = new_b;
frame.d = new_d;
frame.poly = remove_content(&new_p);
trim_trailing_zeros(&mut frame.poly);
if frame.poly.is_empty() {
continue;
}
stack.push(frame);
continue;
}
let a = frame.a.clone();
let b = frame.b.clone();
let c = frame.c.clone();
let d = frame.d.clone();
{
let q_right_raw = taylor_shift_by_1(&frame.poly);
let mut q_right = remove_content(&q_right_raw);
trim_trailing_zeros(&mut q_right);
if !q_right.is_empty() && q_right.iter().any(|c| *c != 0) {
stack.push(Frame {
poly: q_right,
a: a.clone(),
b: a.clone() + b.clone(),
c: c.clone(),
d: c.clone() + d.clone(),
just_deflated: false,
});
}
}
{
let rev = reverse_coeffs(&frame.poly);
let q_left_raw = taylor_shift_by_1(&rev);
let mut q_left = remove_content(&q_left_raw);
trim_trailing_zeros(&mut q_left);
if !q_left.is_empty() && q_left.iter().any(|c| *c != 0) {
stack.push(Frame {
poly: q_left,
a: b.clone(),
b: a + b,
c: d.clone(),
d: c + d,
just_deflated: false,
});
}
}
}
result
}
pub fn real_roots(poly: &UniPoly) -> Result<Vec<RootInterval>, RealRootError> {
let mut coeffs: Vec<Integer> = poly.coefficients();
trim_trailing_zeros(&mut coeffs);
if coeffs.is_empty() {
return Err(RealRootError::ZeroPolynomial);
}
if coeffs.len() == 1 {
return Ok(vec![]); }
if coeffs.last().is_some_and(|v| *v < 0) {
for c in coeffs.iter_mut() {
*c = Integer::from(0) - c.clone();
}
}
let sq = squarefree_part(&coeffs);
let mut result = Vec::new();
let working = if sq[0] == 0 {
result.push(RootInterval::new(
rug::Rational::from(0),
rug::Rational::from(0),
));
sq[1..].to_vec()
} else {
sq.clone()
};
if working.len() <= 1 {
result.sort_by(|a, b| a.lo.partial_cmp(&b.lo).unwrap_or(std::cmp::Ordering::Equal));
return Ok(result);
}
result.extend(isolate_positive_roots(working.clone()));
let neg_coeffs: Vec<Integer> = working
.iter()
.enumerate()
.map(|(i, c)| {
if i % 2 == 1 {
Integer::from(0) - c.clone()
} else {
c.clone()
}
})
.collect();
let neg_pos = isolate_positive_roots(neg_coeffs);
for iv in neg_pos {
let neg_hi = rug::Rational::from((
Integer::from(0) - iv.lo.numer().clone(),
iv.lo.denom().clone(),
));
let neg_lo = rug::Rational::from((
Integer::from(0) - iv.hi.numer().clone(),
iv.hi.denom().clone(),
));
result.push(RootInterval::new(neg_lo, neg_hi));
}
result.sort_by(|a, b| a.lo.partial_cmp(&b.lo).unwrap_or(std::cmp::Ordering::Equal));
Ok(result)
}
pub fn real_roots_symbolic(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Result<Vec<RootInterval>, RealRootError> {
let poly = UniPoly::from_symbolic(expr, var, pool).map_err(RealRootError::NotAPolynomial)?;
real_roots(&poly)
}
pub fn refine_root(poly: &UniPoly, interval: &RootInterval, prec: u32) -> ArbBall {
if interval.lo == interval.hi {
return ArbBall::from_midpoint_radius(interval.lo.to_f64(), 0.0, prec.max(53));
}
let coeffs_f64: Vec<f64> = poly.coefficients().iter().map(|c| c.to_f64()).collect();
let eval = |x: f64| -> f64 { coeffs_f64.iter().rev().fold(0.0_f64, |acc, &c| acc * x + c) };
let target_width = 2.0_f64.powi(-(prec as i32));
let mut lo = interval.lo.to_f64();
let mut hi = interval.hi.to_f64();
let mut f_lo = eval(lo);
for _ in 0..300 {
if hi - lo <= target_width {
break;
}
let mid = (lo + hi) / 2.0;
let f_mid = eval(mid);
if f_lo * f_mid <= 0.0 {
hi = mid;
} else {
lo = mid;
f_lo = f_mid;
}
}
let center = (lo + hi) / 2.0;
let radius = (hi - lo) / 2.0;
ArbBall::from_midpoint_radius(center, radius, prec.max(53))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flint::{FlintInteger, FlintPoly};
use crate::kernel::{Domain, ExprPool};
fn make_poly(coeffs: &[i64]) -> UniPoly {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let mut flint = FlintPoly::new();
for (i, &c) in coeffs.iter().enumerate() {
let fi = FlintInteger::from_i64(c);
flint.set_coeff_flint(i, &fi);
}
UniPoly {
var: x,
coeffs: flint,
}
}
#[test]
fn sv_all_positive() {
let c: Vec<Integer> = vec![1, 2, 3].into_iter().map(Integer::from).collect();
assert_eq!(sign_variations(&c), 0);
}
#[test]
fn sv_alternating() {
let c: Vec<Integer> = vec![1, -1, 1, -1i64]
.into_iter()
.map(Integer::from)
.collect();
assert_eq!(sign_variations(&c), 3);
}
#[test]
fn sv_with_zeros() {
let c: Vec<Integer> = vec![1i64, 0, -1].into_iter().map(Integer::from).collect();
assert_eq!(sign_variations(&c), 1);
}
#[test]
fn taylor_shift_quadratic() {
let c: Vec<Integer> = vec![1, 2, 1i64].into_iter().map(Integer::from).collect();
let shifted = taylor_shift_by_1(&c);
let expected: Vec<Integer> = vec![4, 4, 1i64].into_iter().map(Integer::from).collect();
assert_eq!(shifted, expected);
}
#[test]
fn taylor_shift_linear() {
let c: Vec<Integer> = vec![2, 3i64].into_iter().map(Integer::from).collect();
let shifted = taylor_shift_by_1(&c);
assert_eq!(shifted[0], Integer::from(5i64));
assert_eq!(shifted[1], Integer::from(3i64));
}
#[test]
fn sqfree_linear_already_squarefree() {
let c: Vec<Integer> = vec![-1i64, 1].into_iter().map(Integer::from).collect();
let sq = squarefree_part(&c);
assert_eq!(sq.len(), 2);
}
#[test]
fn sqfree_removes_double_root() {
let c: Vec<Integer> = vec![1i64, -2, 1].into_iter().map(Integer::from).collect();
let sq = squarefree_part(&c);
assert_eq!(sq.len(), 2, "squarefree part of (x-1)² must be degree 1");
}
#[test]
fn sqfree_triple_root() {
let c: Vec<Integer> = vec![-8i64, 12, -6, 1]
.into_iter()
.map(Integer::from)
.collect();
let sq = squarefree_part(&c);
assert_eq!(sq.len(), 2, "squarefree part of (x-2)³ must be degree 1");
}
#[test]
fn div_t_minus_1_basic() {
let c: Vec<Integer> = vec![-1i64, 0, 1].into_iter().map(Integer::from).collect();
assert_eq!(evaluate_at_1(&c), Integer::from(0i64));
let q = divide_by_t_minus_1(&c);
assert_eq!(q.len(), 2);
assert_eq!(
q[0],
Integer::from(1i64),
"constant term of x+1 should be 1"
);
assert_eq!(
q[1],
Integer::from(1i64),
"x-coefficient of x+1 should be 1"
);
}
#[test]
fn pseudo_rem_double_root() {
let a: Vec<Integer> = vec![1i64, -2, 1].into_iter().map(Integer::from).collect();
let b: Vec<Integer> = vec![-2i64, 2].into_iter().map(Integer::from).collect();
let r = poly_pseudo_rem(&a, &b);
assert!(
r.iter().all(|c| *c == 0),
"prem of (x-1)² by 2(x-1) should be 0, got {:?}",
r
);
}
#[test]
fn isolate_x_minus_1() {
let c: Vec<Integer> = vec![-1i64, 1].into_iter().map(Integer::from).collect();
let roots = isolate_positive_roots(c);
assert_eq!(roots.len(), 1);
assert!(roots[0].lo <= 1);
assert!(roots[0].hi >= 1);
}
#[test]
fn isolate_x_squared_minus_1_positive() {
let c: Vec<Integer> = vec![-1i64, 0, 1].into_iter().map(Integer::from).collect();
let roots = isolate_positive_roots(c);
assert_eq!(roots.len(), 1);
assert!(roots[0].lo <= 1);
assert!(roots[0].hi >= 1);
}
#[test]
fn isolate_two_positive_roots() {
let c: Vec<Integer> = vec![2i64, -3, 1].into_iter().map(Integer::from).collect();
let roots = isolate_positive_roots(c);
assert_eq!(roots.len(), 2, "expected 2 positive roots");
let mut sorted = roots;
sorted.sort_by(|a, b| a.lo.partial_cmp(&b.lo).unwrap());
assert!(
sorted[0].hi <= sorted[1].lo,
"intervals must be disjoint: [{},{}] and [{},{}]",
sorted[0].lo,
sorted[0].hi,
sorted[1].lo,
sorted[1].hi
);
}
#[test]
fn real_roots_x_squared_minus_1() {
let poly = make_poly(&[-1, 0, 1]);
let roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 2, "x² - 1 has 2 real roots");
assert!(roots[0].lo < 0);
assert!(roots[1].lo >= 0);
}
#[test]
fn real_roots_no_real_roots() {
let poly = make_poly(&[1, 0, 1]);
let roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 0);
}
#[test]
fn real_roots_cluster_squarefree() {
let poly = make_poly(&[-1, 0, 1]); let roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 2);
}
#[test]
fn real_roots_disjoint() {
let poly = make_poly(&[-6, 11, -6, 1]);
let mut roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 3);
roots.sort_by(|a, b| a.lo.partial_cmp(&b.lo).unwrap());
for w in roots.windows(2) {
assert!(
w[0].hi <= w[1].lo,
"intervals must be disjoint: {} and {}",
w[0],
w[1]
);
}
}
#[test]
fn real_roots_chebyshev_t4() {
let poly = make_poly(&[1, 0, -8, 0, 8]);
let roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 4, "T₄ has 4 real roots");
for r in &roots {
assert!(r.lo >= -1);
assert!(r.hi <= 1);
}
}
#[test]
fn real_roots_constant_zero() {
let poly = make_poly(&[0]);
assert!(matches!(
real_roots(&poly),
Err(RealRootError::ZeroPolynomial)
));
}
#[test]
fn real_roots_constant_nonzero() {
let poly = make_poly(&[5]);
assert_eq!(real_roots(&poly).unwrap().len(), 0);
}
#[test]
fn real_roots_symbolic_x_squared_minus_4() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let xsq = p.pow(x, p.integer(2_i32));
let expr = p.add(vec![xsq, p.integer(-4_i32)]);
let roots = real_roots_symbolic(expr, x, &p).unwrap();
assert_eq!(roots.len(), 2);
assert!(roots[0].lo <= -2);
assert!(roots[0].hi >= -2);
assert!(roots[1].lo <= 2);
assert!(roots[1].hi >= 2);
}
#[test]
fn real_roots_five_distinct() {
let poly = make_poly(&[-120, 274, -225, 85, -15, 1]);
let roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 5, "product (x-1)…(x-5) must have 5 real roots");
for k in 1..=5i64 {
let rk = rug::Rational::from(k);
assert!(
roots.iter().any(|iv| iv.lo <= rk && iv.hi >= rk),
"root at x={k} not enclosed"
);
}
}
#[test]
fn real_roots_disjoint_five() {
let poly = make_poly(&[-120, 274, -225, 85, -15, 1]);
let mut roots = real_roots(&poly).unwrap();
roots.sort_by(|a, b| a.lo.partial_cmp(&b.lo).unwrap());
for w in roots.windows(2) {
assert!(
w[0].hi <= w[1].lo,
"intervals overlap: {} and {}",
w[0],
w[1]
);
}
}
#[test]
fn refine_root_x_minus_3() {
let poly = make_poly(&[-3, 1]);
let roots = real_roots(&poly).unwrap();
assert_eq!(roots.len(), 1);
let ball = refine_root(&poly, &roots[0], 53);
assert!(ball.contains(3.0), "refined ball must contain x=3");
}
}