java junit 测试选择哪个 delta 以避免浮点错误
java juint testing which delta to chose to avoid floatting point error
我正在使用 jUnit 测试我的 Java 应用程序(我对 Java 完全陌生)。
public class MyClass {
public static void main(String args[]) {
double A = 50000.0;
double B = 1.1;
System.out.println("Result" + A * B);
}
}
一个'normal'答案(从数学角度)是:55000
不过javareturns:
Result 55000.00000000001
因此,我的测试失败了,因为未遵守 assertconsiditon
我的 jUnit 测试看起来像:
Assert.assertEquals("55000", <javaResult>, 0.0);
我认为问题出在最后一个名为 delta 的参数上。因为当我尝试随机更改增量时。以下不给出失败(即测试按预期通过)
Assert.assertEquals("55000", <javaResult>, 0.01);
所以我的问题是:假设我必须只执行乘法,我应该选择哪个增量? (我觉得随机选择 0.01 只是因为它有效......感觉不舒服......)
在正常范围内(量级在[2−1022、21024)),aJavadouble
有一个53位的尾数,所以相邻可表示值之间的步长最多是252的一部分。每当一个数字使用舍入到double
格式时,结果如果在正常范围内,最多与原始数字相差半步。
因此,如果 a 是十进制数字的值转换为 double
值 a
,则 a
= a•(1+e),其中|e| ≤ 2−53.
考虑将两个数字的数学乘积 xy 与将这两个数字转换为 double
x
和 y
的结果进行比较] 并将它们相乘以产生 double
结果。我们可能会将确切的乘积 xy 写成十进制数字,但是,在将其传递给 assertEquals
时,它会被转换为 double
,因此我们实际上传递了 xy•(1+e0) 对于某些 e 0 如上所述。
同理,x转换为一些x
等于x•(1+e 1),而y转换为一些y
等于y•(1+e2)。然后我们将它们相乘形成 (x•(1+e1))•(y•(1+e2))•(1+e3).
最后,我们将 xy•(1+e0) 与 (x•(1+e1))•(y•(1+e2))•(1+e3 )。它们有多大不同?
后者为xy•(1+e1)•(1+ e2)•(1+e3) , 所以差值是 xy•((1+e1)•(1+e2)•(1+e3)−(1+e0)) = xy•(e1+e2+e3+e1e2+e 2e3+e 1e3+e 1e2e3
−e0)。我们可以很容易地看到当 e0 为 −2−53 并且其他错误是+2−53。那么就是4•2−53 + 3•2−106 + 2−159。无论 x 或 y 是正的还是负的,这个误差界限的最大幅度保持不变,所以我们可以将这个界限的差异描述为|xy|•(4•2−53 + 3•2−106 + 2−159).
我们无法使用 double
精确计算,原因有以下三个:
- xy 可能无法准确表示。
- 4•2−53 + 3•2−106 + 2−159不可表示。
- 这两个值用
double
相乘时,可能会出现另一个舍入误差。
为了处理第一个问题,假设我们有所需产品的绝对值 xy,作为十进制数字 N
。然后我们可以替换|xy|在带有 Math.nextAfter(N, Double.POSITIVE_INFINITY)
的表达式中。这增加了少量(尽可能少的)以补偿 N
在转换为 double
时可能向下舍入的事实。 (我们也可以通过将其转换为 double
并向 ∞ 舍入而不是舍入到最近的舍入来准备 N
。)
为了处理第二个问题,我们可以将4•2−53 + 3•2−106 + 2−159 到 double
,向 ∞ 舍入。结果为 4•2−53 + 3•2−106,或 2−51 + 2−103。从 JDK 5 开始,我们可以将其写为 0x1.0000000000001p-51
.
第三题相对于结果最多可以引入2−53的舍入误差。然而,在将误差项转换为 double
时,我们四舍五入了不止于此(2−103 超过 3•2−106 的量 + 2−159 小于误差项的 2−53 倍),所以,即使计算结果四舍五入下降了 2−53(相对),仍然高于期望的数学结果。
因此,Assert.assertEquals("<exactResult>", <javaResult>, Math.nextAfter(N, Double.POSITIVE_INFINITY)*0x1.0000000000001p-51);
只有在满足以下至少一项条件时才会报告错误:
<javaResult>
不是在double
中计算两个十进制数的乘积的结果,其精确乘积是<exactResult>
.
<exactResult>
不在double
的正常范围内。
- 计算出的公差不在
double
的正常范围内(导致它被向下舍入超过上述预期)。 (注意第二个条件隐含了这个第三个,所以第二个可以省略。)
如果 <exactResult>
处于低于正常范围内,则可以使用较小的绝对值作为公差,而不是相对值。我省略了讨论。
请注意,如果产品符合预期,此绑定将不会报告错误。但是,如果产品有误,不保证一定会报错。容差是单个操作错误界限的四倍多(因为涉及四个操作)。因此,断言将接受除计算乘积的正确结果之外的几个值。换句话说,对于非常接近但不同于正确结果的结果,可能会出现假阴性。
这是错误的上限。可以通过更多分析进一步收紧它,并且在某些情况下可以收紧更多,例如如果已知 xy 是可精确表示的,或者如果x 和 y 是已知的。
我希望 nextAfter
可以替换为增加误差因子,但我还没有进行分析以能够断言,比如说,增加一个 ULP 就足够了。
我正在使用 jUnit 测试我的 Java 应用程序(我对 Java 完全陌生)。
public class MyClass {
public static void main(String args[]) {
double A = 50000.0;
double B = 1.1;
System.out.println("Result" + A * B);
}
}
一个'normal'答案(从数学角度)是:55000
不过javareturns:
Result 55000.00000000001
因此,我的测试失败了,因为未遵守 assertconsiditon
我的 jUnit 测试看起来像:
Assert.assertEquals("55000", <javaResult>, 0.0);
我认为问题出在最后一个名为 delta 的参数上。因为当我尝试随机更改增量时。以下不给出失败(即测试按预期通过)
Assert.assertEquals("55000", <javaResult>, 0.01);
所以我的问题是:假设我必须只执行乘法,我应该选择哪个增量? (我觉得随机选择 0.01 只是因为它有效......感觉不舒服......)
在正常范围内(量级在[2−1022、21024)),aJavadouble
有一个53位的尾数,所以相邻可表示值之间的步长最多是252的一部分。每当一个数字使用舍入到double
格式时,结果如果在正常范围内,最多与原始数字相差半步。
因此,如果 a 是十进制数字的值转换为 double
值 a
,则 a
= a•(1+e),其中|e| ≤ 2−53.
考虑将两个数字的数学乘积 xy 与将这两个数字转换为 double
x
和 y
的结果进行比较] 并将它们相乘以产生 double
结果。我们可能会将确切的乘积 xy 写成十进制数字,但是,在将其传递给 assertEquals
时,它会被转换为 double
,因此我们实际上传递了 xy•(1+e0) 对于某些 e 0 如上所述。
同理,x转换为一些x
等于x•(1+e 1),而y转换为一些y
等于y•(1+e2)。然后我们将它们相乘形成 (x•(1+e1))•(y•(1+e2))•(1+e3).
最后,我们将 xy•(1+e0) 与 (x•(1+e1))•(y•(1+e2))•(1+e3 )。它们有多大不同?
后者为xy•(1+e1)•(1+ e2)•(1+e3) , 所以差值是 xy•((1+e1)•(1+e2)•(1+e3)−(1+e0)) = xy•(e1+e2+e3+e1e2+e 2e3+e 1e3+e 1e2e3 −e0)。我们可以很容易地看到当 e0 为 −2−53 并且其他错误是+2−53。那么就是4•2−53 + 3•2−106 + 2−159。无论 x 或 y 是正的还是负的,这个误差界限的最大幅度保持不变,所以我们可以将这个界限的差异描述为|xy|•(4•2−53 + 3•2−106 + 2−159).
我们无法使用 double
精确计算,原因有以下三个:
- xy 可能无法准确表示。
- 4•2−53 + 3•2−106 + 2−159不可表示。
- 这两个值用
double
相乘时,可能会出现另一个舍入误差。
为了处理第一个问题,假设我们有所需产品的绝对值 xy,作为十进制数字 N
。然后我们可以替换|xy|在带有 Math.nextAfter(N, Double.POSITIVE_INFINITY)
的表达式中。这增加了少量(尽可能少的)以补偿 N
在转换为 double
时可能向下舍入的事实。 (我们也可以通过将其转换为 double
并向 ∞ 舍入而不是舍入到最近的舍入来准备 N
。)
为了处理第二个问题,我们可以将4•2−53 + 3•2−106 + 2−159 到 double
,向 ∞ 舍入。结果为 4•2−53 + 3•2−106,或 2−51 + 2−103。从 JDK 5 开始,我们可以将其写为 0x1.0000000000001p-51
.
第三题相对于结果最多可以引入2−53的舍入误差。然而,在将误差项转换为 double
时,我们四舍五入了不止于此(2−103 超过 3•2−106 的量 + 2−159 小于误差项的 2−53 倍),所以,即使计算结果四舍五入下降了 2−53(相对),仍然高于期望的数学结果。
因此,Assert.assertEquals("<exactResult>", <javaResult>, Math.nextAfter(N, Double.POSITIVE_INFINITY)*0x1.0000000000001p-51);
只有在满足以下至少一项条件时才会报告错误:
<javaResult>
不是在double
中计算两个十进制数的乘积的结果,其精确乘积是<exactResult>
.<exactResult>
不在double
的正常范围内。- 计算出的公差不在
double
的正常范围内(导致它被向下舍入超过上述预期)。 (注意第二个条件隐含了这个第三个,所以第二个可以省略。)
如果 <exactResult>
处于低于正常范围内,则可以使用较小的绝对值作为公差,而不是相对值。我省略了讨论。
请注意,如果产品符合预期,此绑定将不会报告错误。但是,如果产品有误,不保证一定会报错。容差是单个操作错误界限的四倍多(因为涉及四个操作)。因此,断言将接受除计算乘积的正确结果之外的几个值。换句话说,对于非常接近但不同于正确结果的结果,可能会出现假阴性。
这是错误的上限。可以通过更多分析进一步收紧它,并且在某些情况下可以收紧更多,例如如果已知 xy 是可精确表示的,或者如果x 和 y 是已知的。
我希望 nextAfter
可以替换为增加误差因子,但我还没有进行分析以能够断言,比如说,增加一个 ULP 就足够了。