use num_traits::Float;
use std::fmt::Debug;
use crate::error::{CoreError, CoreResult, ErrorContext};
pub trait MachineConstants: Float + Debug {
fn machine_epsilon() -> Self;
fn min_positive_normal() -> Self;
fn max_finite() -> Self;
fn min_positive_subnormal() -> Self;
fn mantissa_digits() -> u32;
fn radix() -> u32 {
2
}
}
impl MachineConstants for f32 {
fn machine_epsilon() -> Self {
f32::EPSILON
}
fn min_positive_normal() -> Self {
f32::MIN_POSITIVE
}
fn max_finite() -> Self {
f32::MAX
}
fn min_positive_subnormal() -> Self {
1.401_298_4e-45_f32
}
fn mantissa_digits() -> u32 {
f32::MANTISSA_DIGITS
}
}
impl MachineConstants for f64 {
fn machine_epsilon() -> Self {
f64::EPSILON
}
fn min_positive_normal() -> Self {
f64::MIN_POSITIVE
}
fn max_finite() -> Self {
f64::MAX
}
fn min_positive_subnormal() -> Self {
5e-324_f64
}
fn mantissa_digits() -> u32 {
f64::MANTISSA_DIGITS
}
}
pub fn approx_eq<T: Float>(a: T, b: T, tol: T) -> bool {
if a.is_nan() || b.is_nan() {
return false;
}
if a == b {
return true;
}
(a - b).abs() <= tol
}
pub fn approx_eq_relative<T: Float>(a: T, b: T, rel_tol: T, abs_tol: T) -> bool {
if a.is_nan() || b.is_nan() {
return false;
}
if a == b {
return true;
}
let diff = (a - b).abs();
let max_abs = a.abs().max(b.abs());
diff <= rel_tol * max_abs || diff <= abs_tol
}
pub fn relative_error<T: Float + Debug>(computed: T, exact: T) -> CoreResult<T> {
if computed.is_nan() || exact.is_nan() {
return Err(CoreError::ValueError(ErrorContext::new(
"relative_error: NaN input is not allowed",
)));
}
if exact.is_zero() {
return Err(CoreError::DomainError(ErrorContext::new(
"relative_error: exact value is zero, relative error is undefined",
)));
}
Ok((computed - exact).abs() / exact.abs())
}
pub fn relative_error_safe<T: Float + Debug>(computed: T, exact: T, floor: T) -> CoreResult<T> {
if computed.is_nan() || exact.is_nan() || floor.is_nan() {
return Err(CoreError::ValueError(ErrorContext::new(
"relative_error_safe: NaN input is not allowed",
)));
}
let denom = exact.abs().max(floor);
if denom.is_zero() {
return Err(CoreError::DomainError(ErrorContext::new(
"relative_error_safe: denominator is zero even with floor",
)));
}
Ok((computed - exact).abs() / denom)
}
pub fn ulp_distance(a: f64, b: f64) -> CoreResult<u64> {
if a.is_nan() || b.is_nan() {
return Err(CoreError::ValueError(ErrorContext::new(
"ulp_distance: NaN input is not allowed",
)));
}
if a == b {
return Ok(0);
}
let a_bits = a.to_bits() as i64;
let b_bits = b.to_bits() as i64;
let a_mapped = if a_bits < 0 {
i64::MIN - a_bits
} else {
a_bits
};
let b_mapped = if b_bits < 0 {
i64::MIN - b_bits
} else {
b_bits
};
Ok((a_mapped - b_mapped).unsigned_abs())
}
pub fn ulp_distance_f32(a: f32, b: f32) -> CoreResult<u32> {
if a.is_nan() || b.is_nan() {
return Err(CoreError::ValueError(ErrorContext::new(
"ulp_distance_f32: NaN input is not allowed",
)));
}
if a == b {
return Ok(0);
}
let a_bits = a.to_bits() as i32;
let b_bits = b.to_bits() as i32;
let a_mapped = if a_bits < 0 {
i32::MIN - a_bits
} else {
a_bits
};
let b_mapped = if b_bits < 0 {
i32::MIN - b_bits
} else {
b_bits
};
Ok((a_mapped - b_mapped).unsigned_abs())
}
pub fn is_finite_and_positive<T: Float>(x: T) -> bool {
x.is_finite() && x > T::zero()
}
pub fn is_finite_and_non_negative<T: Float>(x: T) -> bool {
x.is_finite() && x >= T::zero()
}
pub fn clamp_to_range<T: Float + Debug>(x: T, min: T, max: T) -> CoreResult<T> {
if x.is_nan() {
return Err(CoreError::ValueError(ErrorContext::new(
"clamp_to_range: input is NaN",
)));
}
if min > max {
return Err(CoreError::DomainError(ErrorContext::new(format!(
"clamp_to_range: min ({min:?}) > max ({max:?})"
))));
}
if x < min {
Ok(min)
} else if x > max {
Ok(max)
} else {
Ok(x)
}
}
pub fn slices_approx_eq<T: Float>(a: &[T], b: &[T], tol: T) -> bool {
if a.len() != b.len() {
return false;
}
a.iter().zip(b.iter()).all(|(&x, &y)| approx_eq(x, y, tol))
}
pub fn max_abs_difference<T: Float>(a: &[T], b: &[T]) -> CoreResult<T> {
if a.len() != b.len() {
return Err(CoreError::DimensionError(ErrorContext::new(format!(
"max_abs_difference: length mismatch ({} vs {})",
a.len(),
b.len()
))));
}
if a.is_empty() {
return Ok(T::zero());
}
let max = a
.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).abs())
.fold(T::zero(), |acc, d| if d > acc { d } else { acc });
Ok(max)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approx_eq_basic() {
assert!(approx_eq(1.0_f64, 1.0 + 1e-11, 1e-10));
assert!(!approx_eq(1.0_f64, 1.0 + 1e-9, 1e-10));
}
#[test]
fn test_approx_eq_nan() {
assert!(!approx_eq(f64::NAN, 1.0, 1e-10));
assert!(!approx_eq(1.0, f64::NAN, 1e-10));
assert!(!approx_eq(f64::NAN, f64::NAN, 1e-10));
}
#[test]
fn test_approx_eq_infinity() {
assert!(approx_eq(f64::INFINITY, f64::INFINITY, 1e-10));
assert!(approx_eq(f64::NEG_INFINITY, f64::NEG_INFINITY, 1e-10));
assert!(!approx_eq(f64::INFINITY, f64::NEG_INFINITY, 1e-10));
}
#[test]
fn test_approx_eq_zero() {
assert!(approx_eq(0.0_f64, 0.0, 0.0));
assert!(approx_eq(0.0_f64, 1e-16, 1e-15));
}
#[test]
fn test_approx_eq_f32() {
assert!(approx_eq(1.0_f32, 1.0 + 1e-6, 1e-5));
assert!(!approx_eq(1.0_f32, 1.0 + 1e-4, 1e-5));
}
#[test]
fn test_approx_eq_relative_basic() {
assert!(approx_eq_relative(100.0_f64, 100.001, 1e-4, 1e-10));
assert!(!approx_eq_relative(100.0_f64, 101.0, 1e-4, 1e-10));
}
#[test]
fn test_relative_error_basic() {
let err = relative_error(1.01_f64, 1.0).expect("should succeed");
assert!(approx_eq(err, 0.01, 1e-14));
}
#[test]
fn test_relative_error_exact() {
let err = relative_error(1.23_f64, 1.23).expect("should succeed");
assert_eq!(err, 0.0);
}
#[test]
fn test_relative_error_zero_exact() {
let result = relative_error(1.0_f64, 0.0);
assert!(result.is_err());
}
#[test]
fn test_relative_error_nan() {
assert!(relative_error(f64::NAN, 1.0).is_err());
assert!(relative_error(1.0, f64::NAN).is_err());
}
#[test]
fn test_relative_error_safe_near_zero() {
let err = relative_error_safe(0.001_f64, 0.0, 1.0).expect("should succeed");
assert!(approx_eq(err, 0.001, 1e-14));
}
#[test]
fn test_ulp_distance_same() {
assert_eq!(ulp_distance(1.0, 1.0).expect("should succeed"), 0);
}
#[test]
fn test_ulp_distance_adjacent() {
let a = 1.0_f64;
let b = f64::from_bits(a.to_bits() + 1);
assert_eq!(ulp_distance(a, b).expect("should succeed"), 1);
}
#[test]
fn test_ulp_distance_symmetric() {
let a = 1.0_f64;
let b = 1.0 + f64::EPSILON;
let d1 = ulp_distance(a, b).expect("should succeed");
let d2 = ulp_distance(b, a).expect("should succeed");
assert_eq!(d1, d2);
}
#[test]
fn test_ulp_distance_nan() {
assert!(ulp_distance(f64::NAN, 1.0).is_err());
}
#[test]
fn test_ulp_distance_across_zero() {
let d = ulp_distance(-0.0_f64, 0.0).expect("should succeed");
assert_eq!(d, 0);
}
#[test]
fn test_ulp_distance_f32_basic() {
let a = 1.0_f32;
let b = f32::from_bits(a.to_bits() + 1);
assert_eq!(ulp_distance_f32(a, b).expect("should succeed"), 1);
}
#[test]
fn test_ulp_distance_f32_nan() {
assert!(ulp_distance_f32(f32::NAN, 1.0).is_err());
}
#[test]
fn test_is_finite_and_positive_basic() {
assert!(is_finite_and_positive(1.0_f64));
assert!(is_finite_and_positive(f64::MIN_POSITIVE));
assert!(!is_finite_and_positive(0.0_f64));
assert!(!is_finite_and_positive(-1.0_f64));
assert!(!is_finite_and_positive(f64::INFINITY));
assert!(!is_finite_and_positive(f64::NAN));
}
#[test]
fn test_is_finite_and_positive_f32() {
assert!(is_finite_and_positive(0.001_f32));
assert!(!is_finite_and_positive(f32::NEG_INFINITY));
}
#[test]
fn test_is_finite_and_non_negative() {
assert!(is_finite_and_non_negative(0.0_f64));
assert!(is_finite_and_non_negative(1.0_f64));
assert!(!is_finite_and_non_negative(-0.001_f64));
assert!(!is_finite_and_non_negative(f64::NAN));
}
#[test]
fn test_clamp_to_range_normal() {
assert_eq!(clamp_to_range(5.0_f64, 0.0, 10.0).expect("ok"), 5.0);
}
#[test]
fn test_clamp_to_range_below() {
assert_eq!(clamp_to_range(-5.0_f64, 0.0, 10.0).expect("ok"), 0.0);
}
#[test]
fn test_clamp_to_range_above() {
assert_eq!(clamp_to_range(15.0_f64, 0.0, 10.0).expect("ok"), 10.0);
}
#[test]
fn test_clamp_to_range_nan() {
let result = clamp_to_range(f64::NAN, 0.0, 10.0);
assert!(result.is_err());
}
#[test]
fn test_clamp_to_range_inverted() {
let result = clamp_to_range(5.0_f64, 10.0, 0.0);
assert!(result.is_err());
}
#[test]
fn test_clamp_to_range_exact_boundary() {
assert_eq!(clamp_to_range(0.0_f64, 0.0, 10.0).expect("ok"), 0.0);
assert_eq!(clamp_to_range(10.0_f64, 0.0, 10.0).expect("ok"), 10.0);
}
#[test]
fn test_clamp_to_range_f32() {
assert_eq!(clamp_to_range(100.0_f32, -1.0, 1.0).expect("ok"), 1.0);
}
#[test]
fn test_machine_constants_f64() {
assert_eq!(f64::machine_epsilon(), f64::EPSILON);
assert_eq!(f64::min_positive_normal(), f64::MIN_POSITIVE);
assert_eq!(f64::max_finite(), f64::MAX);
assert_eq!(f64::mantissa_digits(), 53);
assert_eq!(f64::radix(), 2);
assert!(f64::min_positive_subnormal() > 0.0);
assert!(f64::min_positive_subnormal() < f64::MIN_POSITIVE);
}
#[test]
fn test_machine_constants_f32() {
assert_eq!(f32::machine_epsilon(), f32::EPSILON);
assert_eq!(f32::min_positive_normal(), f32::MIN_POSITIVE);
assert_eq!(f32::max_finite(), f32::MAX);
assert_eq!(f32::mantissa_digits(), 24);
assert!(f32::min_positive_subnormal() > 0.0);
assert!(f32::min_positive_subnormal() < f32::MIN_POSITIVE);
}
#[test]
fn test_slices_approx_eq_basic() {
let a = [1.0_f64, 2.0, 3.0];
let b = [1.0 + 1e-12, 2.0 - 1e-12, 3.0];
assert!(slices_approx_eq(&a, &b, 1e-10));
}
#[test]
fn test_slices_approx_eq_different_lengths() {
let a = [1.0_f64, 2.0];
let b = [1.0_f64];
assert!(!slices_approx_eq(&a, &b, 1e-10));
}
#[test]
fn test_slices_approx_eq_not_equal() {
let a = [1.0_f64, 2.0];
let b = [1.0_f64, 3.0];
assert!(!slices_approx_eq(&a, &b, 0.5));
}
#[test]
fn test_max_abs_difference_basic() {
let a = [1.0_f64, 2.0, 3.0];
let b = [1.1, 2.0, 2.5];
let d = max_abs_difference(&a, &b).expect("ok");
assert!(approx_eq(d, 0.5, 1e-14));
}
#[test]
fn test_max_abs_difference_empty() {
let a: [f64; 0] = [];
let b: [f64; 0] = [];
assert_eq!(max_abs_difference(&a, &b).expect("ok"), 0.0);
}
#[test]
fn test_max_abs_difference_length_mismatch() {
let a = [1.0_f64];
let b = [1.0_f64, 2.0];
assert!(max_abs_difference(&a, &b).is_err());
}
}