Skip to main content

tract_data/
ulp.rs

1//! Integer ULP (unit in the last place) distance between floating point values.
2//!
3//! Absolute and relative tolerances answer "is the result roughly right?". ULP
4//! distance answers a sharper question: "how many representable floats apart are
5//! these two results?". That is the useful metric when two implementations of the
6//! same kernel are supposed to be equivalent, because it stays meaningful across
7//! the whole dynamic range and cleanly separates a one-off rounding difference
8//! from a genuinely different computation.
9//!
10//! The distance follows the usual total-ordering convention: adjacent floats are
11//! 1 apart, `+0.0` and `-0.0` are 1 apart (they are distinct representations),
12//! two NaNs are 0 apart, and a NaN against anything else is [`UlpFloat::MAX_ULP`].
13
14use half::f16;
15
16/// Floating point types for which an integer ULP distance is defined.
17pub trait UlpFloat: Copy {
18    /// Largest distance representable for this type. Also the distance reported
19    /// between a NaN and a non-NaN value.
20    const MAX_ULP: u64;
21
22    /// Bit pattern of the sign bit, widened to `u64`.
23    const SIGN_MASK: u64;
24
25    /// Raw bit pattern, widened to `u64`.
26    ///
27    /// Within a single sign, the bit patterns of finite floats are monotonic in
28    /// magnitude, which is what makes the subtraction below meaningful.
29    fn ulp_bits(self) -> u64;
30
31    fn ulp_is_nan(self) -> bool;
32
33    /// Bit pattern with the sign bit cleared, i.e. the bits of `|self|`.
34    #[inline]
35    fn ulp_magnitude_bits(self) -> u64 {
36        self.ulp_bits() & !Self::SIGN_MASK
37    }
38
39    #[inline]
40    fn ulp_is_sign_negative(self) -> bool {
41        self.ulp_bits() & Self::SIGN_MASK != 0
42    }
43}
44
45macro_rules! impl_ulp_float {
46    ($t:ty, $bits:ty) => {
47        impl UlpFloat for $t {
48            const MAX_ULP: u64 = <$bits>::MAX as u64;
49            const SIGN_MASK: u64 = 1 << (<$bits>::BITS - 1);
50
51            #[inline]
52            fn ulp_bits(self) -> u64 {
53                self.to_bits() as u64
54            }
55
56            #[inline]
57            fn ulp_is_nan(self) -> bool {
58                <$t>::is_nan(self)
59            }
60        }
61    };
62}
63
64impl_ulp_float!(f16, u16);
65impl_ulp_float!(f32, u32);
66impl_ulp_float!(f64, u64);
67
68/// Integer ULP distance between two floats of the same type.
69pub fn ulp_distance<T: UlpFloat>(a: T, b: T) -> u64 {
70    let (a_nan, b_nan) = (a.ulp_is_nan(), b.ulp_is_nan());
71    if a_nan && b_nan {
72        return 0;
73    }
74    if a_nan || b_nan {
75        return T::MAX_ULP;
76    }
77    if a.ulp_is_sign_negative() != b.ulp_is_sign_negative() {
78        // The two values sit on opposite sides of zero, so their bit patterns are
79        // not comparable directly. Measure each against its own zero, then add one
80        // to bridge the gap between -0.0 and +0.0.
81        return a
82            .ulp_magnitude_bits()
83            .saturating_add(b.ulp_magnitude_bits())
84            .saturating_add(1)
85            .min(T::MAX_ULP);
86    }
87    a.ulp_bits().abs_diff(b.ulp_bits())
88}
89
90/// Largest ULP distance over two sequences, with the index where it occurs.
91///
92/// Iteration stops at the shorter of the two; callers are expected to have
93/// checked the shapes already. Returns `(0, None)` when nothing was compared.
94pub fn max_ulp_distance<T: UlpFloat>(
95    a: impl IntoIterator<Item = T>,
96    b: impl IntoIterator<Item = T>,
97) -> (u64, Option<usize>) {
98    let mut worst = 0;
99    let mut at = None;
100    for (ix, (x, y)) in a.into_iter().zip(b).enumerate() {
101        let d = ulp_distance(x, y);
102        if d > worst || at.is_none() {
103            worst = d;
104            at = Some(ix);
105        }
106    }
107    (worst, at)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn adjacent_floats_are_one_ulp_apart() {
116        assert_eq!(ulp_distance(1.0f32, f32::from_bits(1.0f32.to_bits() + 1)), 1);
117        assert_eq!(ulp_distance(1.0f64, f64::from_bits(1.0f64.to_bits() + 1)), 1);
118        assert_eq!(
119            ulp_distance(f16::from_f32(1.0), f16::from_bits(f16::from_f32(1.0).to_bits() + 1)),
120            1
121        );
122    }
123
124    #[test]
125    fn identical_values_are_zero_ulp_apart() {
126        assert_eq!(ulp_distance(0.0f32, 0.0f32), 0);
127        assert_eq!(ulp_distance(-3.25f32, -3.25f32), 0);
128        assert_eq!(ulp_distance(f32::INFINITY, f32::INFINITY), 0);
129    }
130
131    #[test]
132    fn distance_is_symmetric_and_sign_aware() {
133        assert_eq!(ulp_distance(1.0f32, -1.0f32), ulp_distance(-1.0f32, 1.0f32));
134        // Signed zeros are distinct representations, hence 1 apart.
135        assert_eq!(ulp_distance(0.0f32, -0.0f32), 1);
136        // Straddling zero costs the two magnitudes plus the zero crossing.
137        let tiny = f32::from_bits(1);
138        assert_eq!(ulp_distance(tiny, -tiny), 3);
139    }
140
141    #[test]
142    fn nan_handling() {
143        assert_eq!(ulp_distance(f32::NAN, f32::NAN), 0);
144        assert_eq!(ulp_distance(f32::NAN, 1.0f32), f32::MAX_ULP);
145        assert_eq!(ulp_distance(1.0f32, f32::NAN), f32::MAX_ULP);
146    }
147
148    #[test]
149    fn infinity_is_adjacent_to_max_finite() {
150        assert_eq!(ulp_distance(f32::MAX, f32::INFINITY), 1);
151        assert_eq!(ulp_distance(-f32::MAX, f32::NEG_INFINITY), 1);
152    }
153
154    #[test]
155    fn magnitude_independence() {
156        // The same "one rounding step" error reads as 1 ULP at any scale, which is
157        // the whole point of the metric.
158        for scale in [1e-30f32, 1.0, 1e30] {
159            assert_eq!(ulp_distance(scale, f32::from_bits(scale.to_bits() + 1)), 1);
160        }
161    }
162
163    #[test]
164    fn max_over_slice_reports_position() {
165        let a = [1.0f32, 2.0, 3.0];
166        let b = [1.0f32, 2.0, f32::from_bits(3.0f32.to_bits() + 5)];
167        assert_eq!(max_ulp_distance(a, b), (5, Some(2)));
168        assert_eq!(max_ulp_distance::<f32>([], []), (0, None));
169    }
170}