2

The isapprox() function in Julia is used to test if two numbers or arrays are approximately equal. I want to be able to test for approximate equality for any desired number of significant digits. As the code sample below demonstrates, the tolerance for approximation is either given in an absolute value, or a relative (percentage) deviation.

# Syntax
isapprox(a, b; atol = <absolute tolerance>, rtol = <relative tolerance>)

# Examples
# Absolute tolerance
julia> isapprox(10.0,9.9; atol = 0.1) # Tolerance of 0.1
true

# Relative tolerance
julia> isapprox(11.5,10.5; rtol = 0.1) # Rel. tolerance of 10%
true

julia> isapprox(11.4,10.5; rtol = 0.01) # Rel. tolerance of 1%
false

julia> isapprox(98.5, 99.4; rtol = 0.01) # Rel. tolerance of 1%
true

I read in a forum somewhere that setting rtol = 1e-n, where n is the number of significant digits will compare the significant digits. (Unfortunately, I am unable to find it again.) No matter, as the example demonstrates, this is clearly not true.

Given that we in this case want to approximate equality with two significant digits, both 11.4 and 10.5 is approximately equal to 11. However, the relative difference between the two are greater than 1%, returning the approximation false. However, for any number greater than 90, the approximation will be true. Increasing the relative tolerance tenfold to 10% will result in too low sensitivity, as the code demonstrates.

Is there a parameter/value/formula I can set rtol for isapprox() to return true correctly for any desired number of significant digits?

desertnaut
  • 57,590
  • 26
  • 140
  • 166
Pål Bjartan
  • 793
  • 1
  • 6
  • 18

3 Answers3

6

The quick answer is no, there is no fixed value of rtol you can choose to guarantee that isapprox(x, y; rtol=rtol) compares values x and y to a certain number of significant figures. This is because of how isapprox is implemented: rtol is calculated relative to the maximum of norm(x) and norm(y). You would have to calculate a different rtol for each pair of x and y you are comparing.

What it looks like you are asking for is a way to compare the values of x and y rounded to a certain number of significant digits (in base 10). The round method has a keyword sigdigits that might be of use:

isapproxsigfigs(a, b, precision) = round(a, sigdigits=precision) == round(b, sigdigits=precision)

isapproxsigfigs(10, 9.9, 1)  # true,  10 == 10
isapproxsigfigs(10, 9.9, 2)  # false, 10 != 9.9

isapproxsigfigs(11.4, 10.5, 1)  # true,  10 == 10
isapproxsigfigs(11.4, 10.5, 2)  # false, 11 != 10 (remember RoundingMode!)

isapproxsigfigs(11.4, 10.51, 1)  # true,  10 == 10
isapproxsigfigs(11.4, 10.51, 2)  # true,  11 == 11
isapproxsigfigs(11.4, 10.51, 3)  # false, 11.4 != 10.5

For the second example, remember that 10.5 is only "almost 11" if you round ties up. The default RoundingMode used by Julia is RoundNearest, which rounds ties to even. If you want ties to round up, use RoundNearestTiesUp:

isapproxsigfigs2(a, b, precision) = 
    round(a, RoundNearestTiesUp, sigdigits=precision) == 
    round(b, RoundNearestTiesUp, sigdigits=precision)

isapproxsigfigs2(11.4, 10.5, 2)  # true, 11 == 11
PaSTE
  • 4,050
  • 18
  • 26
  • Thanks for the elaborate reply! `RoundNearestTiesUp` also resolves the issue with ties above even integers gets rounded down. – Pål Bjartan Aug 06 '21 at 11:11
0

I think you might just need to define your own function for this. A few tricky details:

  1. What base do you care about?
  2. How do you want to treat +0 and -0?
  3. Numbers that straddle 0 in general?
  4. Denormalized numbers?
  5. Numbers near infinity?

A quick poor person's version that will probably do the right thing in most corner cases would be to do a formatting print with the desired precision for both numbers, then compare the strings.

using Formatting

julia> function isapproxsigfigs(a, b, precision)
         fmt = "{:.$(precision-1)e}"
         format(fmt, a) == format(fmt, b)
       end
isapproxsigfigs (generic function with 1 method)

julia> isapproxsigfigs(pi, 3.14, 3)
true

julia> isapproxsigfigs(pi, 3.14, 4)
false

should work for base 10, always treat positive vs negative numbers as unequal, and probably do the right thing with denormalized numbers. You may want to add explicit checking for infinities and NaN because this implementation treats infinities as equal, and worse yet NaNs as equal.

Note: Future readers may be able to just do

isapproxsigfigs(a, b, precision) = @sprintf("%.*e", precision, a) == @sprintf("%.*e", precision, b)

but Julia does not currently support .* precision specification in its format strings. There is a PR out for that though, so maybe v1.7 onwards will support it

Nicu Stiurca
  • 8,747
  • 8
  • 40
  • 48
-1

You can take a hint from NumPy and implement:

less_equal(x,y) = x <= y
isclose(x, y, atol, rtol) = less_equal(abs(x-y), atol + rtol * abs(y))

This doesn't work very well vectorized, so make sure you have a look at allclose if you want a vectorized version. Also, Using rtol makes it non commutative on x and y, so take care with that.

cako
  • 294
  • 3
  • 11