Search code examples
javajunitfloating-pointassertdelta

java juint testing which delta to chose to avoid floatting point error


I am using jUnit to test my Java app (I am totally new to Java).

public class MyClass {
    public static void main(String args[]) {

        double A = 50000.0;
        double B = 1.1;
         System.out.println("Result" + A * B);
    }
}

A 'normal' answer (from a mathematical perspective) is : 55000

However java returns:

Result 55000.00000000001

Thus, my test fails because the assertconsiditon is not respected

My jUnit test looks like:

        Assert.assertEquals("55000", <javaResult>, 0.0);

I belive the problem is due to the last parameter called delta. Because when I try to change the delta randomly. The following does not give a failure (i.e. the test passed as expected)

        Assert.assertEquals("55000", <javaResult>, 0.01);

So my question is : assuming I have to perform exclusively multiplications, which delta should I chose? (I do not feel confortable to randomly chose 0.01 just because it works...)


Solution

  • In the normal range (magnitudes in [2−1022, 21024)), a Java double has a 53-bit significand, so the step size between adjacent representable values is at most one part in 252. Whenever a number is converted to the double format using round-to-nearest, the result, if it is in the normal range, is at most half a step away from the original number.

    Thus, if a is the value of a decimal numeral that is converted to a double value a, then a = a•(1+e), where |e| ≤ 2−53.

    Consider comparing the mathematical product of two numbers, xy, to the result of converting the two numbers to double x and y and multiplying them to produce a double result. We might write the exact product xy as a decimal numeral, but, in passing it to assertEquals, it will be converted to double, so we actually pass xy•(1+e0) for some e0 as described above.

    Similarly, x is converted to some x equal to x•(1+e1), and y is converted to some y equal to y•(1+e2). Then we multiply these to form (x•(1+e1))•(y•(1+e2))•(1+e3).

    Finally, we compare xy•(1+e0) to (x•(1+e1))•(y•(1+e2))•(1+e3). How different can they be?

    The latter is xy•(1+e1)•(1+e2)•(1+e3), so the difference is xy•((1+e1)•(1+e2)•(1+e3)−(1+e0)) = xy•(e1+e2+e3+e1e2+e2e3+e1e3+e1e2e3e0). We can easily see the error term has its greatest magnitude when e0 is −2−53 and the other errors are +2−53. Then it is 4•2−53 + 3•2−106 + 2−159. Whether x or y is positive or negative, the greatest magnitude of this error bound remains the same, so we may characterize this bound on the difference as |xy|•(4•2−53 + 3•2−106 + 2−159).

    We cannot compute this exactly using double, for three reasons:

    • xy may not be exactly representable.
    • 4•2−53 + 3•2−106 + 2−159 is not representable.
    • When these two values are multiplied using double, there may be another rounding error.

    To deal with the first problem, suppose we have absolute value of the desired product, xy, as a decimal numeral N. Then we can replace |xy| in the expression with Math.nextAfter(N, Double.POSITIVE_INFINITY). This adds a slight amount (the slightest possible) to compensate for the fact that N might round down when converted to double. (We could also prepare N by converting it to a double with rounding toward ∞ instead of rounding to nearest.)

    To deal with the second problem, we can convert 4•2−53 + 3•2−106 + 2−159 to a double with rounding toward ∞. The result is 4•2−53 + 3•2−106, or 2−51 + 2−103. As of JDK 5, we can write that as 0x1.0000000000001p-51.

    The third problem can introduce a rounding error of at most 2−53 relative to the result. However, in converting the error term to double, we rounded up by more than that (the amount by which 2−103 exceeds 3•2−106 + 2−159 is less than 2−53 times the error term), so, even if the computed result is rounded down by 2−53 (relatively), it is still above the desired mathematical result.

    Thus, Assert.assertEquals("<exactResult>", <javaResult>, Math.nextAfter(N, Double.POSITIVE_INFINITY)*0x1.0000000000001p-51); reports an error only if at least one of the following is true:

    • <javaResult> is not the result of computing in double the product of two numbers converted from decimal numerals whose exact product is <exactResult>.
    • <exactResult> is not in the normal range of double.
    • The computed tolerance is not in the normal range of double (leading to it being rounded down by more than anticipated above). (Note that the second condition implies this third one, so the second one can be omitted.)

    If <exactResult> is in the subnormal range, a small absolute value might be used for the tolerance, instead of a relative value. I omit discussion of that.

    Note that this bound is such that the assert will not report an error if the product is as expected. However, it is not guaranteed to report an error if the product is wrong. The tolerance is more than four times the bound on the error of a single operation (because there were four operations involved). Thus, the assert will accept several values other than the proper result of computing the product. In other words, false negatives are possible for results that are very close to, but different from, the proper result.

    This is an upper bound on the error. It might be possible to tighten it further with some more analysis, and it is possible to tighten more in some circumstances, such as if it is known that xy is exactly representable or if particular values for x and y are known.

    I expect the nextAfter could be replaced with an increase to the error factor, but I have not done the analysis to be able to assert that, say, a one ULP increase suffices.