use astro_float::{BigFloat, Consts, RoundingMode};
use num_bigint::BigInt;
use num_complex::Complex64;
use num_rational::Ratio;
use num_traits::{ToPrimitive, Zero};
use super::dense::Poly;
use crate::base::bigcomplex::{c_add, c_div, c_mul, c_one, c_sub, c_zero};
use crate::base::numeric;
pub(crate) use crate::base::bigcomplex::Complex;
#[cfg(test)]
fn poly_eval_complex(
coeffs: &[Ratio<BigInt>],
z: &Complex,
prec: usize,
rm: RoundingMode,
) -> Complex {
let bf: Vec<BigFloat> = coeffs.iter().map(|c| ratio_to_bigfloat(c, prec)).collect();
poly_eval_complex_bf(&bf, z, prec, rm)
}
fn poly_eval_complex_bf(
coeffs: &[BigFloat],
z: &Complex,
prec: usize,
rm: RoundingMode,
) -> Complex {
let mut result = c_zero(prec);
for c in coeffs.iter().rev() {
result = c_mul(&result, z, prec, rm);
result.0 = result.0.add(c, prec, rm);
}
result
}
fn ratio_to_bigfloat(r: &Ratio<BigInt>, prec: usize) -> BigFloat {
numeric::ratio_to_bigfloat(r, prec, RoundingMode::None)
}
fn cauchy_bound(poly: &Poly, prec: usize) -> BigFloat {
ratio_to_bigfloat(&super::sturm::cauchy_bound(poly), prec)
}
fn log2_abs(r: &Ratio<BigInt>) -> f64 {
match r.to_f64() {
Some(v) if v.is_finite() && v != 0.0 => v.abs().log2(),
_ => r.numer().bits() as f64 - r.denom().bits() as f64,
}
}
fn fujiwara_bound(poly: &Poly) -> Option<f64> {
let n = poly.degree()?;
if n == 0 {
return None;
}
let lc = poly.coeff(n);
if lc.is_zero() {
return None;
}
let mut max_log = f64::NEG_INFINITY;
for i in 1..=n {
let c = poly.coeff(n - i);
if c.is_zero() {
continue;
}
let mut ratio = &c / &lc;
if i == n {
ratio /= Ratio::from_integer(BigInt::from(2));
}
let l = log2_abs(&ratio) / i as f64;
if l > max_log {
max_log = l;
}
}
if !max_log.is_finite() {
return None;
}
let bound = 2.0 * max_log.exp2();
(bound.is_finite() && bound > 0.0).then_some(bound)
}
fn start_radius(poly: &Poly, prec: usize) -> BigFloat {
match fujiwara_bound(poly) {
Some(b) => BigFloat::from_f64(b * 1.1, prec),
None => cauchy_bound(poly, prec),
}
}
struct StartCircle {
start: usize,
count: usize,
radius: f64,
}
fn newton_polygon_circles(poly: &Poly) -> Option<Vec<StartCircle>> {
let n = poly.degree()?;
let pts: Vec<(usize, f64)> = poly
.coeffs()
.iter()
.enumerate()
.filter(|(_, c)| !c.is_zero())
.map(|(i, c)| (i, log2_abs(c)))
.collect();
if pts.len() < 2 || pts.iter().any(|(_, y)| !y.is_finite()) {
return None;
}
let mut hull: Vec<(usize, f64)> = Vec::with_capacity(pts.len());
for &p in &pts {
while hull.len() >= 2 {
let o = hull[hull.len() - 2];
let a = hull[hull.len() - 1];
let cross =
(a.0 as f64 - o.0 as f64) * (p.1 - o.1) - (a.1 - o.1) * (p.0 as f64 - o.0 as f64);
if cross >= 0.0 {
hull.pop();
} else {
break;
}
}
hull.push(p);
}
let mut circles: Vec<StartCircle> = Vec::with_capacity(hull.len() - 1);
for w in hull.windows(2) {
let (k, yk) = w[0];
let (l, yl) = w[1];
let count = l - k;
let radius = ((yk - yl) / count as f64).exp2();
if !radius.is_finite() || radius <= 0.0 {
return None;
}
circles.push(StartCircle {
start: k,
count,
radius,
});
}
(circles.iter().map(|c| c.count).sum::<usize>() == n).then_some(circles)
}
fn initial_guesses(poly: &Poly, n: usize, prec: usize, cc: &mut Consts) -> Vec<Complex> {
let rm = RoundingMode::None;
let two_pi = cc.pi(prec, rm).mul(&BigFloat::from_i32(2, prec), prec, rm);
let n_bf = BigFloat::from_i64(n as i64, prec);
let quarter = BigFloat::from_f64(0.25, prec);
let offset = BigFloat::from_f64(0.4, prec);
let mut point = |center: &BigFloat, radius: &BigFloat, frac: &BigFloat, rotation: &BigFloat| {
let angle = two_pi
.mul(frac, prec, rm)
.add(rotation, prec, rm)
.add(&offset, prec, rm);
let cos_a = angle.cos(prec, rm, cc);
let sin_a = angle.sin(prec, rm, cc);
let re = center.add(&radius.mul(&cos_a, prec, rm), prec, rm);
let im = radius.mul(&sin_a, prec, rm);
(re, im)
};
if let Some(circles) = newton_polygon_circles(poly) {
let zero = BigFloat::new(prec);
let mut guesses = Vec::with_capacity(n);
for circle in circles {
let radius = BigFloat::from_f64(circle.radius, prec);
let m_bf = BigFloat::from_i64(circle.count as i64, prec);
let rotation = two_pi
.mul(&BigFloat::from_i64(circle.start as i64, prec), prec, rm)
.div(&n_bf, prec, rm);
for j in 0..circle.count {
let frac = BigFloat::from_i64(j as i64, prec)
.add(&quarter, prec, rm)
.div(&m_bf, prec, rm);
guesses.push(point(&zero, &radius, &frac, &rotation));
}
}
return guesses;
}
let radius = start_radius(poly, prec);
let center = if n >= 2 && poly.coeffs().len() > n {
let an = &poly.coeffs()[n];
let an1 = &poly.coeffs()[n - 1];
if !an.is_zero() {
let ratio = -(an1 / an) / Ratio::from_integer(BigInt::from(n));
ratio_to_bigfloat(&ratio, prec)
} else {
BigFloat::new(prec)
}
} else {
BigFloat::new(prec)
};
let zero = BigFloat::new(prec);
(0..n)
.map(|k| {
let frac = BigFloat::from_i64(k as i64, prec)
.add(&quarter, prec, rm)
.div(&n_bf, prec, rm);
point(¢er, &radius, &frac, &zero)
})
.collect()
}
pub(crate) fn aberth_roots(poly: &Poly, prec: usize, max_iter: usize) -> Vec<Complex> {
if poly.degree().is_none_or(|d| d == 0) {
return vec![];
}
let rm = RoundingMode::None;
let wp = prec + 64;
let zero_mult = poly.coeffs().iter().take_while(|c| c.is_zero()).count();
let mut roots: Vec<Complex> = (0..zero_mult).map(|_| c_zero(wp)).collect();
let reduced = if zero_mult > 0 {
Poly::from_coeffs(poly.coeffs()[zero_mult..].to_vec())
} else {
poly.clone()
};
let n = match reduced.degree() {
Some(d) if d >= 1 => d,
_ => return roots,
};
let monic = reduced.make_monic();
let deriv = monic.derivative();
let mut cc = match Consts::new() {
Ok(cc) => cc,
Err(e) => {
tracing::warn!(error = ?e, "aberth_roots: astro-float constants init failed");
return roots;
}
};
let mut nonzero = aberth_iterate(&monic, &deriv, n, wp, prec, max_iter, rm, &mut cc);
roots.append(&mut nonzero);
roots.sort_by(|a, b| {
let re_cmp = a.0.cmp(&b.0).unwrap_or(0);
if re_cmp < 0 {
std::cmp::Ordering::Less
} else if re_cmp > 0 {
std::cmp::Ordering::Greater
} else {
let im_cmp = a.1.cmp(&b.1).unwrap_or(0);
if im_cmp < 0 {
std::cmp::Ordering::Less
} else if im_cmp > 0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
});
roots
}
pub(crate) const ABERTH_TOLERANCE: f64 = 1e-30;
#[allow(clippy::too_many_arguments)]
fn aberth_iterate(
monic: &Poly,
deriv: &Poly,
n: usize,
wp: usize,
prec: usize,
max_iter: usize,
rm: RoundingMode,
cc: &mut Consts,
) -> Vec<Complex> {
let mut roots = initial_guesses(monic, n, wp, cc);
let monic_bf: Vec<BigFloat> = monic
.coeffs()
.iter()
.map(|c| ratio_to_bigfloat(c, wp))
.collect();
let deriv_bf: Vec<BigFloat> = deriv
.coeffs()
.iter()
.map(|c| ratio_to_bigfloat(c, wp))
.collect();
let threshold = BigFloat::from_f64(ABERTH_TOLERANCE, wp);
for _iter in 0..max_iter {
let mut max_correction = BigFloat::new(wp);
let mut corrections: Vec<Complex> = Vec::with_capacity(n);
for i in 0..n {
let p_zi = poly_eval_complex_bf(&monic_bf, &roots[i], wp, rm);
let pp_zi = poly_eval_complex_bf(&deriv_bf, &roots[i], wp, rm);
let mut sum_recip = c_zero(wp);
for j in 0..n {
if j != i {
let diff = c_sub(&roots[i], &roots[j], wp, rm);
let diff_abs_sq =
diff.0
.mul(&diff.0, wp, rm)
.add(&diff.1.mul(&diff.1, wp, rm), wp, rm);
if diff_abs_sq.is_zero() {
continue;
}
let recip = c_div(&c_one(wp), &diff, wp, rm);
sum_recip = c_add(&sum_recip, &recip, wp, rm);
}
}
let pz_sum = c_mul(&p_zi, &sum_recip, wp, rm);
let denom = c_sub(&pp_zi, &pz_sum, wp, rm);
let denom_abs_sq =
denom
.0
.mul(&denom.0, wp, rm)
.add(&denom.1.mul(&denom.1, wp, rm), wp, rm);
let correction = if denom_abs_sq.is_zero() {
c_zero(wp)
} else {
c_div(&p_zi, &denom, wp, rm)
};
let corr_abs_sq = correction.0.mul(&correction.0, wp, rm).add(
&correction.1.mul(&correction.1, wp, rm),
wp,
rm,
);
if corr_abs_sq.sub(&max_correction, prec, rm).is_positive() {
max_correction = corr_abs_sq;
}
corrections.push(correction);
}
for i in 0..n {
roots[i] = c_sub(&roots[i], &corrections[i], wp, rm);
}
let threshold_sq = threshold.mul(&threshold, wp, rm);
if max_correction.is_zero() || !max_correction.sub(&threshold_sq, prec, rm).is_positive() {
break;
}
}
roots
}
pub(crate) const ROOTOF_DEFAULT_PREC: usize = 128;
pub(crate) fn rootof_roots(poly: &Poly, prec: usize) -> Vec<Complex> {
aberth_roots(poly, prec + 64, 200)
}
const ROOTOF_TIE_BITS: usize = 60;
pub(crate) fn real_root_index(
roots: &[Complex],
lo: &Ratio<BigInt>,
hi: &Ratio<BigInt>,
) -> Option<usize> {
let wp = ROOTOF_DEFAULT_PREC + 128;
let rm = RoundingMode::None;
let one = BigFloat::from_i32(1, wp);
let tie = BigFloat::from_i32(2, wp)
.powi(ROOTOF_TIE_BITS, wp, rm)
.reciprocal(wp, rm);
let lo_bf = ratio_to_bigfloat(lo, wp);
let hi_bf = ratio_to_bigfloat(hi, wp);
let slack = hi_bf.abs().max(&lo_bf.abs()).max(&one).mul(
&BigFloat::from_i32(2, wp)
.powi(wp - 16, wp, rm)
.reciprocal(wp, rm),
wp,
rm,
);
let lo_bf = lo_bf.sub(&slack, wp, rm);
let hi_bf = hi_bf.add(&slack, wp, rm);
let mut found: Option<usize> = None;
for (k, (re, im)) in roots.iter().enumerate() {
if re.is_nan() || im.is_nan() {
return None;
}
if re.cmp(&lo_bf).unwrap_or(0) < 0 || re.cmp(&hi_bf).unwrap_or(0) > 0 {
continue;
}
let scale = re.abs().max(&one);
let noise = scale.mul(&tie, wp, rm);
if im.abs().cmp(&noise).unwrap_or(1) > 0 {
continue;
}
if found.is_some() {
return None;
}
found = Some(k);
}
let k = found?;
let re_k = &roots[k].0;
let noise = re_k.abs().max(&one).mul(&tie, wp, rm);
let tied = roots
.iter()
.enumerate()
.any(|(j, (re, _))| j != k && re.sub(re_k, wp, rm).abs().cmp(&noise).unwrap_or(-1) <= 0);
if tied {
tracing::debug!(
index = k,
"real_root_index: another root shares the real part; RootOf index not stable"
);
return None;
}
Some(k)
}
#[allow(dead_code)] pub(crate) fn rootof_eval_f64(poly: &Poly, index: usize) -> Option<Complex64> {
let n = poly.degree()?;
if index >= n {
return None;
}
let roots = aberth_roots(poly, 128, 100);
if index >= roots.len() {
return None;
}
let (re, im) = &roots[index];
Some(Complex64::new(bigfloat_to_f64(re), bigfloat_to_f64(im)))
}
const REAL_AXIS_NOISE: f64 = 1e-6;
fn is_real_root_near(
part: &Poly,
z: Complex64,
sturm: &mut Option<super::sturm::SturmChain>,
) -> bool {
let Complex64 { re, im } = z;
if !re.is_finite() || !im.is_finite() {
return false;
}
let scale = re.abs().max(1.0);
if im.abs() > REAL_AXIS_NOISE * scale {
return false;
}
let Some(center) = crate::base::numeric::f64_to_ratio_exact(re) else {
return false;
};
let Some(eps) = crate::base::numeric::f64_to_ratio_exact(scale * 2f64.powi(-30)) else {
return false;
};
let chain = sturm.get_or_insert_with(|| super::sturm::SturmChain::new(part));
let lo = ¢er - &eps;
let hi = ¢er + &eps;
chain.count_roots_in_closed(&lo, &hi) >= 1
}
fn bigfloat_to_f64(bf: &BigFloat) -> f64 {
let s = format!("{}", bf);
s.parse::<f64>().unwrap_or(f64::NAN)
}
pub(crate) fn nroots_f64(poly: &Poly, prec_bits: usize) -> Vec<Complex64> {
let mut out: Vec<Complex64> = Vec::new();
if poly.degree().unwrap_or(0) == 0 {
return out;
}
let (_content, parts) = poly.sqf_list();
for (part, mult) in parts {
if part.degree().unwrap_or(0) == 0 {
continue;
}
let max_iter = 100 + 20 * part.degree().unwrap_or(0);
let roots = aberth_roots(&part, prec_bits, max_iter);
let mut sturm: Option<super::sturm::SturmChain> = None;
for (re, im) in roots {
let mut z = Complex64::new(bigfloat_to_f64(&re), bigfloat_to_f64(&im));
if z.im != 0.0 && is_real_root_near(&part, z, &mut sturm) {
z.im = 0.0;
}
if z.re != 0.0 && z.re.abs() < ABERTH_TOLERANCE * z.norm().max(1.0) {
z.re = 0.0;
}
for _ in 0..mult {
out.push(z);
}
}
}
out.sort_by(|a, b| {
a.re.partial_cmp(&b.re)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.im.partial_cmp(&b.im).unwrap_or(std::cmp::Ordering::Equal))
});
out
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::One;
fn poly_from_coeffs(coeffs: &[i64]) -> Poly {
let rat_coeffs: Vec<Ratio<BigInt>> = coeffs
.iter()
.map(|&c| Ratio::from_integer(BigInt::from(c)))
.collect();
Poly::from_coeffs(rat_coeffs)
}
#[test]
fn aberth_converges_from_newton_polygon_start() {
let cases: Vec<(&str, Poly)> = vec![
("x^2+1", poly_from_coeffs(&[1, 0, 1])),
("x^3-1", poly_from_coeffs(&[-1, 0, 0, 1])),
("x^5-x-1", poly_from_coeffs(&[-1, -1, 0, 0, 0, 1])),
("(x-10)^5+1", {
let mut p = poly_from_coeffs(&[1]);
for _ in 0..5 {
p = &p * &poly_from_coeffs(&[-10, 1]);
}
&p + &poly_from_coeffs(&[1])
}),
("wilkinson10", {
let mut p = poly_from_coeffs(&[1]);
for k in 1..=10 {
p = &p * &poly_from_coeffs(&[-k, 1]);
}
p
}),
("wilkinson20", {
let mut p = poly_from_coeffs(&[1]);
for k in 1..=20 {
p = &p * &poly_from_coeffs(&[-k, 1]);
}
p
}),
("(x^2-2)(x^2-3)(x-1000)(x+1/1000)", {
let p = &poly_from_coeffs(&[-2, 0, 1]) * &poly_from_coeffs(&[-3, 0, 1]);
let p = &p * &poly_from_coeffs(&[-1000, 1]);
&p * &Poly::from_coeffs(vec![
Ratio::new(BigInt::from(1), BigInt::from(1000)),
Ratio::from_integer(BigInt::from(1)),
])
}),
("x^30 + 3x^7 - 2x + 5", {
let mut c = vec![0i64; 31];
c[0] = 5;
c[1] = -2;
c[7] = 3;
c[30] = 1;
poly_from_coeffs(&c)
}),
];
for (name, p) in cases {
let roots = aberth_roots(&p, 192, 60);
assert_eq!(roots.len(), p.degree().unwrap_or(0), "{name}");
for (re, im) in &roots {
let v = poly_eval_complex(
p.coeffs(),
&(re.clone(), im.clone()),
256,
RoundingMode::None,
);
let mag = bigfloat_to_f64(&v.0).abs() + bigfloat_to_f64(&v.1).abs();
assert!(
mag < 1e-20,
"{name}: residual {mag} at {} {}",
bigfloat_to_f64(re),
bigfloat_to_f64(im)
);
}
}
}
#[test]
fn newton_polygon_separates_scales() {
let p = &poly_from_coeffs(&[1, 0, 10_000]) * &poly_from_coeffs(&[10_000, 0, 1]);
let circles = newton_polygon_circles(&p).expect("finite logs");
assert_eq!(circles.len(), 2);
assert_eq!((circles[0].start, circles[0].count), (0, 2));
assert!(
(circles[0].radius - 0.01).abs() < 1e-9,
"{}",
circles[0].radius
);
assert_eq!((circles[1].start, circles[1].count), (2, 2));
assert!(
(circles[1].radius - 100.0).abs() < 1e-6,
"{}",
circles[1].radius
);
let q = poly_from_coeffs(&[-1, -1, 0, 0, 0, 1]);
let circles = newton_polygon_circles(&q).expect("finite logs");
assert_eq!(circles.len(), 1);
assert_eq!(circles[0].count, 5);
assert!((circles[0].radius - 1.0).abs() < 1e-12);
}
#[test]
fn nroots_snaps_noise_but_not_tiny_roots() {
let roots = nroots_f64(&poly_from_coeffs(&[1, 0, 1]), 128);
assert_eq!(roots.len(), 2);
assert!(roots.iter().all(|z| z.re == 0.0), "{roots:?}");
assert!(
roots.iter().all(|z| (z.im.abs() - 1.0).abs() < 1e-15),
"{roots:?}"
);
let tiny = Ratio::new(BigInt::from(-1), BigInt::from(10).pow(40));
let p = Poly::from_coeffs(vec![tiny, Ratio::zero(), Ratio::one()]);
let roots = nroots_f64(&p, 128);
assert_eq!(roots.len(), 2);
assert!(
(roots[0].re + 1e-20).abs() < 1e-33 && roots[0].im == 0.0,
"{roots:?}"
);
assert!(
(roots[1].re - 1e-20).abs() < 1e-33 && roots[1].im == 0.0,
"{roots:?}"
);
}
#[test]
fn aberth_quadratic_real_roots() {
let poly = poly_from_coeffs(&[6, -5, 1]);
let roots = aberth_roots(&poly, 128, 100);
assert_eq!(roots.len(), 2);
let mut real_parts: Vec<f64> = roots.iter().map(|r| bigfloat_to_f64(&r.0)).collect();
real_parts.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!(
(real_parts[0] - 2.0).abs() < 1e-10,
"root 0: {}",
real_parts[0]
);
assert!(
(real_parts[1] - 3.0).abs() < 1e-10,
"root 1: {}",
real_parts[1]
);
}
#[test]
fn aberth_quadratic_complex_roots() {
let poly = poly_from_coeffs(&[1, 0, 1]);
let roots = aberth_roots(&poly, 128, 100);
assert_eq!(roots.len(), 2);
for root in &roots {
let re = bigfloat_to_f64(&root.0);
let im = bigfloat_to_f64(&root.1);
assert!(re.abs() < 1e-10, "real part should be ~0: {re}");
assert!((im.abs() - 1.0).abs() < 1e-10, "|im| should be ~1: {im}");
}
}
#[test]
fn aberth_quintic() {
let poly = poly_from_coeffs(&[-1, -1, 0, 0, 0, 1]);
let roots = aberth_roots(&poly, 128, 200);
assert_eq!(roots.len(), 5);
for (i, root) in roots.iter().enumerate() {
let val = poly_eval_complex(poly.coeffs(), root, 128, RoundingMode::None);
let mag_sq = bigfloat_to_f64(&val.0).powi(2) + bigfloat_to_f64(&val.1).powi(2);
assert!(
mag_sq < 1e-15,
"root {i} residual too large: |p(z)|^2 = {mag_sq}"
);
}
let real_roots: Vec<_> = roots
.iter()
.filter(|r| bigfloat_to_f64(&r.1).abs() < 1e-8)
.collect();
assert_eq!(real_roots.len(), 1, "should have exactly 1 real root");
let real_val = bigfloat_to_f64(&real_roots[0].0);
assert!(
(real_val - 1.1673).abs() < 0.001,
"real root ≈ 1.1673, got {real_val}"
);
}
#[test]
fn rootof_eval_f64_basic() {
let poly = poly_from_coeffs(&[-4, 0, 1]);
let r0 = rootof_eval_f64(&poly, 0).unwrap();
let r1 = rootof_eval_f64(&poly, 1).unwrap();
assert!(r0.im.abs() < 1e-10);
assert!(r1.im.abs() < 1e-10);
let mut reals = [r0.re, r1.re];
reals.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((reals[0] - (-2.0)).abs() < 1e-10);
assert!((reals[1] - 2.0).abs() < 1e-10);
}
#[test]
fn rootof_out_of_range() {
let poly = poly_from_coeffs(&[-1, 0, 1]); assert!(rootof_eval_f64(&poly, 2).is_none());
assert!(rootof_eval_f64(&poly, 100).is_none());
}
#[test]
fn nroots_with_multiplicity() {
let poly = poly_from_coeffs(&[2, -3, 0, 1]);
let roots = nroots_f64(&poly, 128);
assert_eq!(roots.len(), 3);
assert!((roots[0].re + 2.0).abs() < 1e-12, "{roots:?}");
assert!((roots[1].re - 1.0).abs() < 1e-12, "{roots:?}");
assert!((roots[2].re - 1.0).abs() < 1e-12, "{roots:?}");
assert!(roots.iter().all(|r| r.im.abs() < 1e-12));
}
#[test]
fn nroots_wilkinson_like_degree_10() {
let mut poly = poly_from_coeffs(&[1]);
for k in 1..=10 {
poly = &poly * &poly_from_coeffs(&[-k, 1]);
}
let roots = nroots_f64(&poly, 192);
assert_eq!(roots.len(), 10);
for (i, r) in roots.iter().enumerate() {
assert!((r.re - (i as f64 + 1.0)).abs() < 1e-8, "root {i}: {r:?}");
assert!(r.im.abs() < 1e-8);
}
}
}