blob: c08fc24c1060d59c34d19e761b6f276ec4bdaa6e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.util;
import java.util.Random;
import java.math.BigDecimal;
import java.math.MathContext;
import java.lang.reflect.Field;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.test.TestCase;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.DependsOn;
import org.junit.Test;
import static org.junit.Assert.*;
import static java.lang.StrictMath.*;
/**
* Tests {@link DoubleDouble} using {@link BigDecimal} as the references.
* Those tests need {@link DoubleDouble#DISABLED} to be set to {@code false}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 0.4
* @module
*/
@DependsOn(org.apache.sis.math.DecimalFunctionsTest.class)
public final strictfp class DoubleDoubleTest extends TestCase {
/**
* Number of time to repeat arithmetic tests.
*/
private static final int NUMBER_OF_REPETITIONS = 1000;
/**
* The tolerance factor (as a multiplicand) for the addition and subtraction operations.
* This is a tolerance factor in units of {@link DoubleDouble#value} ULP. Results smaller
* than 1 ULP of {@link DoubleDouble#error} will be clamped.
*/
private static final double ADD_TOLERANCE_FACTOR = 1E-17;
/**
* The tolerance factor (as a multiplicand) for the multiplication and division operations.
* This is a tolerance factor in units of {@link DoubleDouble#value} ULP. Results smaller
* than 1 ULP of {@link DoubleDouble#error} will be clamped.
*/
private static final double PRODUCT_TOLERANCE_FACTOR = 1E-15;
/**
* The random number generator to use for the test.
*/
private final Random random = TestUtilities.createRandomNumberGenerator();
/**
* Returns the next {@code double} random value. The scale factor is a power of two
* in order to change only the exponent part of the IEEE representation.
*/
private double nextRandom() {
return random.nextDouble() * 2048 - 1024;
}
/**
* Fetches the next {@code DoubleDouble} random values and store them in the given object.
*/
private void nextRandom(final DoubleDouble dd) {
dd.setToSum(nextRandom(), random.nextDouble());
}
/**
* Returns a {@code BigDecimal} representation of the given {@code DoubleDouble} value.
*/
private static BigDecimal toBigDecimal(final DoubleDouble dd) {
return new BigDecimal(dd.value).add(new BigDecimal(dd.error));
}
/**
* Asserts that the given {@code DoubleDouble} is normalized and has a value equals to the expected one.
* More specifically:
*
* <ul>
* <li>the {@link DoubleDouble#value} is strictly equals to the expected value, and</li>
* <li>the {@link DoubleDouble#error} is not greater than 1 ULP of the above value.</li>
* </ul>
*/
private static void assertNormalizedAndEquals(final double expected, final DoubleDouble actual) {
assertTrue("DoubleDouble is not normalized.", abs(actual.error) <= ulp(actual.value));
assertEquals("Unexpected arithmetic result.", expected, actual.value, STRICT);
}
/**
* Asserts that the result of some operation is equal to the expected value,
* up to a tolerance value determined by the extended arithmetic precision.
*
* @param expected the expected value, computed using {@code BigInteger} arithmetic.
* @param actual the actual value.
* @param ef multiplication factor for the tolerance threshold.
*/
private static void assertExtendedEquals(final BigDecimal expected, final DoubleDouble actual, final double ef) {
final BigDecimal value = toBigDecimal(actual);
final double delta = abs(expected.subtract(value).doubleValue());
final double threshold = max(ulp(actual.error), ulp(actual.value) * ef);
if (!(delta <= threshold)) { // Use ! for catching NaN values.
fail("Arithmetic error:\n" +
" Expected: " + expected + '\n' +
" Actual: " + value + '\n' +
" Difference: " + delta + '\n' +
" Threshold: " + threshold + '\n' +
" Value ULP: " + ulp(actual.value) + '\n');
}
}
/**
* Tests {@link DoubleDouble#normalize()}.
*/
@Test
@DependsOnMethod("testSetToQuickSum")
public void testNormalize() {
final DoubleDouble dd = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
double a = nextRandom();
double b = nextRandom();
if (abs(a) < abs(b)) {
final double t = a;
a = b;
b = t;
}
dd.value = a;
dd.error = b;
dd.normalize();
assertNormalizedAndEquals(a + b, dd);
assertExtendedEquals(new BigDecimal(a).add(new BigDecimal(b)), dd, ADD_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#setToQuickSum(double, double)}.
*/
@Test
public void testSetToQuickSum() {
final DoubleDouble dd = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
double a = nextRandom();
double b = nextRandom();
if (abs(a) < abs(b)) {
final double t = a;
a = b;
b = t;
}
dd.setToQuickSum(a, b);
assertNormalizedAndEquals(a + b, dd);
assertExtendedEquals(new BigDecimal(a).add(new BigDecimal(b)), dd, ADD_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#setToSum(double, double)}.
*/
@Test
public void testSetToSum() {
final DoubleDouble dd = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
final double a = nextRandom();
final double b = nextRandom();
dd.setToSum(a, b);
assertNormalizedAndEquals(a + b, dd);
assertExtendedEquals(new BigDecimal(a).add(new BigDecimal(b)), dd, ADD_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#setToProduct(double, double)}.
*/
@Test
public void testSetToProduct() {
final DoubleDouble dd = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
final double a = nextRandom();
final double b = nextRandom();
dd.setToProduct(a, b);
assertNormalizedAndEquals(a * b, dd);
assertExtendedEquals(new BigDecimal(a).multiply(new BigDecimal(b)), dd, ADD_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#addKahan(double)}.
*/
@Test
public void testAddKahan() {
final DoubleDouble dd = new DoubleDouble(1, 0);
BigDecimal expected = BigDecimal.ONE;
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
final double b = random.nextDouble();
dd.addKahan(b);
expected = expected.add(new BigDecimal(b));
assertExtendedEquals(expected, dd, ADD_TOLERANCE_FACTOR * NUMBER_OF_REPETITIONS);
}
}
/**
* Tests {@link DoubleDouble#add(DoubleDouble)}.
*/
@Test
@DependsOnMethod({"testSetToSum", "testNormalize"})
public void testAdd() {
final DoubleDouble dd = new DoubleDouble();
final DoubleDouble op = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
nextRandom(dd);
nextRandom(op);
final BigDecimal expected = toBigDecimal(dd).add(toBigDecimal(op));
dd.add(op); // Must be after 'expected' computation.
assertExtendedEquals(expected, dd, ADD_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#multiply(DoubleDouble)}.
*/
@Test
@DependsOnMethod({"testSetToProduct", "testNormalize"})
public void testMultiply() {
final DoubleDouble dd = new DoubleDouble();
final DoubleDouble op = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
nextRandom(dd);
nextRandom(op);
final BigDecimal expected = toBigDecimal(dd).multiply(toBigDecimal(op), MathContext.DECIMAL128);
dd.multiply(op); // Must be after 'expected' computation.
assertExtendedEquals(expected, dd, PRODUCT_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#divide(DoubleDouble)}.
*/
@Test
@DependsOnMethod({"testMultiply", "testSetToSum", "testSetToQuickSum"})
public void testDivide() {
final DoubleDouble dd = new DoubleDouble();
final DoubleDouble op = new DoubleDouble();
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
nextRandom(dd);
nextRandom(op);
final BigDecimal expected = toBigDecimal(dd).divide(toBigDecimal(op), MathContext.DECIMAL128);
dd.divide(op); // Must be after 'expected' computation.
assertExtendedEquals(expected, dd, PRODUCT_TOLERANCE_FACTOR);
}
}
/**
* Tests {@link DoubleDouble#ratio_1m_1p()}.
*/
@Test
@DependsOnMethod("testDivide")
public void testRatio_1m_1p() {
final DoubleDouble t = new DoubleDouble(0.25);
t.ratio_1m_1p();
assertEquals((1 - 0.25) / (1 + 0.25), t.doubleValue(), STRICT);
}
/**
* Tests {@link DoubleDouble#sqrt()} first with the square root of 2, then with random values.
* In the {@code sqrt(2)} case:
*
* <ul>
* <li>The error using {@code double} arithmetic is approximately 1E-16.</li>
* <li>The error using double-double arithmetic is expected to be slightly less that 1E-32.</li>
* </ul>
*/
@Test
@DependsOnMethod({"testMultiply", "testDivide"})
public void testSqrt() {
final BigDecimal SQRT2 = new BigDecimal("1.414213562373095048801688724209698");
final DoubleDouble dd = new DoubleDouble(2d);
dd.sqrt();
assertNormalizedAndEquals(sqrt(2), dd);
assertEquals(0, SQRT2.subtract(toBigDecimal(dd)).doubleValue(), 1E-32);
/*
* If we have been able to compute √2, now test with random values.
* Since the range of values is approximately [-1000 … 1000], use
* a tolerance value 1000 time the one that we used for √2.
*/
for (int i=0; i<NUMBER_OF_REPETITIONS; i++) {
nextRandom(dd);
if (dd.value < 0) {
dd.negate();
}
final double value = dd.value;
final double error = dd.error;
dd.square();
dd.sqrt();
dd.subtract(value, error);
assertEquals(0, dd.doubleValue(), 1E-29);
}
dd.clear();
dd.sqrt();
assertTrue(dd.isZero());
}
/**
* Tests the {@link DoubleDouble#series(double...)} method.
*/
@Test
@DependsOnMethod({"testMultiply", "testAdd"})
public void testSeries() {
final DoubleDouble t = new DoubleDouble(2d);
t.series(1, 1./3, 1./9, 1./7, 1./13); // Random coefficients.
assertEquals(1 + 2./3 + 4./9 + 8./7 + 16./13, t.doubleValue(), STRICT);
}
/**
* List of all {@link DoubleDouble#VALUES} as string decimal representations,
* together with some additional values to test.
*/
private static final String[] PREDEFINED_VALUES = {
"0.1",
"0.01",
"0.001",
"0.0001",
"0.00001",
"0.000001",
"0.3048", // Feet to metres
"0.201168", // Link to metres
"0.9144", // Yard to metres
"1.8288", // Fathom to metres
"20.1168", // Chain to metres
"2.54", // Inch to centimetres
"0.9", // Degree to grads
"1.111111111111111111111111111111111", // Grad to degrees
"0.002777777777777777777777777777777778", // 1/360°
"0.0002777777777777777777777777777777778", // Second to degrees
"0.01666666666666666666666666666666667", // Minute to degrees
"0.000004848136811095359935899141023579480", // Arc-second to radians
"0.01745329251994329576923690768488613", // Degrees to radians
"57.2957795130823208767981548141052", // Radians to degrees
"3.14159265358979323846264338327950", // π
"6.28318530717958647692528676655901", // 2π
"1.570796326794896619231321691639751", // π/2
"0.785398163397448309615660845819876", // π/4
"2.356194490192344928846982537459627", // 3π/4
"1.414213562373095048801688724209698" // √2
};
/**
* Verifies the {@link DoubleDouble#VALUES} and {@link DoubleDouble#ERRORS} arrays.
*
* <ul>
* <li>Elements in the {@code VALUES} array shall be sorted in increasing order.</li>
* <li>{@code VALUES} and {@code ERRORS} arrays shall have the same length.</li>
* <li>The arrays do not contains an entry for a value that could be omitted.</li>
* </ul>
*
* @throws ReflectiveOperationException if this test uses wrong field names.
*/
@Test
public void testArraysConsistency() throws ReflectiveOperationException {
Field field = DoubleDouble.class.getDeclaredField("VALUES");
field.setAccessible(true);
final double[] values = (double[]) field.get(null);
assertTrue(ArraysExt.isSorted(values, true));
field = DoubleDouble.class.getDeclaredField("ERRORS");
field.setAccessible(true);
final double[] errors = (double[]) field.get(null);
assertEquals(values.length, errors.length);
for (int i=0; i<values.length; i++) {
final double value = values[i];
final double delta = DecimalFunctions.deltaForDoubleToDecimal(value);
if (delta == errors[i]) {
fail("(value,entry) pair for value " + value + " can be omitted.");
}
}
}
/**
* Tests {@link DoubleDouble#errorForWellKnownValue(double)}.
*/
@Test
@DependsOnMethod("testArraysConsistency")
public void testErrorForWellKnownValue() {
for (final String text : PREDEFINED_VALUES) {
final double value = Double.valueOf(text);
final BigDecimal accurate = new BigDecimal(text);
final BigDecimal approximation = new BigDecimal(value);
final double expected = accurate.subtract(approximation).doubleValue();
assertEquals(text, expected, DoubleDouble.errorForWellKnownValue( value), STRICT);
assertEquals(text, -expected, DoubleDouble.errorForWellKnownValue(-value), STRICT);
assertFalse("There is no point to define an entry for values having no error.", expected == 0);
}
}
/**
* Tests π values using the {@link StrictMath#PI} constant.
* This test method serves two purposes:
*
* <ul>
* <li>Ensure that the results of small arithmetic operations on {@link StrictMath#PI} produce
* numbers that {@link DoubleDouble#errorForWellKnownValue(double)} can find.</li>
* <li>Compare with the values computed by the {@code qd-2.3.14} package (a C/C++ library),
* which is taken as the reference implementation.</li>
* </ul>
*/
@Test
@DependsOnMethod("testErrorForWellKnownValue")
public void testPI() {
assertEquals(1.224646799147353207E-16, DoubleDouble.errorForWellKnownValue(PI ), STRICT);
assertEquals(2.449293598294706414E-16, DoubleDouble.errorForWellKnownValue(PI * 2), STRICT);
assertEquals(6.123233995736766036E-17, DoubleDouble.errorForWellKnownValue(PI / 2), STRICT);
assertEquals(3.061616997868383018E-17, DoubleDouble.errorForWellKnownValue(PI / 4), STRICT);
assertEquals(9.184850993605148436E-17, DoubleDouble.errorForWellKnownValue(PI * (3./4)), STRICT);
assertEquals(9.320078015422868E-23, DoubleDouble.errorForWellKnownValue(PI / (180 * 60 * 60)), STRICT);
// Following is actually an anti-regression test.
assertEquals("toDegrees", -1.9878495670576283E-15, DoubleDouble.errorForWellKnownValue(180 / PI), STRICT);
assertEquals("toDegrees", -1.9878495670576283E-15, DoubleDouble.errorForWellKnownValue(toDegrees(1)), STRICT);
assertEquals("toRadians", 2.9486522708701687E-19, DoubleDouble.errorForWellKnownValue(PI / 180), STRICT);
assertEquals("toRadians", 2.9486522708701687E-19, DoubleDouble.errorForWellKnownValue(toRadians(1)), STRICT);
}
/**
* Tests the {@code DoubleDouble.createFoo()} methods.
*/
@Test
@DependsOnMethod("testErrorForWellKnownValue")
public void testCreate() {
for (int i=0; ; i++) {
final DoubleDouble dd;
switch (i) {
case 0: dd = DoubleDouble.createPi(); break;
case 1: dd = DoubleDouble.createRadiansToDegrees(); break;
case 2: dd = DoubleDouble.createDegreesToRadians(); break;
case 3: dd = DoubleDouble.createSecondsToRadians(); break;
default: return; // Test done.
}
assertEquals(DoubleDouble.errorForWellKnownValue(dd.value), dd.error, STRICT);
}
}
/**
* Tests initialization with a long value.
*/
@Test
public void testLong() {
long value = Long.MAX_VALUE - 10;
DoubleDouble t = new DoubleDouble(value);
assertEquals(-10, t.error, STRICT);
assertEquals(value, t.longValue());
value = Long.MIN_VALUE + 10;
t = new DoubleDouble(value);
assertEquals(10, t.error, STRICT);
assertEquals(value, t.longValue());
}
}