Add benchmark for selecting the best implementation strategy, and document the result analysis.
git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK8@1753650 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
index 0c17bf6..8d802f5 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
@@ -51,6 +51,14 @@
*
* Note that since this boolean is static final, the compiler should exclude the code in the branch that is never
* executed (no need to comment-out that code).
+ *
+ *
+ * <p><b>BENCHMARK AND ANALYSIS:</b>
+ * as of July 2016, benchmarking shows no benefit in using trigonometric identities for {@code EqualAreaProjection}
+ * (contrarily to {@link ConformalProjection} where we did measured a benefit). This may be because in this class,
+ * the series expansion is unconditionally followed by iterative method in order to reach the centimetric precision.
+ * We observe that the original series expansion allows convergence in only one iteration, while the formulas using
+ * trigonometric identifies often requires two iterations. Consequently we disallow those modifications for now.</p>
*/
private static final boolean ALLOW_TRIGONOMETRIC_IDENTITIES = false;
@@ -212,9 +220,9 @@
final double t4β = 0.5 - sin2_β; // = sin(4β) / ( 4⋅sin(2β))
final double t8β = (cos2_β - sin2_β)*(cos2_β*cos2_β - cos2_β + 1./8); // = sin(8β) / (32⋅sin(2β))
- assert identityEquals(t2β, sin(2*β) / ( 2 ));
- assert identityEquals(t4β, sin(4*β) / ( 8 * t2β));
- assert identityEquals(t8β, sin(8*β) / (64 * t2β));
+ assert ConformalProjection.identityEquals(t2β, sin(2*β) / ( 2 ));
+ assert ConformalProjection.identityEquals(t4β, sin(4*β) / ( 8 * t2β));
+ assert ConformalProjection.identityEquals(t8β, sin(8*β) / (64 * t2β));
φ = (ci8*t8β + ci4*t4β + ci2) * t2β + β;
}
@@ -246,18 +254,6 @@
}
/**
- * Verifies if a trigonometric identity produced the expected value. This method is used in assertions only.
- * The tolerance threshold is approximatively 1.5E-12 (note that it still about 7000 time greater than
- * {@code Math.ulp(1.0)}).
- *
- * @see #ALLOW_TRIGONOMETRIC_IDENTITIES
- */
- private static boolean identityEquals(final double actual, final double expected) {
- // Use !(a > b) instead of (a <= b) in order to tolerate NaN.
- return !(abs(actual - expected) > (ANGULAR_TOLERANCE / 1000));
- }
-
- /**
* Restores transient fields after deserialization.
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/Benchmark.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/Benchmark.java
new file mode 100644
index 0000000..4dc1dbd
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/Benchmark.java
@@ -0,0 +1,281 @@
+/*
+ * 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.projection;
+
+import java.util.List;
+import java.util.Random;
+import java.io.IOException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+import org.apache.sis.math.Statistics;
+import org.apache.sis.math.StatisticsFormat;
+import org.apache.sis.measure.Latitude;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.referencing.provider.AbstractProvider;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.MathTransformFactoryMock;
+
+
+/**
+ * Measures the performance of a given map projection implementation.
+ * This class can be used for comparing different implementation alternatives,
+ * for example with {@code ALLOW_TRIGONOMETRIC_IDENTITIES} flag on or off.
+ *
+ * <p><b>Usage:</b> modify the provider created in the {@code main} method if needed, and run. Change map projection
+ * implementation (for example by changing a {@code ALLOW_TRIGONOMETRIC_IDENTITIES} flag value) and run again.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @since 0.8
+ * @version 0.8
+ * @module
+ */
+@SuppressWarnings("UseOfSystemOutOrSystemErr")
+public final class Benchmark {
+ /**
+ * Runs the benchmark and prints the time result to the standard output.
+ * Edit this method for measuring the performance of a different map projection implementation.
+ *
+ * @param args ignored.
+ * @throws Exception if an error occurred while creating the map projection, projecting a point, <i>etc.</i>
+ */
+ public static void main(String[] args) throws Exception {
+ final Benchmark benchmark = new Benchmark( // Put on next line the provider of the projection to benchmark.
+ new org.apache.sis.internal.referencing.provider.LambertConformal2SP(),
+ 8, // Central meridian
+ 25, // Standard parallel 1
+ 40); // Standard parallel 2
+
+ for (int i=0; i<10; i++) {
+ benchmark.run(true);
+ System.gc();
+ Thread.sleep(1000);
+ }
+ benchmark.printStatistics();
+ }
+
+ /**
+ * Number of points to project. Can be modified freely.
+ */
+ private static final int NUM_POINTS = 1000000;
+
+ /**
+ * Number of dimension (not modifiable).
+ */
+ private static final int DIMENSION = 2;
+
+ /**
+ * The math transforms to use for the forward projection.
+ */
+ private final Transforms forward;
+
+ /**
+ * The math transforms to use for the inverse projection.
+ */
+ private final Transforms inverse;
+
+ /**
+ * The coordinates to project, filled with random points.
+ */
+ private final double[] coordinates;
+
+ /**
+ * The result of map projections.
+ */
+ private final double[] result;
+
+ /**
+ * Difference between inverse projections and the original coordinates.
+ */
+ private final Statistics errors;
+
+ /**
+ * Random number generator for the points to project.
+ */
+ private final Random random;
+
+ /**
+ * Prepares benchmarking for the map projection created by the given provider.
+ */
+ private Benchmark(final AbstractProvider provider,
+ final double centralMeridian,
+ final double standardParallel1,
+ final double standardParallel2) throws FactoryException, NoninvertibleTransformException
+ {
+ random = new Random();
+ final Parameters values = Parameters.castOrWrap(provider.getParameters().createValue());
+ values.parameter(Constants.SEMI_MAJOR) .setValue(MapProjectionTestCase.WGS84_A);
+ values.parameter(Constants.SEMI_MINOR) .setValue(MapProjectionTestCase.WGS84_B);
+ values.parameter(Constants.CENTRAL_MERIDIAN) .setValue(centralMeridian);
+ values.parameter(Constants.STANDARD_PARALLEL_1).setValue(standardParallel1);
+ values.parameter(Constants.STANDARD_PARALLEL_2).setValue(standardParallel2);
+ forward = new Transforms("Forward", new MathTransformFactoryMock(provider).createParameterizedTransform(values));
+ inverse = new Transforms("Inverse", forward.projection.inverse());
+ coordinates = new double[NUM_POINTS * DIMENSION];
+ final double λmin = centralMeridian - 40;
+ final double Δλ = 80;
+ final double φmin = standardParallel1 * 0.75;
+ final double Δφ = Math.min(standardParallel2 * 1.25, Latitude.MAX_VALUE) - φmin;
+ for (int i=0; i<coordinates.length;) {
+ coordinates[i++] = random.nextDouble() * Δλ + λmin; // Longitude
+ coordinates[i++] = random.nextDouble() * Δφ + φmin; // Latitude
+ }
+ result = new double[coordinates.length];
+ errors = new Statistics("Errors (cm)");
+ }
+
+ /**
+ * Decomposition of the {@code MathTransform} that perform map projections.
+ */
+ private static final class Transforms {
+ /**
+ * The original (full) map projection.
+ */
+ final MathTransform projection;
+
+ /**
+ * The transform that performs the non-linear part of the map projection.
+ * This is the transform that we want to benchmark.
+ */
+ private final MathTransform kernel;
+
+ /**
+ * The normalization performed before the map projection.
+ */
+ private final LinearTransform normalize;
+
+ /**
+ * The denormalization performed after the map projection.
+ */
+ private final LinearTransform denormalize;
+
+ /**
+ * Statistics about the time needed for performing the map projection.
+ */
+ private final Statistics performance;
+
+ /**
+ * Creates a decomposition of the given map projection.
+ *
+ * @param label a label for display purpose.
+ * @param projection the map projection to benchmark.
+ */
+ private Transforms(final String label, final MathTransform projection) {
+ this.projection = projection;
+ performance = new Statistics(label);
+ final List<MathTransform> steps = MathTransforms.getSteps(projection);
+ int kernelIndex = -1;
+ for (int i=steps.size(); --i >= 0;) {
+ if (!(steps.get(i) instanceof LinearTransform)) {
+ if (kernelIndex >= 0) {
+ throw new IllegalArgumentException("Found more than one non-linear kernel.");
+ }
+ kernelIndex = i;
+ }
+ }
+ if (kernelIndex < 0) {
+ throw new IllegalArgumentException("Non-linear kernel not found.");
+ }
+ kernel = steps.get(kernelIndex);
+ normalize = singleton(steps.subList(0, kernelIndex));
+ denormalize = singleton(steps.subList(kernelIndex + 1, steps.size()));
+ }
+
+ /**
+ * Returns the single elements of the given list, or an identity transform if none.
+ */
+ private static LinearTransform singleton(final List<MathTransform> components) {
+ switch (components.size()) {
+ case 0: return MathTransforms.identity(DIMENSION);
+ case 1: return (LinearTransform) components.get(0);
+ default: throw new IllegalArgumentException("Unexpected number of components.");
+ }
+ }
+
+ /**
+ * Runs the benchmark on the complete map projection, including the linear parts.
+ */
+ final void runComplete(final double[] sources, final double[] targets) throws TransformException {
+ long time = System.nanoTime();
+ projection.transform(sources, 0, targets, 0, NUM_POINTS);
+ time = System.nanoTime() - time;
+ final double seconds = time * NANOS_TO_SECONDS;
+ System.out.printf("%s time: %1.4f%n", performance.name(), seconds);
+ performance.accept(seconds);
+ }
+
+ /**
+ * Runs the benchmark only on the non-linear part of the map projection,
+ * ignoring the linear parts.
+ */
+ final void runKernel(final double[] sources, final double[] targets) throws TransformException {
+ normalize.transform(sources, 0, targets, 0, NUM_POINTS);
+ long time = System.nanoTime();
+ kernel.transform(targets, 0, targets, 0, NUM_POINTS);
+ time = System.nanoTime() - time;
+ denormalize.transform(targets, 0, targets, 0, NUM_POINTS);
+ final double seconds = time * NANOS_TO_SECONDS;
+ System.out.printf("%s time: %1.4f%n", performance.name(), seconds);
+ performance.accept(seconds);
+ }
+
+ /**
+ * For reporting the time measured by {@link System#nanoTime()}.
+ */
+ private static final double NANOS_TO_SECONDS = 1E-9;
+ }
+
+ /**
+ * Runs the benchmark.
+ *
+ * @param kernelOnly {@code true} for measuring the performance of only the non-linear part,
+ * or {@code false} for measuring the performance of the whole projection.
+ */
+ private void run(final boolean kernelOnly) throws TransformException {
+ if (kernelOnly) {
+ forward.runKernel(coordinates, result);
+ inverse.runKernel(result, result);
+ } else {
+ forward.runComplete(coordinates, result);
+ inverse.runComplete(result, result);
+ }
+ for (int i=0; i<NUM_POINTS;) {
+ final double dx = result[i] - coordinates[i]; coordinates[i++] += random.nextDouble() - 0.5;
+ final double dy = result[i] - coordinates[i]; coordinates[i++] += random.nextDouble() - 0.5;
+ errors.accept(Math.hypot(dx*DEGREES_TO_CENTIMETRES, dy*DEGREES_TO_CENTIMETRES));
+ }
+ }
+
+ /**
+ * For reporting the errors in more convenient units.
+ * This is an approximative conversion based on the nautical mile length.
+ */
+ private static final double DEGREES_TO_CENTIMETRES = 60*1852*100;
+
+ /**
+ * Prints statistics about measured time.
+ */
+ private void printStatistics() throws IOException {
+ System.out.println();
+ StatisticsFormat.getInstance().format(new Statistics[] {forward.performance, inverse.performance}, System.out);
+ System.out.printf("%nAverage error is %1.2E cm (standard deviation %1.1E).%n", errors.mean(), errors.standardDeviation(false));
+ System.out.flush();
+ }
+}