use half::f16;
pub trait UlpFloat: Copy {
const MAX_ULP: u64;
const SIGN_MASK: u64;
fn ulp_bits(self) -> u64;
fn ulp_is_nan(self) -> bool;
#[inline]
fn ulp_magnitude_bits(self) -> u64 {
self.ulp_bits() & !Self::SIGN_MASK
}
#[inline]
fn ulp_is_sign_negative(self) -> bool {
self.ulp_bits() & Self::SIGN_MASK != 0
}
}
macro_rules! impl_ulp_float {
($t:ty, $bits:ty) => {
impl UlpFloat for $t {
const MAX_ULP: u64 = <$bits>::MAX as u64;
const SIGN_MASK: u64 = 1 << (<$bits>::BITS - 1);
#[inline]
fn ulp_bits(self) -> u64 {
self.to_bits() as u64
}
#[inline]
fn ulp_is_nan(self) -> bool {
<$t>::is_nan(self)
}
}
};
}
impl_ulp_float!(f16, u16);
impl_ulp_float!(f32, u32);
impl_ulp_float!(f64, u64);
pub fn ulp_distance<T: UlpFloat>(a: T, b: T) -> u64 {
let (a_nan, b_nan) = (a.ulp_is_nan(), b.ulp_is_nan());
if a_nan && b_nan {
return 0;
}
if a_nan || b_nan {
return T::MAX_ULP;
}
if a.ulp_is_sign_negative() != b.ulp_is_sign_negative() {
return a
.ulp_magnitude_bits()
.saturating_add(b.ulp_magnitude_bits())
.saturating_add(1)
.min(T::MAX_ULP);
}
a.ulp_bits().abs_diff(b.ulp_bits())
}
pub fn max_ulp_distance<T: UlpFloat>(
a: impl IntoIterator<Item = T>,
b: impl IntoIterator<Item = T>,
) -> (u64, Option<usize>) {
let mut worst = 0;
let mut at = None;
for (ix, (x, y)) in a.into_iter().zip(b).enumerate() {
let d = ulp_distance(x, y);
if d > worst || at.is_none() {
worst = d;
at = Some(ix);
}
}
(worst, at)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adjacent_floats_are_one_ulp_apart() {
assert_eq!(ulp_distance(1.0f32, f32::from_bits(1.0f32.to_bits() + 1)), 1);
assert_eq!(ulp_distance(1.0f64, f64::from_bits(1.0f64.to_bits() + 1)), 1);
assert_eq!(
ulp_distance(f16::from_f32(1.0), f16::from_bits(f16::from_f32(1.0).to_bits() + 1)),
1
);
}
#[test]
fn identical_values_are_zero_ulp_apart() {
assert_eq!(ulp_distance(0.0f32, 0.0f32), 0);
assert_eq!(ulp_distance(-3.25f32, -3.25f32), 0);
assert_eq!(ulp_distance(f32::INFINITY, f32::INFINITY), 0);
}
#[test]
fn distance_is_symmetric_and_sign_aware() {
assert_eq!(ulp_distance(1.0f32, -1.0f32), ulp_distance(-1.0f32, 1.0f32));
assert_eq!(ulp_distance(0.0f32, -0.0f32), 1);
let tiny = f32::from_bits(1);
assert_eq!(ulp_distance(tiny, -tiny), 3);
}
#[test]
fn nan_handling() {
assert_eq!(ulp_distance(f32::NAN, f32::NAN), 0);
assert_eq!(ulp_distance(f32::NAN, 1.0f32), f32::MAX_ULP);
assert_eq!(ulp_distance(1.0f32, f32::NAN), f32::MAX_ULP);
}
#[test]
fn infinity_is_adjacent_to_max_finite() {
assert_eq!(ulp_distance(f32::MAX, f32::INFINITY), 1);
assert_eq!(ulp_distance(-f32::MAX, f32::NEG_INFINITY), 1);
}
#[test]
fn magnitude_independence() {
for scale in [1e-30f32, 1.0, 1e30] {
assert_eq!(ulp_distance(scale, f32::from_bits(scale.to_bits() + 1)), 1);
}
}
#[test]
fn max_over_slice_reports_position() {
let a = [1.0f32, 2.0, 3.0];
let b = [1.0f32, 2.0, f32::from_bits(3.0f32.to_bits() + 5)];
assert_eq!(max_ulp_distance(a, b), (5, Some(2)));
assert_eq!(max_ulp_distance::<f32>([], []), (0, None));
}
}