This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new c65774a Document better `GeodeticCalculator` accuracy considerations
and replace the special cases in JUnit tests by a `KnownProblem` enumeration in
an attempt to make clearer what the problem can be and how the tests handle
them.
c65774a is described below
commit c65774ab7961b2bc0c9322083bb5a492de3118d7
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jan 18 14:36:20 2021 +0100
Document better `GeodeticCalculator` accuracy considerations and replace
the special cases in JUnit tests by a `KnownProblem` enumeration in an attempt
to make clearer what the problem can be and how the tests handle them.
---
.../apache/sis/internal/referencing/Formulas.java | 6 ++
.../sis/referencing/GeodesicsOnEllipsoid.java | 15 ++-
.../apache/sis/referencing/GeodeticCalculator.java | 4 +-
.../sis/referencing/GeodesicsOnEllipsoidTest.java | 47 +++------
.../sis/referencing/GeodeticCalculatorTest.java | 105 ++++++++++++---------
5 files changed, 94 insertions(+), 83 deletions(-)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
index ee258e4..80c250a 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
@@ -43,6 +43,12 @@ public final class Formulas extends Static {
* assuming that the unit of measurement is metre. This constant
determines also
* (indirectly) the minimum accuracy of iterative methods in map
projections.
*
+ * <h4>Maintenance</h4>
+ * If this value is modified, then all usages of this constant should be
verified.
+ * Some usages may need to be compensated. For example {@code
GeodesicsOnEllipsoid}
+ * uses a millimetric precision by dividing the tolerance by 10 or more.
We way want
+ * to keep the same precision there even if {@code LINEAR_TOLERANCE} was
made smaller.
+ *
* @see #ANGULAR_TOLERANCE
* @see org.apache.sis.internal.util.Numerics#COMPARISON_THRESHOLD
*/
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
index 52fd130..2fb94d0 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodesicsOnEllipsoid.java
@@ -88,16 +88,21 @@ class GeodesicsOnEllipsoid extends GeodeticCalculator {
/**
* Accuracy threshold for iterative computations, in radians.
- * We take a finer accuracy than default SIS configuration in order to met
the accuracy of numbers
- * published in Karney (2013). If this value is modified, the effect can
be verified by executing
- * the {@code GeodesicsOnEllipsoidTest} methods that compare computed
values against Karney's tables.
- * Remember to update {@link GeodeticCalculator} class javadoc if this
value is changed.
+ * This accuracy must be at least {@value Formulas#ANGULAR_TOLERANCE}
degrees (converted to radians) for
+ * conformance with the accuracy reported in {@link GeodeticCalculator}
class javadoc. Actually we take
+ * a finer accuracy than above value in order to met the accuracy of
numbers published in Karney (2013),
+ * but this extra accuracy is not guaranteed because it is hard to achieve
in all cases.
*
* <p><b>Note:</b> when the iteration loop detects that it reached this
requested accuracy, the loop
* completes the iteration step which was in progress. Consequently the
final accuracy is one iteration
* better than the accuracy computed from this value.</p>
+ *
+ * <h4>Maintenance</h4>
+ * If this value is modified, the effect can be verified by executing the
{@code GeodesicsOnEllipsoidTest}
+ * methods that compare computed values against Karney's tables. The
{@link GeodeticCalculator} javadoc may
+ * need to be edited accordingly.
*/
- static final double ITERATION_TOLERANCE = Formulas.ANGULAR_TOLERANCE *
(PI/180) / 20;
+ static final double ITERATION_TOLERANCE = (Formulas.ANGULAR_TOLERANCE /
20) * (PI/180);
/**
* Difference between ending point and antipode of starting point for
considering them as nearly antipodal.
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
index 6161778..06d29a7 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
@@ -84,7 +84,7 @@ import static
org.apache.sis.internal.referencing.provider.ModifiedAzimuthalEqui
* </li>
* </ul>
*
- * <h2>Algorithms and accuracy</h2>
+ * <h2>Algorithms</h2>
* {@code GeodeticCalculator} uses two set of formulas, depending if the
figure of the Earth
* {@linkplain Ellipsoid#isSphere() is a sphere} or an ellipsoid.
* Publications relevant to this class are:
@@ -104,7 +104,9 @@ import static
org.apache.sis.internal.referencing.provider.ModifiedAzimuthalEqui
* for the reference implementation.</li>
* </ul>
*
+ * <h2>Accuracy</h2>
* {@code GeodeticCalculator} aims for a positional accuracy of one centimetre.
+ * The accuracy is often better (about one millimetre), but not everywhere.
* Azimuthal accuracy corresponds to an error of one centimetre at a distance
of one kilometer,
* except for nearly antipodal points (less than 1° of longitude and latitude
from antipode)
* and points close to the poles where the azimuthal errors are larger.
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodesicsOnEllipsoidTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodesicsOnEllipsoidTest.java
index c39d1ec..75ccf4e 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodesicsOnEllipsoidTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodesicsOnEllipsoidTest.java
@@ -126,7 +126,7 @@ public final strictfp class GeodesicsOnEllipsoidTest
extends GeodeticCalculatorT
* {@code true} if iteration stopped before to reach the desired
accuracy because of limitation
* in {@code double} precision. This field must be reset to {@code
false} before any new point.
*
- * @see #iterationReachedPrecisionLimit()
+ * @see #relaxIfConfirmed(KnownProblem)
* @see #clear()
*/
private boolean iterationReachedPrecisionLimit;
@@ -143,8 +143,7 @@ public final strictfp class GeodesicsOnEllipsoidTest
extends GeodeticCalculatorT
@Override void store(final String name, final double value) {
if (name.equals("dα₁ ≪ α₁")) {
iterationReachedPrecisionLimit = true;
- }
- if (name.length() == 1) {
+ } else if (name.length() == 1) {
switch (name.charAt(0)) {
case 'x': x = value; break;
case 'y': y = value; break;
@@ -523,40 +522,24 @@ public final strictfp class GeodesicsOnEllipsoidTest
extends GeodeticCalculatorT
}
/**
- * Returns {@code true} if iteration stopped before to reach the desired
accuracy because of limitation in
- * {@code double} precision. This problem may happen in the {@link
GeodesicsOnEllipsoid#computeDistance()}
+ * Returns a value {@literal > 1} if iteration stopped before to reach the
desired accuracy because of limitation
+ * in {@code double} precision. This problem may happen in the {@link
GeodesicsOnEllipsoid#computeDistance()}
* method when {@literal dα₁ ≪ α₁}. If locale variable storage is enabled,
this situation is flagged by the
* {@code "dα₁ ≪ α₁"} key. Otherwise we conservatively assume that this
situation occurred.
*/
@Override
- boolean iterationReachedPrecisionLimit() {
- if (GeodesicsOnEllipsoid.STORE_LOCAL_VARIABLES) {
- return testedEarth.iterationReachedPrecisionLimit;
- } else {
- // Conservative value in absence of information.
- return true;
- }
- }
-
- /**
- * Tells whether failure to compute geodesic for the given data should
cause the test case to fail.
- * This is invoked by {@link #compareAgainstDataset()}.
- *
- * @param expected a row from the {@code $SIS_DATA/Tests/GeodTest.dat}
file.
- * @return whether the JUnit test should fail.
- */
- @Override
- boolean isFailure(final double[] expected) {
- final double φ1 = expected[COLUMN_φ1];
- final double φ2 = expected[COLUMN_φ2];
- final double Δλ = expected[COLUMN_λ2] - expected[COLUMN_λ1];
- if (Δλ > 90 && max(abs(φ1), abs(φ2)) < 2E-4) {
- return true; // Ignore equatorial case
in previous version (not anymore).
- }
- if (Δλ > 179 && abs(φ1 + φ2) < 0.002) {
- return false; // Ignore antipodal case.
+ double relaxIfConfirmed(final KnownProblem potentialProblem) {
+ if (potentialProblem ==
KnownProblem.ITERATION_REACHED_PRECISION_LIMIT) {
+ if (GeodesicsOnEllipsoid.STORE_LOCAL_VARIABLES) {
+ if (testedEarth.iterationReachedPrecisionLimit) {
+ return 2;
+ }
+ } else {
+ // No information about whether the problem really occurred.
+ return 2;
+ }
}
- return super.isFailure(expected);
+ return super.relaxIfConfirmed(potentialProblem);
}
/**
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
index 1df1bb6..e7b62eb 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
@@ -463,24 +463,25 @@ public strictfp class GeodeticCalculatorTest extends
TestCase {
* We execute only one test for each row instead than
executing both tests,
* for making sure that `GeodeticCalculator` never see the
expected values.
*/
+ KnownProblem potentialProblem = null;
final boolean isTestingInverse = random.nextBoolean();
- final double cosφ1 = abs(cos(toRadians(expected[COLUMN_φ1])));
// For adjusting longitude tolerance.
- final double cosφ2 = abs(cos(toRadians(expected[COLUMN_φ2])));
+ final double φ1 = expected[COLUMN_φ1];
+ final double φ2 = expected[COLUMN_φ2];
+ final double cosφ1 = abs(cos(toRadians(φ1))); // For
adjusting longitude tolerance.
+ final double cosφ2 = abs(cos(toRadians(φ2)));
+ final double Δλ = abs(expected[COLUMN_λ2] -
expected[COLUMN_λ1]);
double linearTolerance, latitudeTolerance, longitudeTolerance,
azimuthTolerance;
- final boolean isLongArcOnEquator;
if (isSphere) {
/*
* When spherical formulas are used instead than
ellipsoidal formulas, an error up to 1% is expected
* in distance calculations (source: Wikipedia). A yet
larger error is observed for azimuth values,
* especially near poles and between antipodal points.
Following are empirical thresholds.
*/
- isLongArcOnEquator = false;
// Not a problem in spherical case.
linearTolerance = expected[COLUMN_Δs] * 0.01;
latitudeTolerance = toDegrees(linearTolerance /
c.semiMajorAxis);
- longitudeTolerance = expected[COLUMN_φ2] > 89.5 ? 180 :
latitudeTolerance / cosφ2;
+ longitudeTolerance = φ2 > 89.5 ? 180 : latitudeTolerance /
cosφ2;
azimuthTolerance = 0.5;
// About 8.8 metres at distance of 1 km.
if (isTestingInverse) {
- final double Δλ = abs(expected[COLUMN_λ2] -
expected[COLUMN_λ1]);
if (Δλ > 179) azimuthTolerance = 100;
else if (Δλ > 178) azimuthTolerance = 20;
else if (Δλ > 175) azimuthTolerance = 10;
@@ -498,18 +499,16 @@ public strictfp class GeodeticCalculatorTest extends
TestCase {
longitudeTolerance = Formulas.ANGULAR_TOLERANCE / cosφ2;
azimuthTolerance = Formulas.LINEAR_TOLERANCE * (180/PI)
/ 10000;
if (isTestingInverse) {
- final double Δ = max(abs(180 - abs(expected[COLUMN_λ2]
- expected[COLUMN_λ1])),
- abs(expected[COLUMN_φ1]
+ expected[COLUMN_φ2]));
- if (Δ < 1) {
+ if (max(abs(180 - Δλ), abs(φ1 + φ2)) < 1) {
azimuthTolerance = 1 * (180/PI) / 10000;
// 1 meter for 10 km.
}
+ if (Δλ > 90 && max(abs(φ1), abs(φ2)) < 2E-4) {
+ potentialProblem =
KnownProblem.ITERATION_REACHED_PRECISION_LIMIT;
+ }
+ if (Δλ > 179 && abs(φ1 + φ2) < 0.002) {
+ potentialProblem =
KnownProblem.NO_CONVERGENCE_ON_ANTIPODAL_POINTS;
+ }
}
- /*
- * If the start and end points are both within about 3
meters from equator and
- * their distance is more than 10,000 km, we may need to
relax the tolerance.
- * This is an empirical adjustment based on test failures
observation.
- */
- isLongArcOnEquator = Math.min(cosφ1, cosφ2) >=
0.9999999999999 && expected[COLUMN_Δs] > 1E+7;
assertEquals("Consistency with accuracy reported in
Javadoc.", 0.001, linearTolerance, 0.0005);
}
/*
@@ -536,21 +535,24 @@ public strictfp class GeodeticCalculatorTest extends
TestCase {
assertEquals("λ₂", expected[COLUMN_λ2],
end.getOrdinate(1), longitudeTolerance);
assertEquals("α₂", expected[COLUMN_α2],
c.getEndingAzimuth(), azimuthTolerance / cosφ2);
assertEquals("∆s", expected[COLUMN_Δs],
c.getGeodesicDistance(), linearTolerance *
- (isLongArcOnEquator &&
iterationReachedPrecisionLimit() ? 2 : 1));
+
relaxIfConfirmed(potentialProblem));
clear();
} catch (GeodeticException | AssertionError e) {
- if (!isTestingInverse || e instanceof AssertionError ||
isFailure(expected)) {
- out.printf("Test failure at line %d: %s%n"
- + "The values provided in the test file are:%n"
- + "(φ₁,λ₁) = %16.12f %16.12f%n"
- + "(φ₂,λ₂) = %16.12f %16.12f%n"
- + "The values computed by the geodesic
calculator are:%n",
- reader.getLineNumber(),
e.getLocalizedMessage(),
- expected[0], expected[1], expected[3],
expected[4]);
- out.println(c);
- throw e;
+ if (e instanceof GeodeticException) {
+ if (potentialProblem ==
KnownProblem.NO_CONVERGENCE_ON_ANTIPODAL_POINTS) {
+ noConvergenceCount++;
+ continue;
+ }
}
- noConvergenceCount++;
+ out.printf("Test failure at line %d: %s%n"
+ + "The values provided in the test file are:%n"
+ + "(φ₁,λ₁) = %16.12f %16.12f%n"
+ + "(φ₂,λ₂) = %16.12f %16.12f%n"
+ + "The values computed by the geodesic calculator
are:%n",
+ reader.getLineNumber(), e.getLocalizedMessage(),
+ expected[0], expected[1], expected[3],
expected[4]);
+ out.println(c);
+ throw e;
}
}
}
@@ -558,31 +560,44 @@ public strictfp class GeodeticCalculatorTest extends
TestCase {
}
/**
- * Returns {@code true} if iteration stopped before to reach the desired
accuracy because the correction
- * to apply in iterative steps is smaller than what can be applied using
IEEE 754 double arithmetic.
- * It never happen in the spherical case because there is no iteration in
that case.
+ * Returns the factor by which to relax the linear tolerance, or 1 for no
relaxation.
+ * This method is invoked after {@link
GeodeticCalculator#computeDistance()} has been invoked.
+ * It should check if the potential problem really occurred, and if yes
return a value greater
+ * than 1. If no problem (other than IEEE 754 rounding errors) is expected
to occur, then this
+ * method should return exactly 1.
*
- * <p>This method is invoked only in situations empirically identified as
susceptible to have this problem:</p>
- * <uL>
- * <li>When start point and end point are both within about 3 meters
from equator
- * and their distance is more than 10,000 km.</li>
- * </ul>
+ * @param potentialProblem the problem that may happen, or {@code null}
if none.
+ * @return factor by which to relax the linear tolerance threshold, or 1
if no relaxation.
*/
- boolean iterationReachedPrecisionLimit() {
- return false;
+ double relaxIfConfirmed(KnownProblem potentialProblem) {
+ return 1;
}
/**
- * Tells whether failure to compute geodesic for the given data should
cause the test case to fail.
- * The default implementation always return {@code true}. Subclass can
override if some points are
- * known to fail.
+ * Known problems with our implementation of inverse geodetic with
ellipsoidal formulas.
+ * Those problems may occur in {@link #compareAgainstDataset()} test when
ellipsoidal formulas
+ * are used (those problems do not occur with spherical formulas). This is
an enumeration
+ * of cases where {@link GeodesicsOnEllipsoid#computeDistance()} does not
converge.
*
- * @param expected a row from the {@code $SIS_DATA/Tests/GeodTest.dat}
file.
- * Use {@code COLUMN_*} constant for accessing values by column
indices.
- * @return whether the JUnit test should fail.
+ * @see #compareAgainstDataset()
+ * @see #relaxIfConfirmed(KnownProblem)
+ * @see <a href="https://issues.apache.org/jira/browse/SIS-467">SIS-467</a>
*/
- boolean isFailure(final double[] expected) {
- return true;
+ enum KnownProblem {
+ /**
+ * Iteration stopped before to reach the desired accuracy because the
correction to apply in iterative
+ * steps is smaller than what can be applied using IEEE 754 double
arithmetic. This problem occurs in
+ * {@literal α₁ -= dα₁} expression when {@literal dα₁ ≪ α₁}, in which
case the subtraction has no effect.
+ * It has been observed on the equator between 2 close points. This is
not considered a failure because
+ * the precision is still in 1 cm precision target. We only need to
relax the 1 mm precision check.
+ */
+ ITERATION_REACHED_PRECISION_LIMIT,
+
+ /**
+ * Iteration failed with a "no convergence error". It sometime happens
during distance calculation between
+ * antipodal points.
+ */
+ NO_CONVERGENCE_ON_ANTIPODAL_POINTS;
}
/**