diff --git a/tests/pass/float.rs b/tests/pass/float.rs index 6259253749..b533b43209 100644 --- a/tests/pass/float.rs +++ b/tests/pass/float.rs @@ -13,78 +13,26 @@ use std::fmt::{Debug, Display, LowerHex}; use std::hint::black_box; use std::{f32, f64}; -/// Another way of checking if 2 floating-point numbers are almost equal to eachother. -/// Using `a` and `b` as floating-point numbers: +/// Compare the two floats, allowing for $ulp many ULPs of error. /// -/// Instead of performing a simple EPSILON check (which we used at first): -/// The absolute difference between 'a' and 'b' must not be greater than some E (10^-6, ...) -/// -/// We will now use ULP: `Units in the Last Place` or `Units of Least Precision`, -/// more specific, the difference in ULP of `a` and `b`. -/// First: The ULP of a float 'a' is the smallest possible change at 'a', so the ULP difference represents how -/// many discrete floating-point steps are needed to reach 'b' from 'a'. -/// -/// ULP(a) is the distance between the 2 closest floating-point numbers `x` and `y` around `a`, satisfying x < a < y, x != y. -/// To use this to calculate the ULP difference we have to halve it (we need it at `a`, but we just went "up" and "down", halving it gives us this ULP). -/// Then take the difference of `b` and `a` and divide it by that ULP and finally round it. -/// We know now how many floating-point changes we have to apply to `a` to get to `b`. -/// -/// So if this ULP difference is less than or equal to our chosen upper bound -/// we can say that `a` and `b` are approximately equal, because they lie "close" enough to each other to be considered equal. -/// -/// Note: We can see that checking `a` and `b` with different signs has no meaning, but we should not forget -/// -0.0 and +0.0. +/// ULP means "Units in the Last Place" or "Units of Least Precision". +/// The ULP of a float `a`` is the smallest possible change at `a`, so the ULP difference represents how +/// many discrete floating-point steps are needed to reach the actual value from the expected value. /// /// Essentially ULP can be seen as a distance metric of floating-point numbers, but with /// the same amount of "spacing" between all consecutive representable values. So even though 2 very large floating point numbers /// have a large value difference, their ULP can still be 1, so they are still "approximatly equal", /// but the EPSILON check would have failed. -/// -fn approx_eq_check( - actual: F, - expected: F, - allowed_ulp: F::Int, -) -> Result<(), NotApproxEq> -where - F::Int: PartialOrd, -{ - let actual_signum = actual.signum(); - let expected_signum = expected.signum(); - - if actual_signum != expected_signum { - // Floats with different signs must both be 0. - if actual != expected { - return Err(NotApproxEq::SignsDiffer); - } - } else { - let ulp = (expected.next_up() - expected.next_down()).halve(); - let ulp_diff = ((actual - expected) / ulp).round().as_int(); - - if ulp_diff > allowed_ulp { - return Err(NotApproxEq::UlpFail(ulp_diff)); - } - } - Ok(()) -} - -/// Give more context to execution and result of [`approx_eq_check`]. -enum NotApproxEq { - SignsDiffer, - - /// Contains the actual ulp value calculated. - UlpFail(F::Int), -} - macro_rules! assert_approx_eq { ($a:expr, $b:expr, $ulp:expr) => {{ - let (a, b) = ($a, $b); - let allowed_ulp = $ulp; - match approx_eq_check(a, b, allowed_ulp) { - Err(NotApproxEq::SignsDiffer) => - panic!("{a:?} is not approximately equal to {b:?}: signs differ"), - Err(NotApproxEq::UlpFail(actual_ulp)) => - panic!("{a:?} is not approximately equal to {b:?}\nulp diff: {actual_ulp} > {allowed_ulp}"), - Ok(_) => {} + let (actual, expected) = ($a, $b); + let allowed_ulp_diff = $ulp; + // Approximate the ULP by taking half the distance between the number one place "up" + // and the number one place "down". + let ulp = (expected.next_up() - expected.next_down()) / 2.0; + let ulp_diff = ((actual - expected) / ulp).round() as i32; + if ulp_diff > allowed_ulp_diff { + panic!("{actual:?} is not approximately equal to {expected:?}\ndifference in ULP: {ulp_diff} > {allowed_ulp_diff}"); }; }}; @@ -110,14 +58,7 @@ fn main() { test_non_determinism(); } -trait Float: - Copy - + PartialEq - + Debug - + std::ops::Sub - + std::cmp::PartialOrd - + std::ops::Div -{ +trait Float: Copy + PartialEq + Debug { /// The unsigned integer with the same bit width as this float type Int: Copy + PartialEq + LowerHex + Debug; const BITS: u32 = size_of::() as u32 * 8; @@ -131,15 +72,6 @@ trait Float: const EXPONENT_BIAS: u32 = Self::EXPONENT_SAT >> 1; fn to_bits(self) -> Self::Int; - - // to make "approx_eq_check" generic - fn signum(self) -> Self; - fn next_up(self) -> Self; - fn next_down(self) -> Self; - fn round(self) -> Self; - // self / 2 - fn halve(self) -> Self; - fn as_int(self) -> Self::Int; } macro_rules! impl_float { @@ -152,27 +84,6 @@ macro_rules! impl_float { fn to_bits(self) -> Self::Int { self.to_bits() } - - fn signum(self) -> Self { - self.signum() - } - fn next_up(self) -> Self { - self.next_up() - } - fn next_down(self) -> Self { - self.next_down() - } - fn round(self) -> Self { - self.round() - } - - fn halve(self) -> Self { - self / 2.0 - } - - fn as_int(self) -> Self::Int { - self as Self::Int - } } }; }