pub const DEFAULT_NEAR_ZERO_EPS: f64 = 1e-12;
#[inline]
pub fn is_near_zero(x: f64) -> bool {
is_near_zero_eps(x, DEFAULT_NEAR_ZERO_EPS)
}
#[inline]
pub fn is_near_zero_eps(x: f64, eps: f64) -> bool {
x.is_finite() && x.abs() <= eps
}
#[inline]
pub fn sum_frac_residual(xs: &[f64]) -> f64 {
1.0 - xs.iter().sum::<f64>()
}
#[inline]
pub fn norm_l1(xs: &[f64]) -> f64 {
xs.iter().map(|x| x.abs()).sum()
}
#[inline]
pub fn norm_l2(xs: &[f64]) -> f64 {
xs.iter().map(|x| x * x).sum::<f64>().sqrt()
}
#[inline]
pub fn norm_linf(xs: &[f64]) -> f64 {
xs.iter().map(|x| x.abs()).fold(0.0_f64, f64::max)
}
#[inline]
pub fn converged_scalar(new: f64, old: f64, atol: f64, rtol: f64) -> bool {
(new - old).abs() <= atol + rtol * new.abs()
}
pub fn parabolic_vertex(p0: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> Option<f64> {
let (x0, y0) = p0;
let (x1, y1) = p1;
let (x2, y2) = p2;
let d01 = (y0 - y1) * (x1 - x2);
let d21 = (y2 - y1) * (x1 - x0);
let denom = d01 - d21;
if is_near_zero(denom) {
return None;
}
let num = (x1 - x2) * d01 - (x1 - x0) * d21;
Some(x1 - 0.5 * num / denom)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn near_zero_basic_and_nan() {
assert!(is_near_zero(0.0));
assert!(is_near_zero(1e-15));
assert!(!is_near_zero(1e-9));
assert!(!is_near_zero(f64::NAN));
assert!(!is_near_zero(f64::INFINITY));
}
#[test]
fn sum_frac_residual_works() {
assert!((sum_frac_residual(&[0.4, 0.6]) - 0.0).abs() < 1e-15);
assert!((sum_frac_residual(&[0.3, 0.3, 0.3]) - 0.1).abs() < 1e-15);
assert_eq!(sum_frac_residual(&[]), 1.0);
}
#[test]
fn norms_basic() {
let v = [3.0, -4.0];
assert!((norm_l1(&v) - 7.0).abs() < 1e-15);
assert!((norm_l2(&v) - 5.0).abs() < 1e-15); assert!((norm_linf(&v) - 4.0).abs() < 1e-15);
assert_eq!(norm_l1(&[]), 0.0);
assert_eq!(norm_l2(&[]), 0.0);
assert_eq!(norm_linf(&[]), 0.0);
}
#[test]
fn converged_scalar_handles_both_regimes() {
assert!(converged_scalar(1e-10, 0.0, 1e-9, 1e-6));
assert!(!converged_scalar(1e-8, 0.0, 1e-9, 1e-6));
assert!(converged_scalar(1.0 + 1e-7, 1.0, 1e-12, 1e-6));
assert!(!converged_scalar(1.0 + 1e-5, 1.0, 1e-12, 1e-6));
}
#[test]
fn parabolic_vertex_finds_known_minimum() {
let f = |x: f64| (x - 2.0).powi(2) + 1.0;
let v = parabolic_vertex((0.0, f(0.0)), (1.5, f(1.5)), (5.0, f(5.0)))
.expect("non-degenerate parabola");
assert!((v - 2.0).abs() < 1e-12, "vertex = {v}, expected 2.0");
}
#[test]
fn parabolic_vertex_degenerate_returns_none() {
assert!(parabolic_vertex((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)).is_none());
}
}