Skip to content

Commit 94d5ba5

Browse files
use another definition of ULP to compute ULP diff + extend explanation of this
1 parent cab874e commit 94d5ba5

File tree

1 file changed

+76
-42
lines changed

1 file changed

+76
-42
lines changed

tests/pass/float.rs

+76-42
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ use std::fmt::{Debug, Display, LowerHex};
1212
use std::hint::black_box;
1313
use 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.
4461
fn 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>>
4966
where
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

8196
macro_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:?}\nulp diff: {actual_ulp} > {ulp}"),
91-
_ => {}
104+
panic!("{a:?} is not approximately equal to {b:?}\nulp 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

140163
macro_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

Comments
 (0)