jtempest / float_eq-rs

Compare IEEE floating point values for equality.
Other
39 stars 6 forks source link

Consider how ULPs should deal with special values #4

Closed jtempest closed 4 years ago

jtempest commented 4 years ago

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.

jtempest commented 4 years ago

A quick comparison of how existing Rust floating point equality libs treat NaNs and infinities.

Conclusion:

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:

jtempest commented 4 years ago

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

jtempest commented 4 years ago

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()).

jtempest commented 4 years ago

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.

jtempest commented 4 years ago

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.