use thiserror::Error;
use crate::numerics::utils::is_near_zero;
#[derive(Debug, Error, PartialEq)]
pub enum HalleyError {
#[error("did not converge in {max_iter} iterations (last residual {last_residual})")]
NoConvergence { max_iter: usize, last_residual: f64 },
#[error("Halley denominator is near zero at x = {x}; iteration cannot proceed")]
SingularStep { x: f64 },
#[error("non-finite evaluation at x = {x}")]
NonFiniteEvaluation { x: f64 },
}
pub fn halley<F>(
mut f_and_derivs: F,
x0: f64,
xtol: f64,
max_iter: usize,
) -> Result<f64, HalleyError>
where
F: FnMut(f64) -> (f64, f64, f64),
{
let mut x = x0;
for iter in 0..max_iter {
let (f, df, ddf) = f_and_derivs(x);
if !(f.is_finite() && df.is_finite() && ddf.is_finite()) {
return Err(HalleyError::NonFiniteEvaluation { x });
}
if f == 0.0 {
return Ok(x);
}
let denom = 2.0 * df * df - f * ddf;
if is_near_zero(denom) {
return Err(HalleyError::SingularStep { x });
}
let delta = 2.0 * f * df / denom;
let x_new = x - delta;
if delta.abs() <= xtol + x_new.abs() * f64::EPSILON {
return Ok(x_new);
}
x = x_new;
if iter + 1 == max_iter {
return Err(HalleyError::NoConvergence {
max_iter,
last_residual: f,
});
}
}
unreachable!("loop exits via convergence return or NoConvergence")
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"expected {expected}, got {actual}"
);
}
#[test]
fn finds_sqrt_via_halley() {
let r = halley(|x: f64| (x * x - 2.0, 2.0 * x, 2.0), 1.0, 1e-15, 50).unwrap();
assert_close(r, std::f64::consts::SQRT_2, 1e-15);
}
#[test]
fn finds_dottie_number() {
let r = halley(
|x: f64| (x.cos() - x, -x.sin() - 1.0, -x.cos()),
0.5,
1e-15,
50,
)
.unwrap();
assert_close(r, 0.739_085_133_215_160_7, 1e-15);
}
#[test]
fn cubic_convergence_beats_newton_iteration_count() {
let mut iters = 0;
let target = 612.0_f64.sqrt();
let _r = halley(
|x: f64| {
iters += 1;
(x * x - 612.0, 2.0 * x, 2.0)
},
10.0,
1e-15,
50,
)
.unwrap();
assert!(
iters <= 6,
"Halley took {iters} iterations on √612 (expected ≤ 6); approx target {target}"
);
}
#[test]
fn detects_singular_step() {
let err = halley(|_x: f64| (2.0, 1.0, 1.0), 0.5, 1e-12, 50).unwrap_err();
assert!(matches!(err, HalleyError::SingularStep { .. }));
}
#[test]
fn reports_non_finite_evaluation() {
let err = halley(|_x: f64| (f64::NAN, 1.0, 1.0), 0.0, 1e-12, 50).unwrap_err();
assert!(matches!(err, HalleyError::NonFiniteEvaluation { .. }));
}
}