/*
* 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.referencing.operation.matrix;
import java.util.Random;
import Jama.Matrix;
import org.apache.sis.math.Statistics;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.test.TestCase;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.DependsOnMethod;
import org.junit.Test;
import static org.apache.sis.test.Assert.*;
/**
* Base classes of tests for {@link MatrixSIS} implementations.
* This class uses the following {@code Matrices} factory methods:
*
* <ul>
* <li>{@link Matrices#createDiagonal(int, int)} (sometime delegates to {@link Matrices#createIdentity(int)})</li>
* <li>{@link Matrices#create(int, int, double[])}</li>
* <li>{@link Matrices#createZero(int, int)}</li>
* </ul>
*
* So this class is indirectly a test of those factory methods.
* However this class does not test any other {@code Matrices} methods.
*
* <p>This class uses <a href="http://math.nist.gov/javanumerics/jama">JAMA</a> as the reference implementation.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @since 0.4
* @version 0.4
* @module
*/
public abstract strictfp class MatrixTestCase extends TestCase {
/**
* {@code true} for reusing the same sequences of random numbers in every execution of test cases, or
* {@code false} for "truly" random sequences of random numbers. This flag can be set to {@code false}
* for testing purpose, but should be set to {@code true} otherwise for avoiding random test failure.
* This is needed because we want to set {@link #TOLERANCE} to a small value, but it is very difficult
* to guaranteed that a random sequence of numbers will not cause a larger discrepancy.
*
* <p>Note that this flag is set to {@code false} if double-double arithmetic is disabled because in such
* case, the results should be identical to the JAMA results (i.e. equal using a {@link #TOLERANCE} of zero)
* for any sequence of numbers.</p>
*/
protected static final boolean DETERMINIST = !DoubleDouble.DISABLED;
/**
* A constant for any test in this class or a subclass which expect
* a floating point value to be strictly equals to an other value.
*/
static final double STRICT = 0;
/**
* Tolerance factor for comparisons of floating point numbers between SIS and JAMA implementation,
* which is {@value}. Note that the matrix element values used in this class vary between 0 and 100,
* and the {@code StrictMath.ulp(100.0)} value is approximatively 1.4E-14.
*
* {@section How this value is determined}
* Experience (by looking at {@link #statistics}) shows that the differences are usually smaller than 1E-12.
* However when using non-determinist sequence of random values ({@link #DETERMINIST} sets to {@code false}),
* we do have from time-to-time a difference around 1E-9.
*
* Those differences exist because SIS uses double-double arithmetic, while JAMA uses ordinary double.
* To remove that ambiguity, one can temporarily set {@link DoubleDouble#DISABLED} to {@code true},
* in which case the SIS results should be strictly identical to the JAMA ones.
*
* @see SolverTest#TOLERANCE
* @see NonSquareMatrixTest#printStatistics()
*/
protected static final double TOLERANCE = DoubleDouble.DISABLED ? STRICT : 1E-11;
/**
* Number of random matrices to try in arithmetic operation tests.
*/
static final int NUMBER_OF_REPETITIONS = 100;
/**
* The threshold in matrix determinant for attempting to compute the inverse.
* Matrix with a determinant of 0 are not invertible, but we keep a margin for safety.
*/
private static final double DETERMINANT_THRESHOLD = 0.001;
/**
* Statistics about the different between the JAMA and SIS matrix elements, or {@code null}
* if those statistics do not need to be collected. This is used during the test development
* phase for tuning the tolerance threshold.
*
* @see NonSquareMatrixTest#printStatistics()
*/
static final Statistics statistics = verbose ? new Statistics("|SIS - JAMA|") : null;
/**
* Random number generator, created by {@link #initialize(long)} as the first operation of
* any test method which will use random numbers. This random number generator will use a
* fixed seed if {@link #DETERMINIST} is {@code true}, which is the normal case.
*/
private Random random;
/**
* For subclasses only.
*/
MatrixTestCase() {
}
/**
* Initializes the random number generator to the given seed. If {@link #DETERMINIST} is {@code false}
* (which happen only when performing some more extensive tests), then the given seed will be replaced
* by a random one.
*
* @param seed The initial seed.
*/
final void initialize(final long seed) {
random = DETERMINIST ? new Random(seed) : TestUtilities.createRandomNumberGenerator();
}
/**
* Computes a random size for the next matrix to create. This method is overridden
* only by subclasses that test matrix implementations supporting arbitrary sizes.
*
* @param random The random number generator to use for computing a random matrix size.
*/
void prepareNewMatrixSize(final Random random) {
}
/** Returns the number of rows of the matrix being tested. */ abstract int getNumRow();
/** Returns the number of columns of the matrix being tested. */ abstract int getNumCol();
/**
* Validates the given matrix.
* The default implementation verifies only the matrix size. Subclasses should override this method
* for additional checks, typically ensuring that it is an instance of the expected class.
*/
void validate(final MatrixSIS matrix) {
assertEquals("numRow", getNumRow(), matrix.getNumRow());
assertEquals("numCol", getNumCol(), matrix.getNumCol());
}
/**
* Verifies that the SIS matrix is equals to the JAMA one, up to the given tolerance value.
*
* @param expected The JAMA matrix used as a reference implementation.
* @param actual The SIS matrix to compare to JAMA.
* @param tolerance The tolerance threshold, usually either {@link #STRICT} or {@link #TOLERANCE}.
*/
static void assertEqualsJAMA(final Matrix expected, final MatrixSIS actual, final double tolerance) {
final int numRow = actual.getNumRow();
final int numCol = actual.getNumCol();
assertEquals("numRow", expected.getRowDimension(), numRow);
assertEquals("numCol", expected.getColumnDimension(), numCol);
final String name = actual.getClass().getSimpleName();
for (int j=0; j<numRow; j++) {
for (int i=0; i<numCol; i++) {
final double e = expected.get(j,i);
final double a = actual.getElement(j,i);
assertEquals(name, e, a, tolerance);
assertEquals(name, e, actual.getNumber(j,i).doubleValue(), tolerance);
if (tolerance != STRICT && statistics != null) {
synchronized (statistics) {
statistics.accept(StrictMath.abs(e - a));
}
}
}
}
}
/**
* Asserts that the given matrix is equals to the given expected values, up to the given tolerance threshold.
* This method compares the elements values in two slightly redundant ways.
*/
static void assertEqualsElements(final double[] expected, final int numRow, final int numCol,
final MatrixSIS actual, final double tolerance)
{
assertEquals("numRow", numRow, actual.getNumRow());
assertEquals("numCol", numCol, actual.getNumCol());
assertArrayEquals(expected, actual.getElements(), tolerance); // First because more informative in case of failure.
assertTrue(Matrices.create(numRow, numCol, expected).equals(actual, tolerance));
}
/**
* Creates an array of the given length filled with random values. All random values are between 0 inclusive
* and 100 exclusive. This method never write negative values. Consequently, any strictly negative value set
* by the test method is guaranteed to be different than all original values in the returned array.
*/
final double[] createRandomPositiveValues(final int length) {
final double[] elements = new double[length];
for (int k=0; k<length; k++) {
elements[k] = random.nextDouble() * 100;
}
return elements;
}
/**
* Creates a matrix initialized with a random array of element values,
* then tests the {@link MatrixSIS#getElement(int, int)} method for each element.
* This test will use {@link Matrices#create(int, int, double[])} for creating the matrix.
*
* <p>If this test fails, then all other tests in this class will be skipped since it would
* not be possible to verify the result of any matrix operation.</p>
*/
@Test
public void testGetElements() {
initialize(3812872376135347328L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final double[] elements = createRandomPositiveValues(numRow * numCol);
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
validate(matrix);
/*
* The JAMA constructor uses column-major array (FORTRAN convention), while SIS uses
* row-major array. So we have to transpose the JAMA matrix after construction.
*/
assertEqualsJAMA(new Matrix(elements, numCol).transpose(), matrix, STRICT);
assertArrayEquals("getElements", elements, matrix.getElements(), STRICT);
}
/**
* Tests {@link MatrixSIS#getElement(int, int)} and {@link MatrixSIS#setElement(int, int, double)}.
* This test sets random values in elements at random index, and compares with a JAMA matrix taken
* as the reference implementation.
*/
@Test
@DependsOnMethod("testGetElements")
public void testSetElement() {
initialize(-8079924100564483073L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final MatrixSIS matrix = Matrices.createZero(numRow, numCol);
validate(matrix);
final Matrix reference = new Matrix(numRow, numCol);
/*
* End of initialization - now perform the actual test.
*/
assertEqualsJAMA(reference, matrix, STRICT);
for (int k=0; k<50; k++) {
final int j = random.nextInt(numRow);
final int i = random.nextInt(numCol);
final double e = random.nextDouble() * 100;
reference.set(j, i, e);
matrix.setElement(j, i, e);
assertEqualsJAMA(reference, matrix, STRICT);
}
}
/**
* Tests {@link MatrixSIS#isIdentity()}. This method will first invoke {@link Matrices#createDiagonal(int, int)}
* and ensure that the result contains 1 on the diagonal and 0 elsewhere.
*
* <p>This method will opportunistically tests {@link MatrixSIS#isAffine()}. The two methods are related
* since {@code isIdentity()} delegates part of its work to {@code isAffine()}.</p>
*/
@Test
@DependsOnMethod("testSetElement")
public void testIsIdentity() {
initialize(6173145457052452823L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final MatrixSIS matrix = Matrices.createDiagonal(numRow, numCol);
validate(matrix);
/*
* End of initialization - now perform the actual test.
*/
assertEquals("isAffine", numRow == numCol, matrix.isAffine());
assertEquals("isIdentity", numRow == numCol, matrix.isIdentity());
for (int j=0; j<numRow; j++) {
for (int i=0; i<numCol; i++) {
final double element = matrix.getElement(j,i);
assertEquals((i == j) ? 1 : 0, element, STRICT);
matrix.setElement(j, i, random.nextDouble() - 1.1);
assertEquals("isAffine", (numRow == numCol) && (j != numRow-1), matrix.isAffine());
assertFalse("isIdentity", matrix.isIdentity());
matrix.setElement(j, i, element);
}
}
assertEquals("isAffine", numRow == numCol, matrix.isAffine());
assertEquals("isIdentity", numRow == numCol, matrix.isIdentity());
}
/**
* Tests {@link MatrixSIS#clone()}, {@link MatrixSIS#equals(Object)} and {@link MatrixSIS#hashCode()}.
*/
@Test
@DependsOnMethod("testSetElement")
public void testCloneEquals() {
initialize(-4572234104840706847L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final double[] elements = createRandomPositiveValues(numRow * numCol);
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
final MatrixSIS clone = matrix.clone();
validate(matrix);
validate(clone);
assertNotSame("clone", matrix, clone);
assertEquals("equals", matrix, clone);
assertEquals("hashCode", matrix.hashCode(), clone.hashCode());
for (int j=0; j<numRow; j++) {
for (int i=0; i<numCol; i++) {
final double element = clone.getElement(j,i);
clone.setElement(j, i, random.nextDouble() - 2); // Negative value is guaranteed to be different.
assertFalse(matrix.equals(clone));
assertFalse(clone.equals(matrix));
clone.setElement(j, i, element);
}
}
assertEquals("equals", matrix, clone);
}
/**
* Tests {@link MatrixSIS#transpose()}.
*/
@Test
@DependsOnMethod("testGetElements")
public void testTranspose() {
initialize(585037875560696050L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final double[] elements = createRandomPositiveValues(numRow * numCol);
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
validate(matrix);
/*
* The JAMA constructor uses column-major array (FORTRAN convention) while SIS uses row-major
* array. In other words, the JAMA matrix is already transposed from the SIS point of view.
*/
matrix.transpose();
assertEqualsJAMA(new Matrix(elements, numCol), matrix, STRICT);
}
/**
* Tests {@link MatrixSIS#normalizeColumns()}.
*/
@Test
@DependsOnMethod("testGetElements")
public void testNormalizeColumns() {
initialize(1549772118153010333L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final double[] elements = createRandomPositiveValues(numRow * numCol);
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
validate(matrix);
matrix.normalizeColumns();
for (int i=0; i<numCol; i++) {
double m = 0;
for (int j=0; j<numRow; j++) {
final double e = matrix.getElement(j, i);
m += e*e;
}
m = StrictMath.sqrt(m);
assertEquals(1, m, 1E-12);
}
}
/**
* Tests {@link MatrixSIS#multiply(Matrix)}.
*/
@Test
@DependsOnMethod("testGetElements")
public void testMultiply() {
initialize(2478887638739725150L);
for (int n=0; n<NUMBER_OF_REPETITIONS; n++) {
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
double[] elements = createRandomPositiveValues(numRow * numCol);
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
final Matrix reference = new Matrix(elements, numCol).transpose();
/*
* Computes new random value for the argument. We mix positive and negative values,
* but with more positive values than negative ones in order to reduce the chances
* to have a product of zero for an element.
*/
final int nx = random.nextInt(8) + 1;
elements = new double[numCol * nx];
for (int k=0; k<elements.length; k++) {
elements[k] = 8 - random.nextDouble() * 10;
}
final Matrix referenceArg = new Matrix(elements, nx).transpose();
final MatrixSIS matrixArg = Matrices.create(numCol, nx, elements);
/*
* Performs the multiplication and compare.
*/
final Matrix referenceResult = reference.times(referenceArg);
final MatrixSIS matrixResult = matrix.multiply(matrixArg);
assertEqualsJAMA(referenceResult, matrixResult, TOLERANCE);
}
}
/**
* Tests {@link MatrixSIS#solve(Matrix)}.
*
* @throws NoninvertibleMatrixException Should never happen.
*/
@Test
@DependsOnMethod("testMultiply")
public void testSolve() throws NoninvertibleMatrixException {
initialize(2108474073121762243L);
for (int n=0; n<NUMBER_OF_REPETITIONS; n++) {
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
double[] elements = createRandomPositiveValues(numRow * numCol);
final Matrix reference = new Matrix(elements, numCol).transpose();
if (!(reference.det() >= DETERMINANT_THRESHOLD)) {
continue; // To close to a singular matrix - search an other one.
}
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
/*
* Computes new random value for the argument. We mix positive and negative values,
* but with more positive values than negative ones in order to reduce the chances
* to have a product of zero for an element.
*/
final int nx = random.nextInt(8) + 1;
elements = new double[numCol * nx];
for (int k=0; k<elements.length; k++) {
elements[k] = 8 - random.nextDouble() * 10;
}
final Matrix referenceArg = new Matrix(elements, nx).transpose();
final MatrixSIS matrixArg = Matrices.create(numCol, nx, elements);
/*
* Performs the operation and compare.
*/
final Matrix referenceResult = reference.solve(referenceArg);
final MatrixSIS matrixResult = matrix.solve(matrixArg);
assertEqualsJAMA(referenceResult, matrixResult, SolverTest.TOLERANCE);
}
}
/**
* Tests {@link MatrixSIS#inverse()}.
* SIS implements the {@code inverse} operation as a special case of the {@code solve} operation.
*
* @throws NoninvertibleMatrixException Should never happen.
*/
@Test
@DependsOnMethod("testSolve")
public void testInverse() throws NoninvertibleMatrixException {
initialize(-9063921123024549789L);
for (int n=0; n<NUMBER_OF_REPETITIONS; n++) {
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final double[] elements = createRandomPositiveValues(numRow * numCol);
final Matrix reference = new Matrix(elements, numCol).transpose();
if (!(reference.det() >= DETERMINANT_THRESHOLD)) {
continue; // To close to a singular matrix - search an other one.
}
final MatrixSIS matrix = Matrices.create(numRow, numCol, elements);
assertEqualsJAMA(reference.inverse(), matrix.inverse(), TOLERANCE);
}
}
/**
* Tests matrix serialization.
*/
@Test
public void testSerialization() {
initialize(-3232759118744327281L);
prepareNewMatrixSize(random);
final int numRow = getNumRow();
final int numCol = getNumCol();
final MatrixSIS matrix = Matrices.create(numRow, numCol, createRandomPositiveValues(numRow * numCol));
assertNotSame(matrix, assertSerializedEquals(matrix));
}
}