Closed jtempest closed 4 years ago
A quick comparison of how existing Rust floating point equality libs treat NaNs and infinities.
Conclusion:
float-cmp
treats it in a bitwise fashion, approx
and assert_float_eq
consider all NaN values not to be equal regardless of provided margin.Libraries:
Definitions:
let nan = f32::NAN;
let nan1 = f32::from_bits(nan.to_bits() + 1);
let nan2 = f32::from_bits(nan.to_bits() + 2);
let inf = f32::INFINITY;
let inf_prev = f32::from_bits(inf.to_bits() - 1);
// float-cmp specific
let margin = F32Margin {
epsilon: 0.0,
ulps: 1,
};
Comparisons used:
// float-cmp:
let margin = F32Margin {
epsilon: 0.0,
ulps: 1,
};
approx_eq!(f32, a, b, margin) // -> bool
// approx
ulps_eq!(a, b, max_ulps = 1) // -> bool
// assert_float_eq
expect_f32_near!(a, b, 1) // -> Result<(), FloatNearError>
nan == nan, ulps margin of 1:
nan == nan1, ulps margin of 1:
nan == nan2, ulps margin of 1:
inf == inf, ulps margin of 1:
inf == inf_prev, ulps margin of 1:
A quick comparison of how existing Rust libraries treat infinity in non-ULPs comparisons, using the definitions above.
Conclusion:
IEEE arithmetic with primitives:
(inf - inf_prev).abs() <= f32::MAX = false
(inf_prev - inf).abs() <= f32::MAX = false
float-cmp:
let margin = F32Margin { epsilon: f32::MAX, ulps: 0, };
approx_eq!(f32, inf, inf_prev, margin) = false
approx_eq!(f32, inf_prev, inf, margin) = false
approx:
abs_diff_eq!(inf, inf_prev, epsilon = f32::MAX) = false
abs_diff_eq!(inf_prev, inf, epsilon = f32::MAX) = false
relative_eq!(inf, inf_prev, max_relative = f32::MAX) = false
relative_eq!(inf_prev, inf, max_relative = f32::MAX) = false
assert_float_eq:
expect_float_absolute_eq!(inf, inf_prev, f32::MAX) = Err
expect_float_absolute_eq!(inf_prev, inf, f32::MAX) = Err
expect_float_relative_eq!(inf, inf_prev, f32::MAX) = Err
expect_float_relative_eq!(inf_prev, inf, f32::MAX) = Err
I think it is sensible for ULPs comparisons to match native floating point behaviour, since we are endeavouring to be able to use them in place of those, at least in terms of float_eq!
and float_ne!
. Therefore, the most consistent behaviour would be:
float_eq!(any_nan, any_value, ulps <= N)
=> false
float_eq!(any_value, any_nan, ulps <= N)
=> false
float_eq!(f32::INFINITY, f32::INFINITY, ulps <= N)
=> true
float_eq!(-f32::INFINITY, -f32::INFINITY, ulps <= N)
=> true
float_eq!(finite_value_or_nan, f32::INFINITY, ulps <= N)
=> false
float_eq!(f32::INFINITY, finite_value_or_nan, ulps <= N)
=> false
float_eq!(finite_value_or_nan, -f32::INFINITY, ulps <= N)
=> false
float_eq!(-f32::INFINITY, finite_value_or_nan, ulps <= N)
=> false
It follows that assert_float_eq
would then use this same behaviour. However, there is a split in potential functionality when it comes to testing - where you might want to check that something is a NaN or not. In this case, users should probably be encouraged to test using assert!(value.is_nan())
.
Counterpoint: ULPs represent the ratio between two numbers. The ratio between f32::MAX
and f32::INFINITY
happens to be infinite, but it is still an ULP of 1 within the range of what ULPs represent. If it ought to be treated as a special case, then perhaps denormals should too. It may be more sensible to document the strangeness at infinity and keep the algorithm simpler, then note in the documentation that advanced users who care about infinities ought to think about what they're testing in more detail.
Comprehensive behaviour tests across all comparison types have now been committed in https://github.com/jtempest/float_eq-rs/commit/b2e324cb51932dc58a22d56415885cebfefc4663, check the primitive unit tests for behavioural details.
Of the special IEEE 754 floating point values, ULPs comparisons only take into account the needs of zero and negative zero. Ideally, they should have some consistency with the way that regular floating point equality works with respect to NaNs and infinities. See also this reddit post.