use symplex::prelude::*;
fn real_count(roots: &[(f64, f64)]) -> usize {
roots.iter().filter(|(_, im)| *im == 0.0).count()
}
#[test]
fn x_cubed_plus_x_has_one_real_root_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.powi(3) + &x;
assert_eq!(f.count_real_roots(&x), Some(1));
let roots = f.nroots(&x, 20).unwrap();
assert_eq!(roots.len(), 3, "{roots:?}");
assert_eq!(real_count(&roots), 1, "{roots:?}");
let zero = roots.iter().find(|(_, im)| *im == 0.0).unwrap();
assert_eq!(zero.0, 0.0, "exact zero root: {roots:?}");
let cplx: Vec<_> = roots.iter().filter(|(_, im)| *im != 0.0).collect();
assert_eq!(cplx.len(), 2);
for (re, im) in &cplx {
assert!(
re.abs() < 1e-30 && (im.abs() - 1.0).abs() < 1e-14,
"{roots:?}"
);
}
}
#[test]
fn real_roots_have_exactly_zero_imaginary_part() {
let ctx = Context::new();
let x = ctx.symbol("x");
let cases: Vec<(Ex, usize)> = vec![
(x.powi(2) - 2, 2),
(x.powi(3) - &x, 3),
(x.powi(3) - &x - 1, 1),
(x.powi(4) - 1, 2),
(x.powi(5) - &x - 1, 1),
(x.powi(4) + 1, 0),
(x.powi(2) + 1, 0),
(x.powi(3) + &x * 2, 1),
(x.powi(6) - &x.powi(3) * 3 + 2, 2),
((&x.powi(2) - 2) * (&x.powi(2) + 3) * (&x - 5), 3),
];
for (f, expected) in cases {
let sturm = f.count_real_roots(&x).unwrap();
assert_eq!(sturm, expected, "Sturm count for {f}");
let roots = f.nroots(&x, 20).unwrap();
assert_eq!(real_count(&roots), sturm, "{f}: {roots:?}");
for (re, im) in &roots {
assert!(
*im == 0.0 || im.abs() > 1e-6,
"{f}: root ({re}, {im}) neither real nor clearly complex"
);
}
}
}
#[test]
fn zero_root_with_multiplicity_is_exact() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.powi(4) + x.powi(2);
let roots = f.nroots(&x, 20).unwrap();
assert_eq!(roots.len(), 4);
let zeros = roots.iter().filter(|r| **r == (0.0, 0.0)).count();
assert_eq!(zeros, 2, "{roots:?}");
assert_eq!(real_count(&roots), 2);
assert_eq!(
f.count_real_roots(&x),
Some(1),
"Sturm counts distinct roots"
);
let sf = f.square_free_part(&x).unwrap();
assert_eq!(real_count(&sf.nroots(&x, 20).unwrap()), 1);
}
#[test]
fn complex_roots_keep_their_imaginary_parts() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (x.powi(2) + 1) * (x.powi(2) + 4);
let roots = f.nroots(&x, 20).unwrap();
assert_eq!(real_count(&roots), 0, "{roots:?}");
for (re, _) in &roots {
assert!(re.abs() < 1e-30, "{roots:?}");
}
let mut ims: Vec<f64> = roots.iter().map(|r| r.1).collect();
ims.sort_by(|a, b| a.partial_cmp(b).unwrap());
for (got, want) in ims.iter().zip([-2.0, -1.0, 1.0, 2.0]) {
assert!((got - want).abs() < 1e-12, "{roots:?}");
}
}
#[test]
fn nearly_real_complex_pair_is_not_flattened() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (&x - 1).powi(2) + ctx.rational(1, 10_000_000_000);
assert_eq!(f.count_real_roots(&x), Some(0));
let roots = f.nroots(&x, 20).unwrap();
assert_eq!(real_count(&roots), 0, "{roots:?}");
for (_, im) in &roots {
assert!((im.abs() - 1e-5).abs() < 1e-12, "{roots:?}");
}
}
#[test]
fn sturm_matches_nroots_on_sampled_polynomials() {
let ctx = Context::new();
let x = ctx.symbol("x");
let coeffs = [-3i64, -1, 0, 1, 2];
for &a in &coeffs {
for &b in &coeffs {
for &c in &coeffs {
for &d in &[-2i64, 1, 3] {
let f = &x.powi(3) * d + &x.powi(2) * c + &x * b + a;
let sturm = f.count_real_roots(&x).unwrap();
let sf = f.square_free_part(&x).unwrap();
let roots = sf.nroots(&x, 20).unwrap();
assert_eq!(real_count(&roots), sturm, "{f}: {roots:?}");
}
}
}
}
}