Repository: obspy/obspy

Issue:
Tests fail on Alpine Linux
I wanted to put ObsPy 1.0.3 in an [Alpine Linux docker](https://gist.github.com/Brtle/eb9749f2e345058c365a521319b0aad4) but a unit test fails:

```
======================================================================
ERROR: test_calc_vincenty_inverse (obspy.geodetics.tests.test_util_geodetics.UtilGeodeticsTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/obspy/geodetics/base.py", line 155, in calc_vincenty_inverse
    alpha = math.asin(sin_alpha)
ValueError: math domain error

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/obspy/geodetics/tests/test_util_geodetics.py", line 49, in test_calc_vincenty_inverse
    res = calc_vincenty_inverse(0, 0, 0, 17)
  File "/usr/lib/python3.6/site-packages/obspy/geodetics/base.py", line 191, in calc_vincenty_inverse
    raise StopIteration
StopIteration

----------------------------------------------------------------------
Ran 1386 tests in 491.079s

FAILED (errors=1)
```

This is quite weird since this unit test doesn't seem to use any particular library (except `math`). I also tried to pull the latest version of ObsPy without success.

I then find [this numpy bug](https://github.com/numpy/numpy/issues/7128). Do you think the unit test can fail due to a bug in musl's trigonometric functions like in numpy?


Discussion:
Hmm - that would be totally possible but I don't know enough about musl to be any help here. Can you maybe add a couple of print statements in the `calc_vincenty_inverse()` function - then we can compare against a non-MUSL libc and potentially narrow down the error.
Hmm, I put some print statements and the [`math.atan2`](https://github.com/obspy/obspy/blob/master/obspy/geodetics/base.py#L145) doesn't give the same result.

On AlpineLinux:

```
>>> import math
>>> math.atan2(0.29237170472273677, 0.9563047559630354)
0.29670597283903605
```

On Ubuntu:

```
>>> import math
>>> math.atan2(0.29237170472273677, 0.9563047559630354)
0.2967059728390361
```

I don't think it's a problem with ObsPy, sorry for opening this issue!
> I don't think it's a problem with ObsPy, sorry for opening this issue!

It definitely helps to document these things so other people might be able to find it by searching for it. In any case - your example only appears to be rounded differently so I'm not sure that the `atan2` function is the issue.
> On AlpineLinux:

```
>>> import math
>>> math.atan2(0.29237170472273677, 0.9563047559630354)
0.29670597283903605
```

> On Ubuntu:

```
>>> import math
>>> math.atan2(0.29237170472273677, 0.9563047559630354)
0.2967059728390361
```

Looks like the same just rounded away last digit?? That could even just be a thing of number of digits printed by default `float.__str__`
It is not just a round issue, I still got a difference with more significant digits.

On AlpineLinux:
```
>>> print('%.25f' % math.atan2(0.29237170472273677, 0.9563047559630354))
0.2967059728390360473504472
```

On Ubuntu:
```
>>> print('%.25f' % math.atan2(0.29237170472273677, 0.9563047559630354))
0.2967059728390361028615985
```

Moreover, the test fails on [this line](https://github.com/obspy/obspy/blob/master/obspy/geodetics/base.py#L148) because the `sin_alpha` variable is equal to 1.0000000000000002220446049 and is normally in [-1, 1].
I see. How about clamping `sin_alpha` to [-1, 1] to make sure the operation is still defined? This should not do anything if all calculations are correct but maybe solve this particular issue? I mean its really a lot of significant digits that are still correct so accuracy should not really suffer a lot even in the degenerate case.
Hmm, I absolutely don't know what this piece of code is doing. However, [this assertRaises](https://github.com/obspy/obspy/blob/master/obspy/geodetics/tests/test_util_geodetics.py#L40) is waiting a `StopIteration` which is actually a `math domain error` raised on ur [`asin` line](https://github.com/obspy/obspy/blob/master/obspy/geodetics/base.py#L148) when the `sin_alpha` is approximately equal to1.00000000000000022204. So I don't think we can easily clamp `sin_alpha`. :-/
@Brtle Can you try this branch here on Alpine Linux? https://github.com/krischer/obspy/tree/musl-workaround

Turns out that MUSL appears to be more correct and we rely on libc being a bit lenient.
This is still failing:

```
# obspy-runtests obspy.geodetics.tests.test_util_geodetics.UtilGeodeticsTestCase.test_calc_vincenty_inverse
Running /usr/lib/python3.6/site-packages/obspy-0.0.0.dev_.g6f1a5940de-py3.6-linux-x86_64.egg/obspy/scripts/runtests.py, ObsPy version '0.0.0.dev+.g6f1a5940de'
E

======================================================================
ERROR: test_calc_vincenty_inverse (obspy.geodetics.tests.test_util_geodetics.UtilGeodeticsTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/obspy-0.0.0.dev_.g6f1a5940de-py3.6-linux-x86_64.egg/obspy/geodetics/base.py", line 149, in calc_vincenty_inverse
    alpha = math.asin(sin_alpha)
ValueError: math domain error

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/obspy-0.0.0.dev_.g6f1a5940de-py3.6-linux-x86_64.egg/obspy/geodetics/tests/test_util_geodetics.py", line 50, in test_calc_vincenty_inverse
    res = calc_vincenty_inverse(0, 0, 0, 17)
  File "/usr/lib/python3.6/site-packages/obspy-0.0.0.dev_.g6f1a5940de-py3.6-linux-x86_64.egg/obspy/geodetics/base.py", line 151, in calc_vincenty_inverse
    raise StopIteration
StopIteration

----------------------------------------------------------------------
Ran 1 test in 0.010s

FAILED (errors=1)
Do you want to report this to tests.obspy.org? [n]:
```
Hmm - I guess I'll have to look into running alpine linux myself. Do you happen to have the dockerfile laying around?
I used [this dockerfile](https://gist.github.com/Brtle/d08c804d98e87cb00d841d95df1d5de1#file-dockerfile-musl-workaround) to test your branch. I'm a bit ashamed, this dockerfile is very ugly but it should work.

Thanks for taking time for this issue!
Would be good to get this Dockerfile cleaned up a bit and added to our Docker setup..

Resolving patch:
diff --git a/obspy/geodetics/base.py b/obspy/geodetics/base.py
--- a/obspy/geodetics/base.py
+++ b/obspy/geodetics/base.py
@@ -40,6 +40,13 @@
 WGS84_F = 1 / 298.257223563
 
 
+def _isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
+    """
+    Equivalent of the :meth:`math.isclose` method compatible with python 2.7.
+    """
+    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+
 def calc_vincenty_inverse(lat1, lon1, lat2, lon2, a=WGS84_A, f=WGS84_F):
     """
     Vincenty Inverse Solution of Geodesics on the Ellipsoid.
@@ -111,7 +118,7 @@ def calc_vincenty_inverse(lat1, lon1, lat2, lon2, a=WGS84_A, f=WGS84_F):
 
     b = a * (1 - f)  # semiminor axis
 
-    if (abs(lat1 - lat2) < 1e-8) and (abs(lon1 - lon2) < 1e-8):
+    if _isclose(lat1, lat2) and _isclose(lon1, lon2):
         return 0.0, 0.0, 0.0
 
     # convert latitudes and longitudes to radians:
@@ -140,41 +147,28 @@ def calc_vincenty_inverse(lat1, lon1, lat2, lon2, a=WGS84_A, f=WGS84_F):
                 pow((math.cos(u_1) * math.sin(u_2) - math.sin(u_1) *
                      math.cos(u_2) * math.cos(dlon)), 2)
             sin_sigma = math.sqrt(sqr_sin_sigma)
+
             cos_sigma = math.sin(u_1) * math.sin(u_2) + math.cos(u_1) * \
                 math.cos(u_2) * math.cos(dlon)
             sigma = math.atan2(sin_sigma, cos_sigma)
             sin_alpha = math.cos(u_1) * math.cos(u_2) * math.sin(dlon) / \
-                math.sin(sigma)
-            alpha = math.asin(sin_alpha)
-            cos2sigma_m = math.cos(sigma) - \
-                (2 * math.sin(u_1) * math.sin(u_2) / pow(math.cos(alpha), 2))
-            c = (f / 16) * pow(math.cos(alpha), 2) * \
-                (4 + f * (4 - 3 * pow(math.cos(alpha), 2)))
+                sin_sigma
+
+            sqr_cos_alpha = 1 - sin_alpha * sin_alpha
+            if _isclose(sqr_cos_alpha, 0):
+                # Equatorial line
+                cos2sigma_m = 0
+            else:
+                cos2sigma_m = cos_sigma - \
+                    (2 * math.sin(u_1) * math.sin(u_2) / sqr_cos_alpha)
+
+            c = (f / 16) * sqr_cos_alpha * (4 + f * (4 - 3 * sqr_cos_alpha))
             last_dlon = dlon
-            dlon = omega + (1 - c) * f * math.sin(alpha) * \
-                (sigma + c * math.sin(sigma) *
-                    (cos2sigma_m + c * math.cos(sigma) *
+            dlon = omega + (1 - c) * f * sin_alpha * \
+                (sigma + c * sin_sigma *
+                    (cos2sigma_m + c * cos_sigma *
                         (-1 + 2 * pow(cos2sigma_m, 2))))
 
-            u2 = pow(math.cos(alpha), 2) * (a * a - b * b) / (b * b)
-            _a = 1 + (u2 / 16384) * (4096 + u2 * (-768 + u2 *
-                                                  (320 - 175 * u2)))
-            _b = (u2 / 1024) * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
-            delta_sigma = _b * sin_sigma * \
-                (cos2sigma_m + (_b / 4) *
-                    (cos_sigma * (-1 + 2 * pow(cos2sigma_m, 2)) - (_b / 6) *
-                        cos2sigma_m * (-3 + 4 * sqr_sin_sigma) *
-                        (-3 + 4 * pow(cos2sigma_m, 2))))
-
-            dist = b * _a * (sigma - delta_sigma)
-            alpha12 = math.atan2(
-                (math.cos(u_2) * math.sin(dlon)),
-                (math.cos(u_1) * math.sin(u_2) -
-                 math.sin(u_1) * math.cos(u_2) * math.cos(dlon)))
-            alpha21 = math.atan2(
-                (math.cos(u_1) * math.sin(dlon)),
-                (-math.sin(u_1) * math.cos(u_2) +
-                 math.cos(u_1) * math.sin(u_2) * math.cos(dlon)))
             iterlimit -= 1
             if iterlimit < 0:
                 # iteration limit reached
@@ -183,6 +177,25 @@ def calc_vincenty_inverse(lat1, lon1, lat2, lon2, a=WGS84_A, f=WGS84_F):
         # usually "math domain error"
         raise StopIteration
 
+    u2 = sqr_cos_alpha * (a * a - b * b) / (b * b)
+    _a = 1 + (u2 / 16384) * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
+    _b = (u2 / 1024) * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
+    delta_sigma = _b * sin_sigma * \
+        (cos2sigma_m + (_b / 4) *
+            (cos_sigma * (-1 + 2 * pow(cos2sigma_m, 2)) - (_b / 6) *
+                cos2sigma_m * (-3 + 4 * sqr_sin_sigma) *
+                (-3 + 4 * pow(cos2sigma_m, 2))))
+
+    dist = b * _a * (sigma - delta_sigma)
+    alpha12 = math.atan2(
+        (math.cos(u_2) * math.sin(dlon)),
+        (math.cos(u_1) * math.sin(u_2) -
+            math.sin(u_1) * math.cos(u_2) * math.cos(dlon)))
+    alpha21 = math.atan2(
+        (math.cos(u_1) * math.sin(dlon)),
+        (-math.sin(u_1) * math.cos(u_2) +
+            math.cos(u_1) * math.sin(u_2) * math.cos(dlon)))
+
     if alpha12 < 0.0:
         alpha12 = alpha12 + (2.0 * math.pi)
     if alpha12 > (2.0 * math.pi):


Test patch:
diff --git a/obspy/geodetics/tests/test_util_geodetics.py b/obspy/geodetics/tests/test_util_geodetics.py
--- a/obspy/geodetics/tests/test_util_geodetics.py
+++ b/obspy/geodetics/tests/test_util_geodetics.py
@@ -37,7 +37,6 @@ def test_calc_vincenty_inverse(self):
                           27.4675551, 17.28133229, -27.65771704, -162.65420626)
         self.assertRaises(StopIteration, calc_vincenty_inverse,
                           27.4675551, 17.28133229, -27.65771704, -162.65420626)
-        self.assertRaises(StopIteration, calc_vincenty_inverse, 0, 0, 0, 13)
         # working examples
         res = calc_vincenty_inverse(0, 0.2, 0, 20)
         self.assertAlmostEqual(res[0], 2204125.9174282863)
@@ -47,6 +46,10 @@ def test_calc_vincenty_inverse(self):
         self.assertAlmostEqual(res[0], 1113194.9077920639)
         self.assertAlmostEqual(res[1], 90.0)
         self.assertAlmostEqual(res[2], 270.0)
+        res = calc_vincenty_inverse(0, 0, 0, 13)
+        self.assertAlmostEqual(res[0], 1447153.3801296828)
+        self.assertAlmostEqual(res[1], 90.0)
+        self.assertAlmostEqual(res[2], 270.0)
         res = calc_vincenty_inverse(0, 0, 0, 17)
         self.assertAlmostEqual(res[0], 1892431.3432465086)
         self.assertAlmostEqual(res[1], 90.0)