@@ -12,15 +12,22 @@ use std::fmt::{Debug, Display, LowerHex};
1212use std:: hint:: black_box;
1313use std:: { f32, f64} ;
1414
15- /// Another way of checking if 2 floating point numbers are almost equal to eachother.
16- /// Using `a` and `b` as floating point numbers:
15+ /// Another way of checking if 2 floating- point numbers are almost equal to eachother.
16+ /// Using `a` and `b` as floating- point numbers:
1717///
18- /// Instead of doing a simple EPSILON check (which we did at first):
19- /// Absolute difference of `a` and `b` can't be greater than some E (10^-6, ...).
20- /// `(a - b).asb() <= E`
18+ /// Instead of performing a simple EPSILON check (which we used at first):
19+ /// The absolute difference between 'a' and 'b' must not be greater than some E (10^-6, ...)
2120///
22- /// We will now use ULP: `Units in the Last Place` or `Units of Least Precision`
23- /// It is the absolute difference of the *unsigned integer* representation of `a` and `b`.
21+ /// We will now use ULP: `Units in the Last Place` or `Units of Least Precision`,
22+ /// more specific, the difference in ULP of `a` and `b`.
23+ /// First: The ULP of a float 'a' is the smallest possible change at 'a', so the ULP difference represents how
24+ /// many discrete floating-point steps are needed to reach 'b' from 'a'.
25+ ///
26+ /// There are 2 techniques we can use to compute the ULP difference of 2 floating-point numbers:
27+ ///
28+ /// ULP(a) is the difference between 'a' and the next larger representable floating-point number.
29+ /// So to get the ULP difference of `a` and `b`, we can take the absolute difference of
30+ /// the *unsigned integer* representation of `a` and `b`.
2431/// For example checking 2 f32's, which in IEEE format looks like this:
2532///
2633/// s: sign bit
@@ -29,71 +36,78 @@ use std::{f32, f64};
2936/// f32: s | eeee eeee | mmm mmmm mmmm mmmm mmmm mmmm
3037/// u32: seee eeee emmm mmmm mmmm mmmm mmmm mmmm
3138///
32- /// We see that checking `a` and `b` with different signs has no meaning, but we should not forget
33- /// -0.0 and +0.0.
34- /// Same with exponents but no zero checking.
3539///
36- /// So when Sign and Exponent are the same, we can get a reasonable ULP value
37- /// by doing the absolute difference, and if this is less than or equal to our chosen upper bound
38- /// we can say that `a` and `b` are approximately equal.
40+ /// So when the sign and exponent are the same, we can compute a reasonable ULP difference.
41+ ///
42+ /// But if you know some details of floating-point numbers, it is possible to get 2 values that sit on the border of the exponent,
43+ /// so `a` can have exponent but `b` can have a different exponent while still being approximately equal to `a`. For this we can use
44+ /// John Harrison's definition. Which states that ULP(a) is the distance between the 2 closest floating-point numbers
45+ /// `x` and `y` around `a`, satisfying x < a < y, x != y. This makes the ULP of `a` twice as large as in the previous defintion and so
46+ /// if we want to use this, we have to halve it.
47+ /// This ULP value of `a` can then be used to find the ULP difference of `a` and `b`:
48+ /// Take the difference of `b` and `a` and divide it by that ULP and finally round it. This is the same value as the first definition,
49+ /// but this way can go around that exponent border problem.
50+ ///
51+ /// If this ULP difference is less than or equal to our chosen upper bound
52+ /// we can say that `a` and `b` are approximately equal, because they lie "close" enough to each other to be considered equal.
3953///
40- /// Basically ULP can be seen as a distance metric of floating point numbers, but having
41- /// the same amount of "spacing" between all consecutive representable values. So eventhough 2 very large floating point numbers
54+ /// Note: We can see that checking `a` and `b` with different signs has no meaning, but we should not forget
55+ /// -0.0 and +0.0.
56+ ///
57+ /// Essentially ULP can be seen as a distance metric of floating-point numbers, but with
58+ /// the same amount of "spacing" between all consecutive representable values. So even though 2 very large floating point numbers
4259/// have a large value difference, their ULP can still be 1, so they are still "approximatly equal",
4360/// but the EPSILON check would have failed.
4461fn approx_eq_check < F : Float > (
4562 actual : F ,
4663 expected : F ,
4764 allowed_ulp : F :: Int ,
48- ) -> Result < ( ) , NotApproxEq < F :: Int > >
65+ ) -> Result < ( ) , NotApproxEq < F > >
4966where
50- F :: Int : PartialOrd + AbsDiff ,
67+ F :: Int : PartialOrd ,
5168{
5269 let actual_signum = actual. signum ( ) ;
5370 let expected_signum = expected. signum ( ) ;
5471
5572 if actual_signum != expected_signum {
73+ // Floats with different signs must both be 0.
5674 if actual != expected {
5775 return Err ( NotApproxEq :: SignsDiffer ) ;
5876 }
5977 } else {
60- // reinterpret the floating point numbers as unsigned integers
61- let actual_bits = actual. to_bits ( ) ;
62- let expected_bits = expected. to_bits ( ) ;
78+ let ulp = ( expected. next_up ( ) - expected. next_down ( ) ) . halve ( ) ;
79+ let ulp_diff = ( ( actual - expected) / ulp) . round ( ) . as_int ( ) ;
6380
64- // take their absolute difference to get ULP
65- let ulp_diff = actual_bits. abs_diff ( expected_bits) ;
6681 if ulp_diff > allowed_ulp {
6782 return Err ( NotApproxEq :: UlpFail ( ulp_diff) ) ;
6883 }
6984 }
7085 Ok ( ( ) )
7186}
7287
73- /// Give more context to execution and result of [`approx_eq_check`]
74- enum NotApproxEq < Int > {
88+ /// Give more context to execution and result of [`approx_eq_check`].
89+ enum NotApproxEq < F : Float > {
7590 SignsDiffer ,
7691
77- /// Contains the actual ulp value calculated
78- UlpFail ( Int ) ,
92+ /// Contains the actual ulp value calculated.
93+ UlpFail ( F :: Int ) ,
7994}
8095
8196macro_rules! assert_approx_eq {
8297 ( $a: expr, $b: expr, $ulp: expr) => { {
8398 let ( a, b) = ( $a, $b) ;
84- let ulp = $ulp;
85- // Floats with different signs must both be 0.
86- match approx_eq_check( a, b, ulp) {
99+ let allowed_ulp = $ulp;
100+ match approx_eq_check( a, b, allowed_ulp) {
87101 Err ( NotApproxEq :: SignsDiffer ) =>
88102 panic!( "{a:?} is not approximately equal to {b:?}: signs differ" ) ,
89103 Err ( NotApproxEq :: UlpFail ( actual_ulp) ) =>
90- panic!( "{a:?} is not approximately equal to {b:?}\n ulp diff: {actual_ulp} > {ulp }" ) ,
91- _ => { }
104+ panic!( "{a:?} is not approximately equal to {b:?}\n ulp diff: {actual_ulp} > {allowed_ulp }" ) ,
105+ Ok ( _ ) => { }
92106 } ;
93107 } } ;
94108
95109 ( $a: expr, $b: expr) => {
96- // accept up to 64ULP (16ULP for host floats and 16ULP for artificial error and 32 for any rounding errors)
110+ // accept up to 64ULP (16ULP for host floats and 16ULP for miri artificial error and 32 for any rounding errors)
97111 assert_approx_eq!( $a, $b, 64 ) ;
98112 } ;
99113}
@@ -114,12 +128,14 @@ fn main() {
114128 test_non_determinism ( ) ;
115129}
116130
117- // to help `approx_eq_check`
118- trait AbsDiff {
119- fn abs_diff ( self , other : Self ) -> Self ;
120- }
121-
122- trait Float : Copy + PartialEq + Debug {
131+ trait Float :
132+ Copy
133+ + PartialEq
134+ + Debug
135+ + std:: ops:: Sub < Output = Self >
136+ + std:: cmp:: PartialOrd
137+ + std:: ops:: Div < Output = Self >
138+ {
123139 /// The unsigned integer with the same bit width as this float
124140 type Int : Copy + PartialEq + LowerHex + Debug ;
125141 const BITS : u32 = size_of :: < Self > ( ) as u32 * 8 ;
@@ -134,7 +150,14 @@ trait Float: Copy + PartialEq + Debug {
134150
135151 fn to_bits ( self ) -> Self :: Int ;
136152
153+ // to make "approx_eq_check" generic
137154 fn signum ( self ) -> Self ;
155+ fn next_up ( self ) -> Self ;
156+ fn next_down ( self ) -> Self ;
157+ fn round ( self ) -> Self ;
158+ // self / 2
159+ fn halve ( self ) -> Self ;
160+ fn as_int ( self ) -> Self :: Int ;
138161}
139162
140163macro_rules! impl_float {
@@ -151,11 +174,22 @@ macro_rules! impl_float {
151174 fn signum( self ) -> Self {
152175 self . signum( )
153176 }
154- }
177+ fn next_up( self ) -> Self {
178+ self . next_up( )
179+ }
180+ fn next_down( self ) -> Self {
181+ self . next_down( )
182+ }
183+ fn round( self ) -> Self {
184+ self . round( )
185+ }
186+
187+ fn halve( self ) -> Self {
188+ self / 2.0
189+ }
155190
156- impl AbsDiff for $ity {
157- fn abs_diff( self , other: Self ) -> Self {
158- self . abs_diff( other)
191+ fn as_int( self ) -> Self :: Int {
192+ self as Self :: Int
159193 }
160194 }
161195 } ;
0 commit comments