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![];
}
if let Some(roots) = ABERTH_MEMO.with(|m| m.borrow_mut().get(poly, prec, max_iter)) {
return roots;
}
let roots = aberth_roots_uncached(poly, prec, max_iter);
ABERTH_MEMO.with(|m| m.borrow_mut().put(poly, prec, max_iter, &roots));
roots
}
const ABERTH_MEMO_CAPACITY: usize = 8;
struct AberthMemo {
entries: Vec<(Poly, usize, usize, Vec<Complex>)>,
}
impl AberthMemo {
fn get(&mut self, poly: &Poly, prec: usize, max_iter: usize) -> Option<Vec<Complex>> {
let pos = self
.entries
.iter()
.position(|(p, pr, it, _)| *pr == prec && *it == max_iter && p == poly)?;
let entry = self.entries.remove(pos);
let roots = entry.3.clone();
self.entries.insert(0, entry);
Some(roots)
}
fn put(&mut self, poly: &Poly, prec: usize, max_iter: usize, roots: &[Complex]) {
self.entries.truncate(ABERTH_MEMO_CAPACITY - 1);
self.entries
.insert(0, (poly.clone(), prec, max_iter, roots.to_vec()));
}
}
thread_local! {
static ABERTH_MEMO: std::cell::RefCell<AberthMemo> =
const { std::cell::RefCell::new(AberthMemo { entries: Vec::new() }) };
}
fn aberth_roots_uncached(poly: &Poly, prec: usize, max_iter: usize) -> Vec<Complex> {
aberth_roots_to(poly, prec, max_iter, None)
}
fn aberth_roots_to(
poly: &Poly,
prec: usize,
max_iter: usize,
tolerance_exp: Option<i64>,
) -> Vec<Complex> {
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 threshold = match tolerance_exp {
None => BigFloat::from_f64(ABERTH_TOLERANCE, wp),
Some(e) => cauchy_bound(&monic, wp).mul(&pow2(e, wp), wp, rm),
};
let mut nonzero = aberth_iterate(
&monic, &deriv, n, wp, prec, max_iter, &threshold, 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,
threshold: &BigFloat,
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();
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
}
#[derive(Clone, Debug)]
pub(crate) struct RootBall {
pub(crate) value: Complex,
pub(crate) radius: Option<BigFloat>,
pub(crate) real: bool,
}
const BALL_PREC: usize = 64;
fn inflate(x: &BigFloat) -> BigFloat {
let rm = RoundingMode::ToEven;
let tiny = pow2(-40, BALL_PREC);
x.add(&x.abs().mul(&tiny, BALL_PREC, rm), BALL_PREC, rm)
}
fn deflate(x: &BigFloat) -> BigFloat {
let rm = RoundingMode::ToEven;
let tiny = pow2(-40, BALL_PREC);
x.sub(&x.abs().mul(&tiny, BALL_PREC, rm), BALL_PREC, rm)
}
fn pow2(e: i64, p: usize) -> BigFloat {
let mut x = BigFloat::from_i32(1, p);
let e = e.saturating_add(1).clamp(
i64::from(astro_float::EXPONENT_MIN),
i64::from(astro_float::EXPONENT_MAX),
);
x.set_exponent(astro_float::Exponent::try_from(e).unwrap_or(0));
x
}
fn modulus(z: &Complex) -> BigFloat {
crate::base::bigcomplex::c_abs(z, BALL_PREC, RoundingMode::ToEven)
}
fn horner_with_scale(
coeffs: &[BigFloat],
abs_coeffs: &[BigFloat],
z: &Complex,
wp: usize,
) -> (Complex, BigFloat) {
let rm = RoundingMode::ToEven;
let value = poly_eval_complex_bf(coeffs, z, wp, rm);
let r = modulus(z);
let mut scale = BigFloat::new(BALL_PREC);
for c in abs_coeffs.iter().rev() {
scale = scale.mul(&r, BALL_PREC, rm).add(c, BALL_PREC, rm);
}
(value, inflate(&scale))
}
pub(crate) fn root_balls(poly: &Poly, roots: &[Complex], prec: usize) -> Vec<RootBall> {
let uncertified = || {
roots
.iter()
.map(|z| RootBall {
value: z.clone(),
radius: None,
real: false,
})
.collect::<Vec<_>>()
};
let n = match poly.degree() {
Some(d) if d >= 1 && d == roots.len() => d,
_ => return uncertified(),
};
if roots.iter().any(|z| !is_finite(z)) {
return uncertified();
}
let rm = RoundingMode::ToEven;
let wp = prec + 64;
let monic = poly.make_monic();
let deriv = monic.derivative();
let to_bf = |p: &Poly, bits: usize| -> Vec<BigFloat> {
p.coeffs()
.iter()
.map(|c| ratio_to_bigfloat(c, bits))
.collect()
};
let p_bf = to_bf(&monic, wp);
let d_bf = to_bf(&deriv, wp);
let p_abs: Vec<BigFloat> = to_bf(&monic, BALL_PREC).iter().map(BigFloat::abs).collect();
let d_abs: Vec<BigFloat> = to_bf(&deriv, BALL_PREC).iter().map(BigFloat::abs).collect();
let n_bits = i64::from(usize::BITS - (8 * n + 8).leading_zeros());
let unit = pow2(
n_bits + 1 - i64::try_from(wp).unwrap_or(i64::MAX / 4),
BALL_PREC,
);
let n_bf = BigFloat::from_u64(n as u64, BALL_PREC);
let mut polished: Vec<Complex> = Vec::with_capacity(n);
let mut radii: Vec<BigFloat> = Vec::with_capacity(n);
for z0 in roots {
let z = newton_polish(&p_bf, &d_bf, z0, wp);
let (pz, p_scale) = horner_with_scale(&p_bf, &p_abs, &z, wp);
let (dz, d_scale) = horner_with_scale(&d_bf, &d_abs, &z, wp);
let p_err = p_scale.mul(&unit, BALL_PREC, rm);
let d_err = d_scale.mul(&unit, BALL_PREC, rm);
let numer = inflate(&modulus(&pz).add(&p_err, BALL_PREC, rm));
let denom = deflate(&deflate(&modulus(&dz)).sub(&p_err.max(&d_err), BALL_PREC, rm));
if !denom.is_positive() {
return uncertified();
}
let r = inflate(&n_bf.mul(&numer, BALL_PREC, rm).div(&denom, BALL_PREC, rm));
if r.is_inf() || r.is_nan() {
return uncertified();
}
polished.push(z);
radii.push(r);
}
let beyond = |a: &Complex, b: &Complex, reach: &BigFloat| -> bool {
let d = c_sub(a, b, wp, rm);
let re = d.0.mul(&d.0, BALL_PREC, rm);
let im = d.1.mul(&d.1, BALL_PREC, rm);
let d2 = deflate(&deflate(&re.add(&im, BALL_PREC, rm)));
let reach = inflate(reach);
d2.cmp(&inflate(&reach.mul(&reach, BALL_PREC, rm)))
.unwrap_or(-1)
> 0
};
for k in 0..n {
for j in (k + 1)..n {
let reach = radii[j].add(&radii[k], BALL_PREC, rm);
if !beyond(&polished[j], &polished[k], &reach) {
return uncertified();
}
}
}
let mut out = Vec::with_capacity(n);
for k in 0..n {
let z = &polished[k];
let centre = (z.0.clone(), BigFloat::new(wp));
let im = modulus(&(z.1.clone(), BigFloat::new(wp)));
let meets_axis = deflate(&im).cmp(&radii[k]).unwrap_or(-1) <= 0;
let mirror = z.1.abs().add(&radii[k], BALL_PREC, rm);
let real = meets_axis
&& (0..n).filter(|&j| j != k).all(|j| {
let reach = mirror.add(&radii[j], BALL_PREC, rm);
beyond(&polished[j], ¢re, &reach)
});
out.push(RootBall {
value: if real { centre } else { z.clone() },
radius: Some(radii[k].clone()),
real,
});
}
out
}
fn is_finite(z: &Complex) -> bool {
!(z.0.is_nan() || z.0.is_inf() || z.1.is_nan() || z.1.is_inf())
}
const CLUSTER_REFINEMENTS: u32 = 3;
#[derive(Clone, Debug, PartialEq, Eq)]
enum ReKey {
Rational(Ratio<BigInt>),
Pair(usize, usize),
}
#[derive(Clone, Debug)]
struct DistinctRoot {
ball: RootBall,
multiplicity: usize,
key: Option<ReKey>,
}
pub(crate) fn certified_roots(poly: &Poly, prec: usize) -> Vec<RootBall> {
if let Some(hit) = CERTIFIED_MEMO.with(|m| m.borrow_mut().get(poly, prec)) {
return hit;
}
let out = certified_roots_uncached(poly, prec);
CERTIFIED_MEMO.with(|m| m.borrow_mut().put(poly, prec, &out));
out
}
struct CertifiedMemo {
entries: Vec<(Poly, usize, Vec<RootBall>)>,
}
impl CertifiedMemo {
fn get(&mut self, poly: &Poly, prec: usize) -> Option<Vec<RootBall>> {
let pos = self
.entries
.iter()
.position(|(p, pr, _)| *pr == prec && p == poly)?;
let entry = self.entries.remove(pos);
let out = entry.2.clone();
self.entries.insert(0, entry);
Some(out)
}
fn put(&mut self, poly: &Poly, prec: usize, balls: &[RootBall]) {
self.entries.truncate(ABERTH_MEMO_CAPACITY - 1);
self.entries.insert(0, (poly.clone(), prec, balls.to_vec()));
}
}
thread_local! {
static CERTIFIED_MEMO: std::cell::RefCell<CertifiedMemo> =
const { std::cell::RefCell::new(CertifiedMemo { entries: Vec::new() }) };
}
fn certified_roots_uncached(poly: &Poly, prec: usize) -> Vec<RootBall> {
let (_, factors) = poly.sqf_list();
let mut distinct: Vec<DistinctRoot> = Vec::new();
for (index, (factor, multiplicity)) in factors.iter().enumerate() {
let multiplicity = usize::try_from(*multiplicity).unwrap_or(usize::MAX);
for (ball, key) in factor_balls(factor, index, prec) {
distinct.push(DistinctRoot {
ball,
multiplicity,
key,
});
}
}
distinct.sort_by(compare_roots);
mark_ambiguous_order(&mut distinct, prec);
let mut out = Vec::with_capacity(poly.degree().unwrap_or(0));
for d in distinct {
for _ in 0..d.multiplicity {
out.push(d.ball.clone());
}
}
out
}
fn compare_roots(a: &DistinctRoot, b: &DistinctRoot) -> std::cmp::Ordering {
let key = |x: &BigFloat, y: &BigFloat| x.cmp(y).unwrap_or(0).cmp(&0);
let (za, zb) = (&a.ball.value, &b.ball.value);
let re = match (&a.key, &b.key) {
(Some(ReKey::Rational(p)), Some(ReKey::Rational(q))) => p.cmp(q),
_ => key(&za.0, &zb.0),
};
re.then_with(|| key(&za.1, &zb.1))
}
fn factor_balls(factor: &Poly, index: usize, prec: usize) -> Vec<(RootBall, Option<ReKey>)> {
let wp = prec + 64;
if factor.degree() == Some(1) {
let root = -(factor.coeff(0) / factor.coeff(1));
let value = ratio_to_bigfloat(&root, wp);
let d = root.denom();
let dyadic = (d & (d - BigInt::from(1))).is_zero();
let exact = root.is_zero() || (dyadic && root.numer().bits() <= wp as u64);
let radius = if exact {
BigFloat::new(BALL_PREC)
} else {
let m = value.exponent().map_or(0, i64::from);
pow2(m + 1 - i64::try_from(wp).unwrap_or(i64::MAX / 4), BALL_PREC)
};
let ball = RootBall {
value: (value, BigFloat::new(wp)),
radius: Some(radius),
real: true,
};
return vec![(ball, Some(ReKey::Rational(root)))];
}
let mut balls = root_balls(factor, &rootof_roots(factor, prec), prec);
let mut p = prec;
for _ in 0..CLUSTER_REFINEMENTS {
if balls.iter().all(|b| b.radius.is_some()) {
break;
}
p = p.saturating_mul(2);
let tolerance = -i64::try_from(p).unwrap_or(i64::MAX / 4) + 32;
let roots = aberth_roots_to(factor, p + 64, 400, Some(tolerance));
balls = root_balls(factor, &roots, p);
}
let mut keys: Vec<Option<ReKey>> = vec![None; balls.len()];
if balls.iter().all(|b| b.radius.is_some()) {
for (pair, (k, j)) in symmetrise_pairs(&mut balls).into_iter().enumerate() {
keys[k] = Some(ReKey::Pair(index, pair));
keys[j] = Some(ReKey::Pair(index, pair));
}
for (k, c) in on_symmetry_line(factor, &mut balls, wp) {
keys[k] = Some(ReKey::Rational(c));
}
}
balls.into_iter().zip(keys).collect()
}
fn on_symmetry_line(
factor: &Poly,
balls: &mut [RootBall],
wp: usize,
) -> Vec<(usize, Ratio<BigInt>)> {
let rm = RoundingMode::ToEven;
let Some(n) = factor.degree().filter(|&n| n >= 2) else {
return Vec::new();
};
let c = -(factor.coeff(n - 1) / (factor.coeff(n) * Ratio::from_integer(BigInt::from(n))));
let shifted = factor.taylor_shift(&c);
let symmetric = shifted
.coeffs()
.iter()
.enumerate()
.all(|(i, a)| (n - i) % 2 == 0 || a.is_zero());
if !symmetric {
return Vec::new();
}
let c_bf = ratio_to_bigfloat(&c, wp);
let d = c.denom();
let c_exact =
c.is_zero() || ((d & (d - BigInt::from(1))).is_zero() && c.numer().bits() <= wp as u64);
let c_rounding = if c_exact {
BigFloat::new(BALL_PREC)
} else {
let m = c_bf.exponent().map_or(0, i64::from);
pow2(m + 1 - i64::try_from(wp).unwrap_or(i64::MAX / 4), BALL_PREC)
};
let mut on_line = Vec::new();
for k in 0..balls.len() {
let Some(rk) = balls[k].radius.clone() else {
continue;
};
let z = balls[k].value.clone();
let meets =
deflate(&z.0.sub(&c_bf, wp, rm).abs()) <= inflate(&rk.add(&c_rounding, BALL_PREC, rm));
if !meets {
continue;
}
let two_c = c_bf.add(&c_bf, wp, rm);
let reflected = (two_c.sub(&z.0, wp, rm), z.1.clone());
let alone = (0..balls.len()).filter(|&j| j != k).all(|j| {
balls[j].radius.as_ref().is_none_or(|rj| {
let reach = rk.add(rj, BALL_PREC, rm).add(&c_rounding, BALL_PREC, rm);
!within_reach(&reflected, &balls[j].value, &reach, wp)
})
});
if alone {
balls[k].value.0 = c_bf.clone();
balls[k].radius = Some(inflate(&rk.add(&c_rounding, BALL_PREC, rm)));
on_line.push((k, c.clone()));
}
}
on_line
}
fn within_reach(a: &Complex, b: &Complex, reach: &BigFloat, wp: usize) -> bool {
let rm = RoundingMode::ToEven;
let d = c_sub(a, b, wp, rm);
let d2 = deflate(&deflate(&d.0.mul(&d.0, BALL_PREC, rm).add(
&d.1.mul(&d.1, BALL_PREC, rm),
BALL_PREC,
rm,
)));
let reach = inflate(reach);
d2.cmp(&inflate(&reach.mul(&reach, BALL_PREC, rm)))
.unwrap_or(1)
<= 0
}
fn symmetrise_pairs(balls: &mut [RootBall]) -> Vec<(usize, usize)> {
let rm = RoundingMode::ToEven;
let n = balls.len();
let mut done = vec![false; n];
let mut pairs = Vec::new();
for k in 0..n {
if done[k] || balls[k].real {
continue;
}
let Some(rk) = balls[k].radius.clone() else {
continue;
};
let wp = balls[k].value.0.mantissa_max_bit_len().unwrap_or(64) + 64;
let mirror = (balls[k].value.0.clone(), balls[k].value.1.neg());
let hits: Vec<usize> = (0..n)
.filter(|&j| {
j != k
&& balls[j].radius.as_ref().is_some_and(|rj| {
within_reach(&mirror, &balls[j].value, &rk.add(rj, BALL_PREC, rm), wp)
})
})
.collect();
let [j] = hits[..] else {
continue;
};
let Some(rj) = balls[j].radius.clone() else {
continue;
};
let misses_axis = |b: &RootBall, r: &BigFloat| deflate(&b.value.1.abs()) > inflate(r);
if !misses_axis(&balls[k], &rk) || !misses_axis(&balls[j], &rj) {
continue;
}
let re =
balls[k]
.value
.0
.add(&balls[j].value.0, wp, rm)
.div(&BigFloat::from_i32(2, wp), wp, rm);
let im =
balls[k]
.value
.1
.sub(&balls[j].value.1, wp, rm)
.div(&BigFloat::from_i32(2, wp), wp, rm);
let radius = inflate(&inflate(&rk.max(&rj)));
balls[k].value = (re.clone(), im.clone());
balls[j].value = (re, im.neg());
balls[k].radius = Some(radius.clone());
balls[j].radius = Some(radius);
done[k] = true;
done[j] = true;
pairs.push((k, j));
}
pairs
}
fn mark_ambiguous_order(roots: &mut [DistinctRoot], prec: usize) {
let rm = RoundingMode::ToEven;
let wp = prec + 64;
let n = roots.len();
let mut ambiguous = vec![false; n];
for k in 0..n {
let Some(rk) = roots[k].ball.radius.clone() else {
ambiguous[k] = true;
continue;
};
for j in (k + 1)..n {
let Some(rj) = roots[j].ball.radius.clone() else {
continue;
};
let (a, c) = (&roots[k].ball.value, &roots[j].ball.value);
let gap = deflate(&c.0.sub(&a.0, wp, rm).abs());
if gap > inflate(&rk.add(&rj, BALL_PREC, rm)) {
continue;
}
let equal_re = roots[k].key.is_some() && roots[k].key == roots[j].key;
let separated_im =
deflate(&c.1.sub(&a.1, wp, rm).abs()) > inflate(&rk.add(&rj, BALL_PREC, rm));
if !(equal_re && separated_im) {
ambiguous[k] = true;
ambiguous[j] = true;
}
}
}
for (root, bad) in roots.iter_mut().zip(ambiguous) {
if bad {
root.ball.radius = None;
root.ball.real = false;
}
}
}
fn newton_polish(p: &[BigFloat], d: &[BigFloat], z0: &Complex, wp: usize) -> Complex {
let rm = RoundingMode::ToEven;
let mag = |z: &Complex| -> Option<i64> {
let part = |x: &BigFloat| {
(!x.is_zero())
.then(|| x.exponent().map(i64::from))
.flatten()
};
match (part(&z.0), part(&z.1)) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, b) => a.or(b),
}
};
let mut z = z0.clone();
let mut previous: Option<i64> = None;
for _ in 0..8 {
let pz = poly_eval_complex_bf(p, &z, wp, rm);
let dz = poly_eval_complex_bf(d, &z, wp, rm);
if mag(&dz).is_none() {
break;
}
let w = c_div(&pz, &dz, wp, rm);
let Some(wm) = mag(&w) else { break };
if previous.is_some_and(|pm| wm >= pm) || !is_finite(&w) {
break;
}
z = c_sub(&z, &w, wp, rm);
previous = Some(wm);
if mag(&z).is_some_and(|zm| wm < zm - i64::try_from(wp).unwrap_or(i64::MAX / 4) + 2) {
break;
}
}
z
}
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);
}
}
#[test]
fn root_balls_certify_radii_and_realness() {
let p = poly_from_coeffs(&[1, -1, 0, 0, 0, 1]);
let prec = 166;
let roots = rootof_roots(&p, prec);
let balls = root_balls(&p, &roots, prec);
assert_eq!(balls.len(), 5);
let reals: Vec<bool> = balls.iter().map(|b| b.value.1.is_zero()).collect();
assert_eq!(reals, [true, false, false, false, false], "{balls:?}");
for b in &balls {
let r = b.radius.as_ref().expect("certified radius");
assert!(
r.is_zero() || r.exponent().is_some_and(|e| i64::from(e) < -150),
"radius {r} for {:?}",
b.value
);
}
}
}