use crate::error::EmlError;
use crate::lower::LoweredOp;
use crate::numeric::lambert_w0;
use crate::poly::{Poly, PolyError};
use crate::numeric::lambert_wm1;
use num_complex::Complex;
use num_rational::Ratio;
#[derive(Debug, Clone)]
pub struct RootsResult {
pub roots: Vec<LoweredOp>,
}
#[derive(Debug, Clone)]
pub struct ComplexRoots {
pub roots: Vec<Complex<f64>>,
}
impl ComplexRoots {
pub fn real_roots(&self, tol: f64) -> Vec<f64> {
self.roots
.iter()
.filter(|c| c.im.abs() < tol)
.map(|c| c.re)
.collect()
}
}
fn ratio_to_f64(r: &Ratio<i64>) -> f64 {
*r.numer() as f64 / *r.denom() as f64
}
pub(crate) fn solve_polynomial(poly: &Poly, _var: usize) -> Result<RootsResult, EmlError> {
let deg = match poly.degree() {
Some(d) => d,
None => return Ok(RootsResult { roots: vec![] }),
};
match deg {
0 => Ok(RootsResult { roots: vec![] }),
1 => {
let a0 = ratio_to_f64(&poly.coeffs[0]);
let a1 = ratio_to_f64(&poly.coeffs[1]);
if a1.abs() < 1e-14 {
return Ok(RootsResult { roots: vec![] });
}
let root = -a0 / a1;
Ok(RootsResult {
roots: vec![LoweredOp::Const(root)],
})
}
2 => {
let a0 = ratio_to_f64(&poly.coeffs[0]);
let a1 = ratio_to_f64(&poly.coeffs[1]);
let a2 = ratio_to_f64(&poly.coeffs[2]);
if a2.abs() < 1e-14 {
if a1.abs() < 1e-14 {
return Ok(RootsResult { roots: vec![] });
}
return Ok(RootsResult {
roots: vec![LoweredOp::Const(-a0 / a1)],
});
}
let disc = a1 * a1 - 4.0 * a2 * a0;
if disc < -1e-12 {
Ok(RootsResult { roots: vec![] })
} else if disc.abs() <= 1e-12 {
let root = -a1 / (2.0 * a2);
Ok(RootsResult {
roots: vec![LoweredOp::Const(root)],
})
} else {
let sq = disc.sqrt();
let r1 = (-a1 - sq) / (2.0 * a2);
let r2 = (-a1 + sq) / (2.0 * a2);
let (r1, r2) = if r1 <= r2 { (r1, r2) } else { (r2, r1) };
Ok(RootsResult {
roots: vec![LoweredOp::Const(r1), LoweredOp::Const(r2)],
})
}
}
3 => {
let a0 = ratio_to_f64(&poly.coeffs[0]);
let a1 = ratio_to_f64(&poly.coeffs[1]);
let a2 = ratio_to_f64(&poly.coeffs[2]);
let a3 = ratio_to_f64(&poly.coeffs[3]);
if a3.abs() < 1e-14 {
let deg2 = Poly {
coeffs: poly.coeffs[..3].to_vec(),
};
return solve_polynomial(°2, _var);
}
let shift = a2 / (3.0 * a3);
let p_coef = (3.0 * a3 * a1 - a2 * a2) / (3.0 * a3 * a3);
let q_coef = (2.0 * a2 * a2 * a2 - 9.0 * a3 * a2 * a1 + 27.0 * a3 * a3 * a0)
/ (27.0 * a3 * a3 * a3);
let disc = -4.0 * p_coef * p_coef * p_coef - 27.0 * q_coef * q_coef;
let mut roots: Vec<f64> = if disc > 1e-12 {
let m = 2.0 * (-p_coef / 3.0).sqrt();
let theta_arg =
(3.0 * q_coef / (2.0 * p_coef) * (-3.0 / p_coef).sqrt()).clamp(-1.0, 1.0);
let theta = theta_arg.acos();
let pi = std::f64::consts::PI;
vec![
m * (theta / 3.0).cos() - shift,
m * ((theta - 2.0 * pi) / 3.0).cos() - shift,
m * ((theta - 4.0 * pi) / 3.0).cos() - shift,
]
} else {
poly.isolate_real_roots(-1e6, 1e6, 1e-10)
.unwrap_or_default()
};
roots.retain(|r| r.is_finite() && poly.eval_f64(*r).abs() < 1e-6);
roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
roots.dedup_by(|a, b| (*a - *b).abs() < 1e-8);
if roots.is_empty() {
roots = poly
.isolate_real_roots(-1e6, 1e6, 1e-10)
.unwrap_or_default();
}
Ok(RootsResult {
roots: roots.into_iter().map(LoweredOp::Const).collect(),
})
}
_ => {
let numeric_roots = poly
.isolate_real_roots(-1e6, 1e6, 1e-10)
.map_err(|_: PolyError| EmlError::NotSolvable)?;
Ok(RootsResult {
roots: numeric_roots.into_iter().map(LoweredOp::Const).collect(),
})
}
}
}
pub(crate) fn try_lambert_w_solve(f: &LoweredOp, var: usize) -> Option<RootsResult> {
const NEG_INV_E: f64 = -0.367_879_441_171_442_32;
let make_lambert_roots = |k: f64| -> Option<RootsResult> {
let root0 = lambert_w0(k).ok().filter(|r| r.is_finite())?;
let mut roots = vec![LoweredOp::Const(root0)];
if (NEG_INV_E - 1e-12..0.0).contains(&k) {
if let Ok(wm1) = lambert_wm1(k) {
if wm1.is_finite() && (wm1 - root0).abs() > 1e-10 {
roots.push(LoweredOp::Const(wm1));
roots.sort_by(|a, b| {
if let (LoweredOp::Const(x), LoweredOp::Const(y)) = (a, b) {
x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
} else {
std::cmp::Ordering::Equal
}
});
}
}
}
Some(RootsResult { roots })
};
match f {
LoweredOp::Sub(lhs, rhs) => {
if let LoweredOp::Const(k) = rhs.as_ref() {
if is_x_exp_x(lhs, var) {
if let Some(result) = make_lambert_roots(*k) {
return Some(result);
}
}
}
if let LoweredOp::Const(k) = lhs.as_ref() {
if is_x_exp_x(rhs, var) {
if let Some(result) = make_lambert_roots(*k) {
return Some(result);
}
}
}
None
}
LoweredOp::Add(lhs, rhs) => {
if let LoweredOp::Const(neg_k) = lhs.as_ref() {
if is_x_exp_x(rhs, var) {
if let Some(result) = make_lambert_roots(-neg_k) {
return Some(result);
}
}
}
if let LoweredOp::Const(neg_k) = rhs.as_ref() {
if is_x_exp_x(lhs, var) {
if let Some(result) = make_lambert_roots(-neg_k) {
return Some(result);
}
}
}
None
}
_ => None,
}
}
fn is_x_exp_x(op: &LoweredOp, var: usize) -> bool {
if let LoweredOp::Mul(a, b) = op {
let a_is_var = matches!(a.as_ref(), LoweredOp::Var(i) if *i == var);
let b_is_exp_var = is_exp_var(b, var);
if a_is_var && b_is_exp_var {
return true;
}
let a_is_exp_var = is_exp_var(a, var);
let b_is_var = matches!(b.as_ref(), LoweredOp::Var(i) if *i == var);
if a_is_exp_var && b_is_var {
return true;
}
}
false
}
fn is_exp_var(op: &LoweredOp, var: usize) -> bool {
if let LoweredOp::Exp(inner) = op {
return matches!(inner.as_ref(), LoweredOp::Var(i) if *i == var);
}
false
}
fn durand_kerner(poly: &Poly, max_iter: usize, tol: f64) -> Result<Vec<Complex<f64>>, EmlError> {
let n = match poly.degree() {
Some(d) => d,
None => return Ok(vec![]),
};
if n == 0 {
return Ok(vec![]);
}
let lead = ratio_to_f64(&poly.leading_coeff());
if lead.abs() < 1e-300 {
return Err(EmlError::NotSolvable);
}
let norm: Vec<f64> = poly.coeffs.iter().map(|c| ratio_to_f64(c) / lead).collect();
let r = {
let max_coeff = norm[..n].iter().map(|c| c.abs()).fold(0.0_f64, f64::max);
(1.0 + max_coeff).max(1.0)
};
let pi = std::f64::consts::PI;
let mut roots: Vec<Complex<f64>> = (0..n)
.map(|k| {
let theta = 2.0 * pi * k as f64 / n as f64 + 0.1;
Complex::from_polar(r, theta)
})
.collect();
let eval_at = |z: Complex<f64>| -> Complex<f64> {
let mut acc = Complex::new(1.0, 0.0);
for i in (0..n).rev() {
acc = acc * z + Complex::new(norm[i], 0.0);
}
acc
};
for _iter in 0..max_iter {
let mut max_change = 0.0_f64;
let prev = roots.clone();
for i in 0..n {
let pz = eval_at(prev[i]);
let denom: Complex<f64> = (0..n)
.filter(|&j| j != i)
.map(|j| prev[i] - prev[j])
.fold(Complex::new(1.0, 0.0), |acc, x| acc * x);
if denom.norm() < 1e-300 {
continue;
}
let delta = pz / denom;
roots[i] = prev[i] - delta;
max_change = max_change.max(delta.norm());
}
if !max_change.is_finite() {
return Err(EmlError::NonConvergence {
method: "durand_kerner",
iterations: max_iter,
});
}
if max_change < tol {
break;
}
}
for root in &mut roots {
for _ in 0..10 {
let pz = eval_at(*root);
if pz.norm() < tol * tol {
break;
}
if n >= 2 {
let mut dacc = Complex::new(n as f64, 0.0);
for i in (0..n - 1).rev() {
dacc = dacc * *root + Complex::new((i + 1) as f64 * norm[i + 1], 0.0);
}
if dacc.norm() < 1e-300 {
break;
}
*root -= pz / dacc;
} else {
break;
}
}
}
roots.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))
});
Ok(roots)
}
fn square_free_dk(poly: &Poly) -> Result<ComplexRoots, EmlError> {
let prim = poly.primitive_part().map_err(|_| EmlError::NotSolvable)?;
let mut roots = durand_kerner(&prim, 300, 1e-12)?;
roots.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))
});
Ok(ComplexRoots { roots })
}
pub fn solve_polynomial_complex(poly: &Poly) -> Result<ComplexRoots, EmlError> {
let n = match poly.degree() {
Some(d) => d,
None => return Ok(ComplexRoots { roots: vec![] }),
};
match n {
0 => Ok(ComplexRoots { roots: vec![] }),
1 => {
let a0 = ratio_to_f64(&poly.coeffs[0]);
let a1 = ratio_to_f64(&poly.coeffs[1]);
if a1.abs() < 1e-14 {
return Ok(ComplexRoots { roots: vec![] });
}
Ok(ComplexRoots {
roots: vec![Complex::new(-a0 / a1, 0.0)],
})
}
2 => {
let a0 = ratio_to_f64(&poly.coeffs[0]);
let a1 = ratio_to_f64(&poly.coeffs[1]);
let a2 = ratio_to_f64(&poly.coeffs[2]);
if a2.abs() < 1e-14 {
let p1 = Poly {
coeffs: poly.coeffs[..2].to_vec(),
};
return solve_polynomial_complex(&p1);
}
let disc = a1 * a1 - 4.0 * a2 * a0;
let mut roots = if disc >= 0.0 {
let sq = disc.sqrt();
vec![
Complex::new((-a1 - sq) / (2.0 * a2), 0.0),
Complex::new((-a1 + sq) / (2.0 * a2), 0.0),
]
} else {
let sq = (-disc).sqrt();
vec![
Complex::new(-a1 / (2.0 * a2), -sq / (2.0 * a2)),
Complex::new(-a1 / (2.0 * a2), sq / (2.0 * a2)),
]
};
roots.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))
});
Ok(ComplexRoots { roots })
}
_ => square_free_dk(poly),
}
}
#[cfg(test)]
mod complex_root_tests {
use super::*;
use num_rational::Ratio;
fn make_poly(int_coeffs: &[i64]) -> Poly {
Poly {
coeffs: int_coeffs.iter().map(|&n| Ratio::new(n, 1)).collect(),
}
}
#[test]
fn test_complex_roots_quadratic_positive_disc() {
let p = make_poly(&[6, -5, 1]);
let roots = solve_polynomial_complex(&p).unwrap();
assert_eq!(roots.roots.len(), 2);
let reals: Vec<f64> = roots.roots.iter().map(|c| c.re).collect();
assert!((reals[0] - 2.0).abs() < 1e-9 || (reals[1] - 2.0).abs() < 1e-9);
assert!((reals[0] - 3.0).abs() < 1e-9 || (reals[1] - 3.0).abs() < 1e-9);
}
#[test]
fn test_complex_roots_quadratic_negative_disc() {
let p = make_poly(&[1, 0, 1]);
let roots = solve_polynomial_complex(&p).unwrap();
assert_eq!(roots.roots.len(), 2);
for r in &roots.roots {
assert!(r.re.abs() < 1e-9, "real part should be 0: {}", r.re);
assert!(
(r.im.abs() - 1.0).abs() < 1e-9,
"imag should be ±1: {}",
r.im
);
}
assert!(roots.real_roots(1e-9).is_empty());
}
#[test]
fn test_real_api_back_compat_x2_plus_1() {
let p = make_poly(&[1, 0, 1]);
let result = solve_polynomial(&p, 0).unwrap();
assert!(
result.roots.is_empty(),
"x²+1 must have no real roots in the real API"
);
}
#[test]
fn test_complex_roots_x4_minus_1() {
let p = make_poly(&[-1, 0, 0, 0, 1]);
let roots = solve_polynomial_complex(&p).unwrap();
assert_eq!(roots.roots.len(), 4);
for r in &roots.roots {
assert!(
(r.norm() - 1.0).abs() < 1e-6,
"root not on unit circle: {:?}",
r
);
}
for r in &roots.roots {
let val = r.powi(4) - Complex::new(1.0, 0.0);
assert!(val.norm() < 1e-9, "root doesn't satisfy p: {:?}", r);
}
}
#[test]
fn test_complex_roots_x5_minus_1() {
let p = make_poly(&[-1, 0, 0, 0, 0, 1]);
let roots = solve_polynomial_complex(&p).unwrap();
assert_eq!(roots.roots.len(), 5);
for r in &roots.roots {
let val = r.powi(5) - Complex::new(1.0, 0.0);
assert!(
val.norm() < 1e-8,
"5th root residual too large: {:?} → {:?}",
r,
val
);
}
}
#[test]
fn test_complex_roots_x5_minus_x_minus_1() {
let p = make_poly(&[-1, -1, 0, 0, 0, 1]);
let roots = solve_polynomial_complex(&p).unwrap();
assert_eq!(roots.roots.len(), 5);
let real_roots = roots.real_roots(1e-6);
assert_eq!(real_roots.len(), 1);
assert!((real_roots[0] - 1.1673039).abs() < 1e-4);
let x = real_roots[0];
let residual = x.powi(5) - x - 1.0;
assert!(residual.abs() < 1e-8);
}
#[test]
fn test_lambert_wm1_two_branches() {
use crate::numeric::lambert_wm1;
let x = lambert_wm1(-0.2_f64).unwrap();
assert!(x < -1.0, "W₋₁ should be < -1: {}", x);
assert!(
(x * x.exp() - (-0.2)).abs() < 1e-10,
"W₋₁(-0.2) must satisfy w·eʷ=-0.2"
);
}
}