#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct RealRoot {
pub(crate) value: f64,
pub(crate) multiplicity: u32,
}
pub(crate) fn eval(coeffs: &[f64], x: f64) -> f64 {
let mut acc = 0.0;
for &c in coeffs.iter().rev() {
acc = acc * x + c;
}
acc
}
pub(crate) fn deriv(coeffs: &[f64]) -> Vec<f64> {
coeffs
.iter()
.enumerate()
.skip(1)
.map(|(i, &c)| i as f64 * c)
.collect()
}
pub(crate) fn real_roots(coeffs: &[f64], tol: f64) -> Vec<RealRoot> {
let scale = coeffs.iter().fold(0.0f64, |m, &c| m.max(c.abs()));
if scale == 0.0 {
return Vec::new();
}
let mut n = coeffs.len();
while n > 1 && coeffs[n - 1].abs() <= tol * scale {
n -= 1;
}
let c = &coeffs[..n];
let mut raw: Vec<f64> = match n - 1 {
0 => Vec::new(),
1 => vec![-c[0] / c[1]],
2 => quadratic_raw(c[2], c[1], c[0], tol),
3 => cubic_raw(c[3], c[2], c[1], c[0], tol),
4 => quartic_raw(c[4], c[3], c[2], c[1], c[0], tol),
_ => Vec::new(),
};
if raw.is_empty() {
return Vec::new();
}
let lead = c[n - 1];
let monic: Vec<f64> = c.iter().map(|&v| v / lead).collect();
let d1 = deriv(&monic);
let d2 = deriv(&d1);
for x in &mut raw {
for _ in 0..2 {
let slope = eval(&d1, *x);
if slope.abs() <= 1e-300 * (1.0 + eval(&monic, *x).abs()) {
break;
}
*x -= eval(&monic, *x) / slope;
}
}
cluster(raw, &monic, &d1, &d2, scale, tol)
}
fn quadratic_raw(a: f64, b: f64, c: f64, tol: f64) -> Vec<f64> {
let disc = b * b - 4.0 * a * c;
let mid = -b / (2.0 * a);
let sep = disc.abs().sqrt() / (2.0 * a.abs());
if sep <= tol * (1.0 + mid.abs()) {
vec![mid]
} else if disc < 0.0 {
Vec::new()
} else {
let q = -0.5 * (b + b.signum() * disc.sqrt());
if q == 0.0 {
vec![0.0]
} else {
vec![q / a, c / q]
}
}
}
fn cubic_raw(a: f64, b: f64, c: f64, d: f64, _tol: f64) -> Vec<f64> {
let (p, q) = (
(3.0 * a * c - b * b) / (3.0 * a * a),
(2.0 * b * b * b - 9.0 * a * b * c + 27.0 * a * a * d) / (27.0 * a * a * a),
);
let shift = -b / (3.0 * a);
let disc = (q / 2.0) * (q / 2.0) + (p / 3.0) * (p / 3.0) * (p / 3.0);
if disc > 0.0 {
let root_disc = disc.sqrt();
vec![(-q / 2.0 + root_disc).cbrt() + (-q / 2.0 - root_disc).cbrt() + shift]
} else {
let s = -p / 3.0;
if s <= 0.0 {
return vec![shift, shift, shift];
}
let m = 2.0 * s.sqrt();
let theta = (-q / 2.0 / (s * s * s).sqrt()).clamp(-1.0, 1.0).acos();
(0..3)
.map(|k| m * ((theta + 2.0 * k as f64 * std::f64::consts::PI) / 3.0).cos() + shift)
.collect()
}
}
fn quartic_raw(a: f64, b: f64, c: f64, d: f64, e: f64, tol: f64) -> Vec<f64> {
let (b, c, d, e) = (b / a, c / a, d / a, e / a);
let shift = -b / 4.0;
let p = c - 3.0 * b * b / 8.0;
let q = d - b * c / 2.0 + b * b * b / 8.0;
let r = e - b * d / 4.0 + b * b * c / 16.0 - 3.0 * b * b * b * b / 256.0;
if q.abs() <= tol * (1.0 + p.abs() + r.abs()) {
let mut out = Vec::new();
for y in quadratic_raw(1.0, p, r, tol) {
if y > tol * (1.0 + y.abs()) {
let s = y.sqrt();
out.push(s + shift);
out.push(-s + shift);
} else if y >= -tol * (1.0 + y.abs()) {
let times = if p.abs() <= tol * (1.0 + p.abs() + r.abs()) {
4
} else {
2
};
out.extend(std::iter::repeat_n(shift, times));
}
}
return out;
}
let mut m_roots = cubic_raw(1.0, 2.0 * p, p * p - 4.0 * r, -q * q, tol);
if m_roots.is_empty() {
return Vec::new();
}
m_roots.sort_by(|x, y| x.partial_cmp(y).unwrap());
let u = m_roots[m_roots.len() - 1].max(0.0).sqrt();
if u <= 0.0 {
return Vec::new();
}
let v = ((p + u * u) - q / u) / 2.0;
let w = ((p + u * u) + q / u) / 2.0;
let mut out = quadratic_raw(1.0, u, v, tol);
out.extend(quadratic_raw(1.0, -u, w, tol));
out.iter().map(|&t| t + shift).collect()
}
fn cluster(
mut raw: Vec<f64>,
monic: &[f64],
d1: &[f64],
d2: &[f64],
scale: f64,
tol: f64,
) -> Vec<RealRoot> {
raw.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut roots: Vec<RealRoot> = Vec::new();
for x in raw {
let gap = tol * (1.0 + x.abs());
let merge = match roots.last() {
Some(last) => (x - last.value).abs() <= gap.max(tol * (1.0 + last.value.abs())),
None => false,
};
if merge {
let last = roots.last_mut().expect("merge implies a root exists");
last.value =
(last.value * last.multiplicity as f64 + x) / (last.multiplicity as f64 + 1.0);
last.multiplicity += 1;
continue;
}
roots.push(RealRoot {
value: x,
multiplicity: 1,
});
}
let degree = monic.len().saturating_sub(1) as u32;
let total: u32 = roots.iter().map(|r| r.multiplicity).sum();
for root in &mut roots {
if root.multiplicity == 1 && total < degree {
let slope_scale = scale * (1.0 + root.value.abs()).powi(degree as i32);
if eval(d1, root.value).abs() <= tol * slope_scale {
root.multiplicity = 2;
if eval(d2, root.value).abs() <= tol * slope_scale && total + 1 < degree {
root.multiplicity = 3;
}
}
}
}
roots
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear() {
let roots = real_roots(&[2.0, 4.0], 1e-12);
assert_eq!(roots.len(), 1);
assert!((roots[0].value + 0.5).abs() < 1e-12);
}
#[test]
fn test_quadratic_two_simple() {
let roots = real_roots(&[6.0, -5.0, 1.0], 1e-12);
assert_eq!(roots.len(), 2);
assert!((roots[0].value - 2.0).abs() < 1e-9);
assert!((roots[1].value - 3.0).abs() < 1e-9);
}
#[test]
fn test_quadratic_double_root() {
let roots = real_roots(&[4.0, -4.0, 1.0], 1e-12);
assert_eq!(roots.len(), 1);
assert!((roots[0].value - 2.0).abs() < 1e-9);
assert_eq!(roots[0].multiplicity, 2);
}
#[test]
fn test_quadratic_no_real_roots() {
let roots = real_roots(&[1.0, 0.0, 1.0], 1e-12);
assert!(roots.is_empty());
}
#[test]
fn test_cubic_three_real() {
let roots = real_roots(&[-6.0, 11.0, -6.0, 1.0], 1e-9);
assert_eq!(roots.len(), 3);
for (r, e) in roots.iter().zip([1.0, 2.0, 3.0]) {
assert!((r.value - e).abs() < 1e-7, "{r:?}");
}
}
#[test]
fn test_cubic_one_real() {
let roots = real_roots(&[1.0, 1.0, 0.0, 1.0], 1e-12);
assert_eq!(roots.len(), 1);
assert!((roots[0].value + 0.682_327_803_828_019_3).abs() < 1e-9);
}
#[test]
fn test_cubic_triple_root() {
let roots = real_roots(&[1.0, 3.0, 3.0, 1.0], 1e-9);
assert_eq!(roots.len(), 1);
assert!((roots[0].value + 1.0).abs() < 1e-6);
assert!(roots[0].multiplicity >= 2, "{roots:?}");
}
#[test]
fn test_cubic_double_plus_simple() {
let roots = real_roots(&[2.0, -3.0, 0.0, 1.0], 1e-9);
assert_eq!(roots.len(), 2);
let dbl = roots.iter().find(|r| (r.value - 1.0).abs() < 1e-6).unwrap();
assert_eq!(dbl.multiplicity, 2);
}
#[test]
fn test_quartic_four_real() {
let roots = real_roots(&[24.0, -50.0, 35.0, -10.0, 1.0], 1e-9);
assert_eq!(roots.len(), 4);
for (r, e) in roots.iter().zip([1.0, 2.0, 3.0, 4.0]) {
assert!((r.value - e).abs() < 1e-6, "{r:?}");
}
}
#[test]
fn test_quartic_no_real_roots() {
let roots = real_roots(&[1.0, 0.0, 2.0, 0.0, 1.0], 1e-12);
assert!(roots.is_empty());
}
#[test]
fn test_quartic_double_double() {
let roots = real_roots(&[1.0, 0.0, -2.0, 0.0, 1.0], 1e-9);
assert_eq!(roots.len(), 2);
assert!(roots.iter().all(|r| r.multiplicity == 2));
}
#[test]
fn test_quartic_quadruple_root() {
let roots = real_roots(&[16.0, 32.0, 24.0, 8.0, 1.0], 1e-9);
assert_eq!(roots.len(), 1);
assert!((roots[0].value + 2.0).abs() < 1e-6);
assert!(roots[0].multiplicity >= 3, "{roots:?}");
}
#[test]
fn test_quartic_two_real_two_complex() {
let roots = real_roots(&[5.0, -6.0, 6.0, -6.0, 1.0], 1e-9);
assert_eq!(roots.len(), 2);
assert!((roots[0].value - 1.0).abs() < 1e-6);
assert!((roots[1].value - 5.0).abs() < 1e-6);
}
#[test]
fn test_leading_zeros_drop_degree() {
let roots = real_roots(&[1.0, 2.0, 1e-15], 1e-12);
assert_eq!(roots.len(), 1);
assert!((roots[0].value + 0.5).abs() < 1e-9);
}
#[test]
fn test_roots_satisfy_equation() {
let cases: Vec<Vec<f64>> = vec![
vec![6.0, -5.0, 1.0],
vec![-6.0, 11.0, -6.0, 1.0],
vec![24.0, -50.0, 35.0, -10.0, 1.0],
vec![5.0, -6.0, 6.0, -6.0, 1.0],
vec![-2.0, 0.0, 3.0, 0.0, 1.0],
];
for coeffs in &cases {
for r in real_roots(coeffs, 1e-9) {
assert!(eval(coeffs, r.value).abs() < 1e-6, "{coeffs:?} {r:?}");
}
}
}
}