logoalt Hacker News

jphtoday at 11:14 AM10 repliesview on HN

I have this floating-point problem at scale and will donate $100 to the author, or to anyone here, who can improve my code the most.

The Rust code in the assert_f64_eq macro is:

    if (a >= b && a - b < f64::EPSILON) || (a <= b && b - a < f64::EPSILON)
I'm the author of the Rust assertables crate. It provides floating-point assert macros much as described in the article.

https://github.com/SixArm/assertables-rust-crate/blob/main/s...

If there's a way to make it more precise and/or specific and/or faster, or create similar macros with better functionality and/or correctness, that's great.

See the same directory for corresponding assert_* macros for less than, greater than, etc.


Replies

hmrytoday at 11:44 AM

Is there any constant more misused in compsci than ieee epsilon? :)

It's defined as the difference between 1.0 and the smallest number larger than 1.0. More usefully, it's the spacing between adjacent representable float numbers in the range 1.0 to 2.0.

Because floats get less precise at every integer power of two, it's impossible for two numbers greater than or equal to 2.0 to be epsilon apart. The spacing between 2.0 and the next larger number is 2*epsilon.

That means `abs(a - b) <= epsilon` is equivalent to `a == b` for any a or b greater than or equal to 2.0. And if you use `<` then the limit will be 1.0 instead.

Epsilon is the wrong tool for the job in 99.9% of cases.

show 3 replies
pclmulqdqtoday at 11:26 AM

Your assertion code here doesn't make a ton of sense. The epsilon of choice here is the distance between 1 and the next number up, and it's completely separated from the scale of the numbers in question. 1e-50 will compare equal to 2e-50, for example.

I would suggest that "equals" actually is for "exactly equals" as in (a == b). In many pieces of floating point code this is the correct thing to test. Then also add a function for "within range of" so your users can specify an epsilon of interest, using the formula (abs(a - b) < eps). You may also want to support multidimensional quantities by allowing the user to specify a distance metric. You probably also want a relative version of the comparison in addition to an absolute version.

Auto-computing epsilons for an equality check is really hard and depends on the usage, as well as the numerics of the code that is upstream and downstream of the comparison. I don't see how you would do it in an assertion library.

judofyrtoday at 12:07 PM

Ignoring the misuse of epsilon, I'd also say that you'd be helping your users more by not providing a general `assert_f64_eq` macro, but rather force the user to decide the error model. Add a required "precision" parameter as an enum with different modes:

    // Precise matching:
    assert_f64_eq!(a, 0.1, Steps(2))
    // same as: assert!(a == 0.1.next_down().next_down())

    // Number of digits (after period) that are matching:
    assert_f64_eq!(a, 0.1, Digits(5))

    // Relative error:
    assert_f64_eq!(a, 0.1, Rel(0.5))
lukaxtoday at 11:24 AM

You generally want both relative and absolute tolerances. Relative handles scale, absolute handles values near zero (raw EPSILON isn’t a universal threshold per IEEE 754).

The usual pattern is abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) to avoid both large-value and near-zero pitfalls.

show 1 reply
thomasmgtoday at 11:38 AM

It depends on the use case, but do you consider NaN to be equal to NaN? For an assert macro, I would expect so. Also, your code works differently for very large and very small numbers, eg. 1.0000001, 1.0000002 vs 1e-100, 1.0000002e-100.

For my own soft-floating point math library, I expect the value is off by a some percentage, not just off by epsilon. And so I have my own almostSame method [1] which accounts for that and is quite a bit more complex. Actually multiple such methods. But well, that's just my own use case.

[1] https://github.com/thomasmueller/bau-lang/blob/main/src/test...

fouronnes3today at 11:55 AM

You should use two tolerances: absolute and relative. See for example numpy.allclose()

https://numpy.org/doc/stable/reference/generated/numpy.allcl...

layer8today at 12:59 PM

Apart from what others have commented, IMO an “assertables” crate should not invent new predicates of its own, especially for domains (like math) that are orthogonal to assertability.

reacwebtoday at 12:35 PM

I suggest

if a.abs()+b.abs() >= (a-b).abs() * 2f64.powi(48)

It remains accurate for small and for big numbers. 48 is slightly less than 52.

lifthrasiirtoday at 11:31 AM

Hyb error [1] might be what you want.

[1] https://arxiv.org/html/2403.07492v2

werdnapktoday at 12:37 PM

The use of epsilon is correct here. It's exactly what I was taught in comp sci over 20 years ago. You can call it's use here an "epsilon-delta".