#[must_use]
pub fn ulp_distance(a: f32, b: f32) -> u32 {
if a.to_bits() == b.to_bits() {
return 0;
}
let ai = to_ordered(a);
let bi = to_ordered(b);
ai.abs_diff(bi) as u32
}
#[must_use]
pub fn within_tolerance(cpu: f32, gpu: f32, budget: u32, abs_floor: f32) -> bool {
ulp_distance(cpu, gpu) <= budget || (cpu - gpu).abs() < abs_floor
}
fn to_ordered(x: f32) -> i64 {
let bits = x.to_bits() as i32;
let ordered = if bits < 0 {
i32::MIN.wrapping_sub(bits)
} else {
bits
};
i64::from(ordered)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_values_are_zero_ulp_apart() {
assert_eq!(ulp_distance(1.0, 1.0), 0);
assert_eq!(ulp_distance(0.0, 0.0), 0);
assert_eq!(ulp_distance(0.0, -0.0), 0);
}
#[test]
fn adjacent_representable_floats_are_one_ulp_apart() {
let a = 1.0f32;
let b = f32::from_bits(a.to_bits() + 1);
assert_eq!(ulp_distance(a, b), 1);
assert_eq!(ulp_distance(b, a), 1);
}
#[test]
fn crossing_zero_counts_the_full_step_distance() {
let a = f32::from_bits(1); let b = -a;
assert_eq!(ulp_distance(a, b), 2);
}
#[test]
fn positive_zero_and_negative_zero_are_zero_ulp_apart() {
assert_eq!(ulp_distance(0.0f32, -0.0f32), 0);
assert_eq!(ulp_distance(-0.0f32, 0.0f32), 0);
}
#[test]
fn same_sign_positive_pairs_are_unaffected_by_the_fix() {
let a = 100.0f32;
let b = f32::from_bits(a.to_bits() + 5);
assert_eq!(ulp_distance(a, b), 5);
assert_eq!(ulp_distance(b, a), 5);
}
#[test]
fn same_sign_negative_pairs_are_unaffected_by_the_fix() {
let a = -100.0f32;
let b = f32::from_bits(a.to_bits() + 5); assert_eq!(ulp_distance(a, b), 5);
assert_eq!(ulp_distance(b, a), 5);
}
#[test]
fn opposite_sign_pairs_away_from_zero_count_the_full_path_through_zero() {
let steps_zero_to_one = ulp_distance(0.0f32, 1.0f32);
let steps_neg_one_to_zero = ulp_distance(-1.0f32, -0.0f32);
let combined = steps_zero_to_one + steps_neg_one_to_zero;
assert_eq!(ulp_distance(-1.0f32, 1.0f32), combined);
assert_eq!(ulp_distance(1.0f32, -1.0f32), combined);
assert_eq!(combined, 2 * 1.0f32.to_bits());
}
#[test]
fn denormals_straddling_zero_step_correctly() {
let a = f32::from_bits(3); let b = f32::from_bits(0x8000_0002); assert_eq!(ulp_distance(a, b), 5);
assert_eq!(ulp_distance(b, a), 5);
}
#[test]
fn denormals_same_sign_step_correctly() {
let a = f32::from_bits(2);
let b = f32::from_bits(9);
assert_eq!(ulp_distance(a, b), 7);
let na = f32::from_bits(0x8000_0002);
let nb = f32::from_bits(0x8000_0009);
assert_eq!(ulp_distance(na, nb), 7);
}
#[test]
fn values_near_f32_max_step_correctly_same_sign() {
let max = f32::MAX;
let one_below = f32::from_bits(max.to_bits() - 1);
assert_eq!(ulp_distance(max, one_below), 1);
let neg_max = -f32::MAX;
let one_above = f32::from_bits(neg_max.to_bits() - 1); assert_eq!(ulp_distance(neg_max, one_above), 1);
}
#[test]
fn values_near_f32_max_step_correctly_opposite_sign() {
let d = ulp_distance(f32::MAX, -f32::MAX);
let expected = 2u32 * f32::MAX.to_bits();
assert_eq!(d, expected);
assert_eq!(ulp_distance(-f32::MAX, f32::MAX), d);
}
}