@@ -12,15 +12,22 @@ use std::fmt::{Debug, Display, LowerHex};
12
12
use std:: hint:: black_box;
13
13
use std:: { f32, f64} ;
14
14
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:
17
17
///
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, ...)
21
20
///
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`.
24
31
/// For example checking 2 f32's, which in IEEE format looks like this:
25
32
///
26
33
/// s: sign bit
@@ -29,71 +36,78 @@ use std::{f32, f64};
29
36
/// f32: s | eeee eeee | mmm mmmm mmmm mmmm mmmm mmmm
30
37
/// u32: seee eeee emmm mmmm mmmm mmmm mmmm mmmm
31
38
///
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.
35
39
///
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.
39
53
///
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
42
59
/// have a large value difference, their ULP can still be 1, so they are still "approximatly equal",
43
60
/// but the EPSILON check would have failed.
44
61
fn approx_eq_check < F : Float > (
45
62
actual : F ,
46
63
expected : F ,
47
64
allowed_ulp : F :: Int ,
48
- ) -> Result < ( ) , NotApproxEq < F :: Int > >
65
+ ) -> Result < ( ) , NotApproxEq < F > >
49
66
where
50
- F :: Int : PartialOrd + AbsDiff ,
67
+ F :: Int : PartialOrd ,
51
68
{
52
69
let actual_signum = actual. signum ( ) ;
53
70
let expected_signum = expected. signum ( ) ;
54
71
55
72
if actual_signum != expected_signum {
73
+ // Floats with different signs must both be 0.
56
74
if actual != expected {
57
75
return Err ( NotApproxEq :: SignsDiffer ) ;
58
76
}
59
77
} 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 ( ) ;
63
80
64
- // take their absolute difference to get ULP
65
- let ulp_diff = actual_bits. abs_diff ( expected_bits) ;
66
81
if ulp_diff > allowed_ulp {
67
82
return Err ( NotApproxEq :: UlpFail ( ulp_diff) ) ;
68
83
}
69
84
}
70
85
Ok ( ( ) )
71
86
}
72
87
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 > {
75
90
SignsDiffer ,
76
91
77
- /// Contains the actual ulp value calculated
78
- UlpFail ( Int ) ,
92
+ /// Contains the actual ulp value calculated.
93
+ UlpFail ( F :: Int ) ,
79
94
}
80
95
81
96
macro_rules! assert_approx_eq {
82
97
( $a: expr, $b: expr, $ulp: expr) => { {
83
98
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) {
87
101
Err ( NotApproxEq :: SignsDiffer ) =>
88
102
panic!( "{a:?} is not approximately equal to {b:?}: signs differ" ) ,
89
103
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 ( _ ) => { }
92
106
} ;
93
107
} } ;
94
108
95
109
( $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)
97
111
assert_approx_eq!( $a, $b, 64 ) ;
98
112
} ;
99
113
}
@@ -114,12 +128,14 @@ fn main() {
114
128
test_non_determinism ( ) ;
115
129
}
116
130
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
+ {
123
139
/// The unsigned integer with the same bit width as this float
124
140
type Int : Copy + PartialEq + LowerHex + Debug ;
125
141
const BITS : u32 = size_of :: < Self > ( ) as u32 * 8 ;
@@ -134,7 +150,14 @@ trait Float: Copy + PartialEq + Debug {
134
150
135
151
fn to_bits ( self ) -> Self :: Int ;
136
152
153
+ // to make "approx_eq_check" generic
137
154
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 ;
138
161
}
139
162
140
163
macro_rules! impl_float {
@@ -151,11 +174,22 @@ macro_rules! impl_float {
151
174
fn signum( self ) -> Self {
152
175
self . signum( )
153
176
}
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
+ }
155
190
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
159
193
}
160
194
}
161
195
} ;
0 commit comments