/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, Geotools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.referencing.operation.transform;

// J2SE dependencies
import java.awt.geom.AffineTransform;
import java.util.Arrays;
import java.util.Random;

// JUnit dependencies
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

// OpenGIS dependencies
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.TransformException;
import org.opengis.geometry.DirectPosition;

// Geotools dependencies
import org.geotools.geometry.DirectPosition1D;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.DefaultMathTransformFactory;
import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.referencing.operation.matrix.XMatrix;


/**
 * Tests various classes of {@link MathTransform}, including {@link ConcatenatedTransform}.
 * Actually, there is many {@link ConcatenatedTransform}, each optimized for special cases.
 * This test tries to test a wide range of subclasses.
 *
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/test/java/org/geotools/referencing/operation/transform/MathTransformTest.java $
 * @version $Id: MathTransformTest.java 25262 2007-04-23 21:11:16Z desruisseaux $
 * @author Martin Desruisseaux
 */
public final class MathTransformTest extends TestCase {
    /**
     * Runs the tests with the textual test runner.
     */
    public static void main(String args[]) {
        junit.textui.TestRunner.run(suite());
    }

    /**
     * Returns the test suite.
     */
    public static Test suite() {
        return new TestSuite(MathTransformTest.class);
    }

    /**
     * Constructs a test case with the given name.
     */
    public MathTransformTest(final String name) {
        super(name);
    }

    /**
     * Random numbers generator.
     */
    private Random random;

    /**
     * The default math transform factory.
     */
    private DefaultMathTransformFactory factory;

    /**
     * Expected accuracy for tests on JTS objects.
     */
    private static final double ACCURACY = 0.03;

    /**
     * Set up common objects used for all tests.
     */
    protected void setUp() throws Exception {
        super.setUp();
        random  = new Random(-3531834320875149028L);
        factory = new DefaultMathTransformFactory();
    }

    /**
     * Tests a transformation on a {@link DirectPosition} object.
     */
    public void testDirectPositionTransform() throws FactoryException, TransformException {
        CoordinateReferenceSystem crs = ReferencingFactoryFinder.getCRSFactory(null).createFromWKT(
                "PROJCS[\"NAD_1983_UTM_Zone_10N\",\n"                      +
                "  GEOGCS[\"GCS_North_American_1983\",\n"                  +
                "    DATUM[\"D_North_American_1983\",\n"                   +
                "      TOWGS84[0,0,0,0,0,0,0]\n,"                          +
                "      SPHEROID[\"GRS_1980\", 6378137, 298.257222101]],\n" +
                "    PRIMEM[\"Greenwich\",0],\n"                           +
                "    UNIT[\"Degree\", 0.017453292519943295]],\n"           +
                "  PROJECTION[\"Transverse_Mercator\"],\n"                 +
                "  PARAMETER[\"False_Easting\",500000],\n"                 +
                "  PARAMETER[\"False_Northing\",0],\n"                     +
                "  PARAMETER[\"Central_Meridian\",-123],\n"                +
                "  PARAMETER[\"Scale_Factor\",0.9996],\n"                  +
                "  PARAMETER[\"Latitude_Of_Origin\",0],\n"                 +
                "  UNIT[\"Meter\",1]]");
        
        MathTransform t = ReferencingFactoryFinder.getCoordinateOperationFactory(null).
                createOperation(DefaultGeographicCRS.WGS84, crs).getMathTransform();
        DirectPosition position = new GeneralDirectPosition(-123, 55);
        position = t.          transform(position, position);
        position = t.inverse().transform(position, position);
        assertEquals(-123, position.getOrdinate(0), 1E-6);
        assertEquals(  55, position.getOrdinate(1), 1E-6);
    }

    /**
     * Tests the {@link ProjectiveTransform} implementation.
     */
    public void testAffineTransform() throws FactoryException, TransformException {
        for (int pass=0; pass<10; pass++) {
            final AffineTransform transform = new AffineTransform();
            transform.rotate(Math.PI*random.nextDouble(), 100*random.nextDouble(), 100*random.nextDouble());
            transform.scale       (2*random.nextDouble(),   2*random.nextDouble());
            transform.shear       (2*random.nextDouble(),   2*random.nextDouble());
            transform.translate (100*random.nextDouble(), 100*random.nextDouble());
            compareTransforms("AffineTransform", new MathTransform[] {
                new ProjectiveTransform(new GeneralMatrix(transform)),
                new AffineTransform2D(transform)
            });
        }
        
        AffineTransform at = new AffineTransform(23.157082917424454,  0.0, 3220.1613428464952,
                                                 0.0, -23.157082917424457, 1394.4593259871676);
        MathTransform mt = factory.createAffineTransform(new GeneralMatrix(at));
        
        final double[] points = new double[] {
                -129.992589135802,    55.9226692948365, -129.987254340541,
                  55.9249676996729, -129.982715772093,    55.9308988434656, 
                -129.989772198265,    55.9289277997662, -129.992589135802, 55.9226692948365
        };
        final double[] transformedPoints = new double[points.length];
        mt.transform(points, 0, transformedPoints, 0, points.length/2);
        at.transform(points, 0, points, 0, points.length/2);
        for (int i=0; i<transformedPoints.length; i++) {
            assertEquals(points[i], transformedPoints[i], ACCURACY);
        }
    }
     
    /**
     * Test various linear transformations. We test for many differents dimensions.
     * The factory class should have created specialized classes for 1D and 2D cases.
     * This test is used in order to ensure that specialized case produces the same
     * results than general cases.
     */
    public void testSubAffineTransform() throws FactoryException, TransformException {
        for (int pass=0; pass<5; pass++) {
            /*
             * Construct the reference matrix.
             */
            final int dimension = 10;
            final GeneralMatrix matrix = new GeneralMatrix(dimension+1, dimension+1);
            for (int i=0; i<dimension; i++) {
                matrix.setElement(i,i,         400*Math.random()-200);
                matrix.setElement(i,dimension, 400*Math.random()-200);
            }
            assertTrue(matrix.isAffine());
            /*
             * Construct all math transforms.
             */
            final MathTransform[] transforms = new MathTransform[dimension];
            for (int i=1; i<=dimension; i++) {
                final GeneralMatrix sub = new GeneralMatrix(i+1, i+1);
                matrix.copySubMatrix(0, 0, i, i, 0, 0, sub);         // Scale terms
                matrix.copySubMatrix(0, dimension, i, 1, 0, i, sub); // Translation terms
                final MathTransform transform = transforms[i-1] = factory.createAffineTransform(sub);
                assertTrue  (sub.isAffine());
                assertEquals(sub, new GeneralMatrix(((LinearTransform) transform).getMatrix()));
                assertInterfaced(transform);
                assertTrue(i ==  transform.getSourceDimensions());
            }
            /*
             * Check transformations and the inverse transformations.
             */
            assertTrue("MathTransform1D", transforms[0] instanceof MathTransform1D);
            assertTrue("MathTransform2D", transforms[1] instanceof MathTransform2D);
            assertEquals(matrix, ((LinearTransform) transforms[dimension-1]).getMatrix());
            compareTransforms("SubAffineTransform", transforms);
            for (int i=0; i<transforms.length; i++) {
                transforms[i] = transforms[i].inverse();
            }
            compareTransforms("SubAffineTransform.inverse", transforms);
        }
    }

    /**
     * Test the concatenation of linear transformations. Concatenation of linear
     * transformations should involve only matrix multiplication.  However, this
     * test will also create {@link ConcatenatedTransform} objects in order to
     * compare their results.
     */
    public void testAffineTransformConcatenation() throws FactoryException, TransformException {
        final MathTransform[] transforms = new MathTransform[2];
        final int numDim = 4;
        final int numPts = 200;
        for (int pass=0; pass<100; pass++) {
            final int     dimSource = random.nextInt(numDim)+1;
            final int     dimTarget = random.nextInt(numDim)+1;
            final int     dimInterm = random.nextInt(numDim)+1;
            final Matrix    matrix1 = getRandomMatrix(dimSource, dimInterm);
            final Matrix    matrix2 = getRandomMatrix(dimInterm, dimTarget);
            final MathTransform tr1 = factory.createAffineTransform(matrix1);
            final MathTransform tr2 = factory.createAffineTransform(matrix2);
            final double[] sourcePt = new double[dimSource * numPts];
            final double[] intermPt = new double[dimInterm * numPts];
            final double[] targetPt = new double[dimTarget * numPts];
            final double[]  compare = new double[dimTarget * numPts];
            final double[]    delta = new double[dimTarget];
            for (int i=0; i<numPts; i++) {
                sourcePt[i] = 100*random.nextDouble()-50;
            }
            tr1.transform(sourcePt, 0, intermPt, 0, numPts);
            tr2.transform(intermPt, 0, targetPt, 0, numPts);
            Arrays.fill(delta, 1E-6);
            /*
             * Create two set of concatenated transform: the first one computed from matrix
             * multiplication;   the second one is forced to a ConcatenatedTransform object
             * for testing purpose.
             */
            transforms[0] = factory.createConcatenatedTransform  (tr1, tr2);
            transforms[1] = ConcatenatedTransform.createConcatenatedTransform(tr1, tr2);
            assertTrue (transforms[0] instanceof LinearTransform);
            assertFalse(transforms[1] instanceof LinearTransform);
            for (int i=0; i<transforms.length; i++) {
                final MathTransform transform = transforms[i];
                assertInterfaced(transform);
                assertEquals("dimSource["+i+']', dimSource, transform.getSourceDimensions());
                assertEquals("dimTarget["+i+']', dimTarget, transform.getTargetDimensions());
                transform.transform(sourcePt, 0, compare, 0, numPts);
                String name = "transform["+i+"]("+dimSource+" -> "+dimInterm+" -> "+dimTarget+')';
                assertPointsEqual(name, targetPt, compare, delta);
            }
        }
    }

    /**
     * Make sure that linear transformation preserve NaN values.
     * This is required for {@link org.geotools.coverage.Category}.
     */
    public void testNaN() throws FactoryException, TransformException {
        final XMatrix matrix = MatrixFactory.create(2);
        matrix.setElement(0,0,0);
        for (int i=0; i<200; i++) {
            final int rawBits = 0x7FC00000 + random.nextInt(100);
            final float value = Float.intBitsToFloat(rawBits);
            assertTrue("isNaN", Float.isNaN(value));
            matrix.setElement(0,1, value);
            final MathTransform1D tr = (MathTransform1D) factory.createAffineTransform(matrix);
            assertTrue("ConstantTransform1D", tr instanceof ConstantTransform1D);
            final float compare = (float) tr.transform(0);
            assertEquals("rawBits", rawBits, Float.floatToRawIntBits(compare));
        }
    }

    /**
     * Test the {@link ExponentialTransform1D} and {@link LogarithmicTransform1D} classes
     * using simple know values.
     */
    public void testLogarithmicTransform() throws FactoryException, TransformException {
        final double[] POWER_2  = {0, 1, 2, 3,  4,  5,  6,   7,   8,   9,   10,   11,   12,   13};
        final double[] VALUE_2  = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192};
        final double[] POWER_10 = {  -5,   -4,   -3,   -2,   -1, 0,   +1,   +2,   +3,   +4,   +5};
        final double[] VALUE_10 = {1E-5, 1E-4, 1E-3, 1E-2, 1E-1, 1, 1E+1, 1E+2, 1E+3, 1E+4, 1E+5};
        compareTransform1D("Exponential",  2, POWER_2,  VALUE_2);
        compareTransform1D("Exponential", 10, POWER_10, VALUE_10);
        compareTransform1D("Logarithmic",  2, VALUE_2,  POWER_2);
        compareTransform1D("Logarithmic", 10, VALUE_10, POWER_10);
    }

    /**
     * Test the concatenation of {@link LinearTransform1D}, {@link ExponentialTransform1D}
     * and {@link LogarithmicTransform1D}.
     */
    public void testLogarithmicAndExponentialConcatenation() throws FactoryException, TransformException {
        final int numPts = 200;
        final double[] sourcePt = new double[numPts];
        final double[] targetPt = new double[numPts];
        final double[]  compare = new double[numPts];
        final double[]    delta = new double[numPts];
        for (int pass=0; pass<100; pass++) {
            for (int i=0; i<numPts; i++) {
                sourcePt[i] = 20*random.nextDouble()+0.1;
            }
            MathTransform ctr = getRandomTransform1D();
            ctr.transform(sourcePt, 0, targetPt,  0, numPts);
            for (int i=random.nextInt(2)+1; --i>=0;) {
                final MathTransform1D step = getRandomTransform1D();
                ctr = (MathTransform1D) factory.createConcatenatedTransform(ctr, step);
                step.transform(targetPt, 0, targetPt, 0, numPts);
            }
            ctr.transform(sourcePt, 0, compare,  0, numPts);
            final double EPS = Math.pow(10, -5+countNonlinear(ctr));
            for (int i=0; i<numPts; i++) {
                delta[i] = Math.max(1E-9, Math.abs(targetPt[i]*EPS));
                if (targetPt[i] >= +1E+300) targetPt[i] = Double.POSITIVE_INFINITY;
                if (targetPt[i] <= -1E+300) targetPt[i] = Double.NEGATIVE_INFINITY;
            }
            assertPointsEqual("transform["+ctr+']', targetPt, compare, delta);
            /*
             * Test the inverse transform. It is difficult to get back the exact original value,
             * since expressions like  'pow(b1, pow(b2,x))'  tend to overflow or underflow very
             * fast. We are very tolerant for this test because of this (exponential expression
             * give exponential error). The 'testLogarithmicTransform' method tested the inverse
             * transform in a more sever way.
             */
            try {
                final MathTransform inv = ctr.inverse();
                Arrays.fill(delta, Math.pow(10, countNonlinear(inv)));
                inv.transform(targetPt, 0, compare,  0, numPts);
                for (int i=0; i<numPts; i++) {
                    if (!isReal(targetPt[i]) || !isReal(compare[i])) {
                        // Ignore all input points that produced NaN or infinity
                        // A succession of 2 "Exponentional" operation produces
                        // infinities pretty fast, so ignore it.
                        sourcePt[i] = Double.NaN;
                    }
                }
                assertPointsEqual("inverse["+inv+']', sourcePt, compare, delta);
            } catch (NoninvertibleTransformException exception) {
                // Some transforms may not be invertible. Ignore...
            }
        }
    }








    ///////////////////////////////////////////////////////////////////////////////////
    ////////////                                                           ////////////
    ////////////               U T I L I T Y   M E T H O D S               ////////////
    ////////////                                                           ////////////
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * Returns a random matrix.
     *
     * @param dimSource Number of dimension for input points.
     * @param dimTarget Number of dimension for outout points.
     */
    private Matrix getRandomMatrix(final int dimSource, final int dimTarget) {
        final XMatrix matrix = MatrixFactory.create(dimTarget+1, dimSource+1);
        for (int j=0; j<dimTarget; j++) { // Don't touch to the last row!
            for (int i=0; i<=dimSource; i++) {
                matrix.setElement(j,i, 10*random.nextDouble()-5);
            }
            if (j <= dimSource) {
                matrix.setElement(j,j, 40*random.nextDouble()+10);
            }
            matrix.setElement(j,dimSource, 80*random.nextDouble()-40);
        }
        if (dimSource == dimTarget) {
            assertTrue("Affine", matrix.isAffine());
        }
        return matrix;
    }

    /**
     * Gets a random one-dimensional transform.
     */
    private MathTransform1D getRandomTransform1D() throws FactoryException {
        final String[] candidates = {
            "Logarithmic",
            "Exponential",
            "Affine"
        };
        final String classification = candidates[random.nextInt(candidates.length)];
        final ParameterValueGroup parameters = factory.getDefaultParameters(classification);
        if (classification.equalsIgnoreCase("Affine")) {
            parameters.parameter("num_row").setValue(2);
            parameters.parameter("num_col").setValue(2);
            parameters.parameter("elt_0_0").setValue(random.nextDouble()*2+0.1); // scale
            parameters.parameter("elt_0_1").setValue(random.nextDouble()*1 - 2); // offset
        } else {
            parameters.parameter("base").setValue(random.nextDouble()*4 + 0.1);
        }
        return (MathTransform1D) factory.createParameterizedTransform(parameters);
    }

    /**
     * Compare the transformation performed by many math transforms. If some transforms
     * don't have the same number of input or output dimension, only the first input or
     * output dimensions will be taken in account.
     *
     * @throws TransformException if a transformation failed.
     */
    private void compareTransforms(final String name, final MathTransform[] transforms)
            throws TransformException
    {
        /*
         * Initialisation...
         */
        final GeneralDirectPosition[] sources = new GeneralDirectPosition[transforms.length];
        final GeneralDirectPosition[] targets = new GeneralDirectPosition[transforms.length];
        int maxDimSource = 0;
        int maxDimTarget = 0;
        for (int i=0; i<transforms.length; i++) {
            final int dimSource = transforms[i].getSourceDimensions();
            final int dimTarget = transforms[i].getTargetDimensions();
            if (dimSource > maxDimSource) maxDimSource = dimSource;
            if (dimTarget > maxDimTarget) maxDimTarget = dimTarget;
            sources[i] = new GeneralDirectPosition(dimSource);
            targets[i] = new GeneralDirectPosition(dimTarget);
        }
        /*
         * Test with an arbitrary number of randoms points.
         */
        for (int pass=0; pass<200; pass++) {
            for (int j=0; j<maxDimSource; j++) {
                final double ord = 100*random.nextDouble();
                for (int i=0; i<sources.length; i++) {
                    final GeneralDirectPosition source = sources[i];
                    if (j < source.ordinates.length) {
                        source.ordinates[j] = ord;
                    }
                }
            }
            for (int j=0; j<transforms.length; j++) {
                assertSame(transforms[j].transform(sources[j], targets[j]), targets[j]);
            }
            /*
             * Compare all target points.
             */
            final StringBuffer buffer = new StringBuffer(name);
            buffer.append(": Compare transform[");
            final int lengthJ = buffer.length();
            for (int j=0; j<targets.length; j++) {
                buffer.setLength(lengthJ);
                buffer.append(j);
                buffer.append("] with [");
                final int lengthI = buffer.length();
                final GeneralDirectPosition targetJ = targets[j];
                for (int i=j+1; i<targets.length; i++) {
                    buffer.setLength(lengthI);
                    buffer.append(i);
                    buffer.append(']');
                    final String label = buffer.toString();
                    final GeneralDirectPosition targetI = targets[i];
                    assertTrue(targetJ.ordinates != targetI.ordinates);
                    for (int k=Math.min(targetJ.ordinates.length, targetI.ordinates.length); --k>=0;) {
                        assertEquals(label, targetJ.ordinates[k], targetI.ordinates[k], 1E-6);
                    }
                }
            }
        }
    }

     /**
      * Compare the result of "Logarithmic" or "Exponential" transform with the expected results.
      *
      * @param classification The identifier name (e.g. "Exponential" or "Logarithmic").
      * @param base           The value for the "base" parameter.
      * @param input          Array of input values.
      * @param expected       Array of expected output values.
      */
    private void compareTransform1D(final String   classification,
                                    final double   base,
                                    final double[] input,
                                    final double[] expected)
            throws FactoryException, TransformException
    {
        assertEquals(input.length, expected.length);
        final ParameterValueGroup parameters = factory.getDefaultParameters(classification);
        parameters.parameter("base").setValue(base);
        final MathTransform1D direct =
             (MathTransform1D) factory.createParameterizedTransform(parameters);
        final MathTransform1D inverse = (MathTransform1D) direct.inverse();
        final DirectPosition1D point = new DirectPosition1D();
        for (int i=0; i<expected.length; i++) {
            final double x = input[i];
            final double y = direct.transform(x);
            assertEquals("transform[x="+x+']', expected[i], y,          1E-6);
            assertEquals("inverse  [y="+y+']', x, inverse.transform(y), 1E-6);
            point.setOrdinate(0, x);
            assertSame(direct.transform(point, point), point);
            assertEquals(y, point.getOrdinate(0), 1E-9);
        }
    }

    /**
     * Count the number of non-linear steps in a {@link MathTransform}.
     */
    private static int countNonlinear(final MathTransform transform) {
        if ((transform instanceof ExponentialTransform1D) ||
            (transform instanceof LogarithmicTransform1D))
        {
            return 1;
        }
        if (transform instanceof ConcatenatedTransform) {
            final ConcatenatedTransform ct = (ConcatenatedTransform) transform;
            return countNonlinear(ct.transform1) +
                   countNonlinear(ct.transform2);
        }
        return 0;
    }








    ///////////////////////////////////////////////////////////////////////////////////
    ////////////                                                           ////////////
    ////////////             A S S E R T I O N   M E T H O D S             ////////////
    ////////////                                                           ////////////
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * Returns {@code true} if the specified number is real (neither NaN or infinite).
     */
    private static boolean isReal(final double value) {
        return !Double.isNaN(value) && !Double.isInfinite(value);
    }

    /**
     * Verify that the specified transform implements {@link MathTransform1D}
     * or {@link MathTransform2D} as needed.
     *
     * @param transform The transform to test.
     */
    private static void assertInterfaced(final MathTransform transform) {
        if (transform instanceof LinearTransform) {
            final Matrix matrix = ((LinearTransform) transform).getMatrix();
            if (!((XMatrix) matrix).isAffine()) {
                // Special case: Non-affine transforms not yet declared as a 1D or 2D transform.
                return;
            }
        }
        int dim = transform.getSourceDimensions();
        if (transform.getTargetDimensions() != dim) {
            dim = 0;
        }
        assertTrue("MathTransform1D", (dim==1) == (transform instanceof MathTransform1D));
        assertTrue("MathTransform2D", (dim==2) == (transform instanceof MathTransform2D));
    }

    /**
     * Compare two arrays of points.
     *
     * @param name      The name of the comparaison to be performed.
     * @param expected  The expected array of points.
     * @param actual    The actual array of points.
     * @param delta     The maximal difference tolerated in comparaisons for each dimension.
     *                  This array length must be equal to coordinate dimension (usually 1, 2 or 3).
     */
    private static void assertPointsEqual(final String   name,
                                          final double[] expected,
                                          final double[] actual,
                                          final double[] delta)
    {
        final int dimension = delta.length;
        final int stop = Math.min(expected.length, actual.length)/dimension * dimension;
        assertEquals("Array length for expected points", stop, expected.length);
        assertEquals("Array length for actual points",   stop,   actual.length);
        final StringBuffer buffer = new StringBuffer(name);
        buffer.append(": point[");
        final int start = buffer.length();
        for (int i=0; i<stop; i++) {
            buffer.setLength(start);
            buffer.append(i/dimension);
            buffer.append(", dimension ");
            buffer.append(i % dimension);
            buffer.append(" of ");
            buffer.append(dimension);
            buffer.append(']');
            if (isReal(expected[i])) {
                // The "two steps" method in ConcatenatedTransformTest sometime produces
                // random NaN numbers. This "two steps" is used only for comparaison purpose;
                // the "real" (tested) method work better.
                assertEquals(buffer.toString(), expected[i], actual[i], delta[i % dimension]);
            }
        }
    }
}
