blob: d83bdc249695749cdac1c70abaed0f576229eb99 [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.referencing.operation.projection;
import java.util.Random;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import static java.lang.Math.*; // Not StrictMath in this particular case.
import org.apache.sis.io.TableAppender;
import org.apache.sis.math.Statistics;
import org.apache.sis.math.StatisticsFormat;
import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.metadata.privy.ReferencingServices;
import static org.apache.sis.util.privy.Constants.NANOS_PER_SECOND;
// Test dependencies
import org.apache.sis.test.Benchmark;
/**
* Implements two alternative methods to compute φ in Mercator projection.
* Those two methods computing the latitude φ from {@code exp(-northing)}
* (ignoring non-relevant terms for this discussion) are:
*
* <ul>
* <li>the series expansion given by §1.3.3 in Geomatics Guidance Note number 7 part 2 – April 2015,</li>
* <li>an iterative process for solving equation (7-9) from Snyder, initially implemented by USGS.</li>
* </ul>
*
* In our measurements, both the iterative process (USGS) and the series expansion (EPSG) have the same accuracy
* when applied on the WGS84 ellipsoid. However, the EPSG formula is 2 times faster on Java 8 (less on more recent
* Java versions). On the other hand, accuracy of the EPSG formula decreases when we increase the eccentricity,
* while the iterative process keeps its accuracy (at the cost of more iterations).
* For the Earth (eccentricity of about 0.082) the errors are less than 0.01 millimetres.
* But the errors become centimetric (for a hypothetical planet of the size of the Earth)
* before eccentricity 0.2 and increase quickly after eccentricity 0.3.
*
* <p>For the WGS84 ellipsoid and the iteration tolerance given by the {@link NormalizedProjection#ITERATION_TOLERANCE}
* constant (currently about 0.25 cm on Earth), the two methods have equivalent precision. Computing φ values for
* millions of random numbers and verifying which method is the most accurate give fifty-fifty results: each method
* win in about 50% of cases. But as we increase the eccentricity, the iterative method wins more often.</p>
*
* @author Martin Desruisseaux (Geomatys)
*/
@Benchmark
public final class MercatorMethodComparison {
/**
* Where to print the outputs of this class.
*/
@SuppressWarnings("UseOfSystemOutOrSystemErr")
private static final PrintStream out = System.out;
/**
* Ellipsoid eccentricity. Value 0 means that the ellipsoid is spherical.
*/
private final double eccentricity;
/**
* Coefficients used in the series expansion.
*/
private final double c2χ, c4χ, c6χ, c8χ;
/**
* The actual implementation to compare with.
*/
private final ConformalProjection implementation;
/**
* Creates a new instance for the same eccentricity as the given projection.
*
* @param projection the projection from which to take the eccentricity.
*/
MercatorMethodComparison(final ConformalProjection projection) {
implementation = projection;
eccentricity = projection.eccentricity;
final double e2 = projection.eccentricitySquared;
final double e4 = e2 * e2;
final double e6 = e2 * e4;
final double e8 = e4 * e4;
/*
* For each line below, add the smallest values first in order to reduce rounding errors.
* The smallest values are the one using the eccentricity raised to the highest power.
*/
c2χ = 13/ 360.* e8 + 1/ 12.* e6 + 5/24.* e4 + e2/2;
c4χ = 811/ 11520.* e8 + 29/240.* e6 + 7/48.* e4;
c6χ = 81/ 1120.* e8 + 7/120.* e6;
c8χ = 4279/161280.* e8;
}
/**
* Computes φ using the series expansion given by Geomatics Guidance Note number 7, part 2.
* This is the first part of the {@link ConformalProjection#φ(double)} method.
*
* @param t the {@code rexpΨ} parameter value.
* @return the latitude (in radians) for the given parameter.
*/
public double bySeriesExpansion(final double t) {
final double χ = PI/2 - 2*atan(t);
return c8χ * sin(8*χ) + // Add the smallest values first for reducing rounding errors.
c6χ * sin(6*χ) +
c4χ * sin(4*χ) +
c2χ * sin(2*χ) + χ;
}
/**
* Computes φ using the iterative method used by USGS.
* This is the second part of the {@link ConformalProjection#φ(double)} method.
*
* @param t the {@code rexpΨ} parameter value.
* @return the latitude (in radians) for the given parameter.
* @throws ProjectionException if the iteration does not converge.
*/
public double byIterativeMethod(final double t) throws ProjectionException {
final double h = 0.5 * eccentricity;
double φ = (PI/2) - 2*atan(t); // Snyder (7-11)
for (int it=0; it < NormalizedProjection.MAXIMUM_ITERATIONS; it++) { // Iteratively solve equation (7-9) from Snyder
final double sinφ = eccentricity * sin(φ);
final double Δφ = φ - = PI/2 - 2*atan(t * pow((1 - sinφ)/(1 + sinφ), hℯ)));
if (abs(Δφ) <= NormalizedProjection.ITERATION_TOLERANCE) {
return φ;
}
}
if (Double.isNaN(t)) {
return Double.NaN;
}
throw new ProjectionException(Resources.format(Resources.Keys.NoConvergence));
}
/**
* Basically a copy of {@link ConformalProjection#expΨ(double, double)}.
*/
final double expΨ(final double φ) {
final double sinφ = eccentricity * sin(φ);
return tan(PI/4 + 0.5*φ) * pow((1 - sinφ) / (1 + sinφ), 0.5*eccentricity);
}
/**
* Compares the φ values computed by the two methods (iterative and series expansion) against the expected φ
* values for random numbers. The result is printed to the standard output stream as the maximum and average errors,
* in units of {@link NormalizedProjection#ITERATION_TOLERANCE} (about 0.25 cm on a planet of the size of Earth).
*
* @param numSamples number of random sample values.
* @throws ProjectionException if an error occurred during the calculation of φ.
*/
public void printAccuracyComparison(final int numSamples) throws ProjectionException {
compare(numSamples, null);
}
/**
* Implementation of {@link #printAccuracyComparison(int)} and {@link #printErrorForExcentricities(double,double)},
* optionally with a comparison with {@link ConformalProjection}.
*/
private void compare(final int numSamples, final TableAppender summarize) throws ProjectionException {
final Statistics iterativeMethodErrors = new Statistics("Iterative method error");
final Statistics seriesExpansionErrors = new Statistics("Series expansion error");
final Statistics implementationErrors = new Statistics("Implementation error");
final Random random = new Random();
for (int i=0; i<numSamples; i++) {
final double φ = random.nextDouble() * PI - PI/2;
final double t = 1 / expΨ(φ);
final double byIterativeMethod = byIterativeMethod(t);
final double bySeriesExpansion = bySeriesExpansion(t);
final double byImplementation = implementation.φ(t);
iterativeMethodErrors.accept(abs - byIterativeMethod) / NormalizedProjection.ITERATION_TOLERANCE);
seriesExpansionErrors.accept(abs - bySeriesExpansion) / NormalizedProjection.ITERATION_TOLERANCE);
implementationErrors .accept(abs - byImplementation) / NormalizedProjection.ITERATION_TOLERANCE);
}
/*
* At this point we finished to collect the statistics for the eccentricity of this particular
* MercatorMethodComparison instance. If this method call is only part of a longer calculation
* for various excentricty values, print a summary in a single line.
* Otherwise print more verbose results.
*/
if (summarize != null) {
summarize.append(String.valueOf(eccentricity)); summarize.nextColumn();
summarize.append(String.valueOf(iterativeMethodErrors.mean())); summarize.nextColumn();
summarize.append(String.valueOf(iterativeMethodErrors.maximum())); summarize.nextColumn();
summarize.append(String.valueOf(seriesExpansionErrors.mean())); summarize.nextColumn();
summarize.append(String.valueOf(seriesExpansionErrors.maximum())); summarize.nextLine();
} else {
Statistics[] stats = new Statistics[] {
iterativeMethodErrors,
seriesExpansionErrors,
implementationErrors,
};
out.println("Comparison of different ways to compute φ for eccentricity " + eccentricity + '.');
out.println("Values are in units of " + NormalizedProjection.ITERATION_TOLERANCE + " radians (about "
+ round(toDegrees(NormalizedProjection.ITERATION_TOLERANCE) * 60 * ReferencingServices.NAUTICAL_MILE * 1000)
+ " mm on Earth).");
final StatisticsFormat format = StatisticsFormat.getInstance();
format.setBorderWidth(1);
try {
format.format(stats, out);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
out.flush();
}
}
/**
* Prints the error of the two methods for various eccentricity values.
* The intent of this method is to find an eccentricity threshold value where we consider the errors too high.
*
* <p>This method is used for determining empirically a value for {@link ConformalProjection#ECCENTRICITY_THRESHOLD}.
* The current threshold value is shown by inserting a horizontal line separator in the table when that threshold
* is crossed.</p>
*
* @param min the first eccentricity value to test.
* @param max the maximal eccentricity value to test.
* @throws ProjectionException if an error occurred in {@link ConformalProjection#φ(double)}.
*/
public static void printErrorForExcentricities(final double min, final double max) throws ProjectionException {
final TableAppender table = new TableAppender(out);
table.appendHorizontalSeparator();
table.append("Eccentricity"); table.nextColumn();
table.append("Mean iterative error"); table.nextColumn();
table.append("Maximal iterative error"); table.nextColumn();
table.append("Mean series error"); table.nextColumn();
table.append("Maximal series error"); table.nextLine();
table.appendHorizontalSeparator();
boolean crossThreshold = false;
final double step = 0.01;
double eccentricity;
for (int i=0; (eccentricity = min + step*i) < max; i++) {
if (!crossThreshold && eccentricity >= ConformalProjection.ECCENTRICITY_THRESHOLD) {
crossThreshold = true;
table.appendHorizontalSeparator();
}
final double semiMinor = sqrt(1 - eccentricity * eccentricity);
final MercatorMethodComparison alt = new MercatorMethodComparison(new NoOp(1, semiMinor));
alt.compare(10000, table);
}
table.appendHorizontalSeparator();
try {
table.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Compares the performance of the 3 methods.
*
* @throws ProjectionException if an error occurred in {@link ConformalProjection#φ(double)}.
*/
private void benchmark() throws ProjectionException {
final Random random = new Random();
final double[] t = new double[1000000];
for (int i=0; i<t.length; i++) {
t[i] = random.nextGaussian() * 3;
}
double s0 = 0, s1 = 0, s2 = 0;
final long t0 = System.nanoTime();
for (int i=0; i<t.length; i++) {
s0 += byIterativeMethod(t[i]);
}
final long t1 = System.nanoTime();
for (int i=0; i<t.length; i++) {
s1 += bySeriesExpansion(t[i]);
}
final long t2 = System.nanoTime();
for (int i=0; i<t.length; i++) {
s2 += implementation.φ(t[i]);
}
final long t3 = System.nanoTime();
final float c = (t1 - t0) / 100f;
out.println("Iterative method: " + ((t1 - t0) / (float) NANOS_PER_SECOND) + " seconds (" + round((t1 - t0) / c) + "%).");
out.println("Series expansion: " + ((t2 - t1) / (float) NANOS_PER_SECOND) + " seconds (" + round((t2 - t1) / c) + "%).");
out.println("Implementation: " + ((t3 - t2) / (float) NANOS_PER_SECOND) + " seconds (" + round((t3 - t2) / c) + "%).");
out.println("Mean φ values: " + (s0 / t.length) + ", "
+ (s1 / t.length) + " and "
+ (s2 / t.length) + ".");
}
/**
* The result is printed to the standard output stream.
*
* @param args ignored.
* @throws ProjectionException if an error occurred in {@link ConformalProjection#φ(double)}.
* @throws InterruptedException if the thread has been interrupted between two benchmarks.
*/
public static void main(String[] args) throws ProjectionException, InterruptedException {
out.println("Comparison of the errors of series expension and iterative method for various eccentricity values.");
printErrorForExcentricities(0.08, 0.3);
out.println();
out.println("Comparison of the errors for a sphere.");
out.println("The errors should be almost zero:");
out.println();
MercatorMethodComparison c = new MercatorMethodComparison(new NoOp(false));
c.compare(10000, null);
out.println();
out.println("Comparison of the errors for the WGS84 eccentricity.");
out.println("The 'ConformalProjection' errors should be the same as the series expansion errors:");
out.println();
c = new MercatorMethodComparison(new NoOp(true));
c.compare(1000000, null);
out.println();
out.println("Comparison of the errors for the eccentricity of an imaginary ellipsoid.");
out.println("The 'ConformalProjection' errors should be the close to the iterative method errors:");
out.println();
c = new MercatorMethodComparison(new NoOp(100, 95));
c.compare(1000000, null);
out.println();
out.println("Benchmarks");
c = new MercatorMethodComparison(new NoOp(true));
for (int i=0; i<4; i++) {
System.gc();
Thread.sleep(1000);
c.benchmark();
out.println();
}
out.flush();
}
}