Repository: fatiando/boule

Issue:
Fix normal gravity of Sphere
Is the centrifugal acceleration for the sphere we have implemented correct? Shouldn't it be projected to the normal of the sphere? This is where it gets a bit confusing. The "gravity" we want is the gradient of the combined potential. But how does that translate to the combined acceleration vector? Do we need to do a vector sum and then take the norm? I would imagine so but that doesn't seem to be what we do. Or am I missing something?

Comments:

Patch:
diff --git a/boule/sphere.py b/boule/sphere.py
index c099e62..166c15f 100644
--- a/boule/sphere.py
+++ b/boule/sphere.py
@@ -12,11 +12,11 @@ from . import Ellipsoid
 @attr.s(frozen=True)
 class Sphere(Ellipsoid):
     """
-    Reference spherical ellipsoid
+    Reference sphere (zero flattening ellipsoids)
 
-    Represents ellipsoids with zero flattening (spheres). Inherits methods and
-    properties of the :class:`boule.Ellipsoid`, guaranteeing no singularities
-    due to zero flattening (and thus zero eccentricity).
+    Inherits methods and properties of the :class:`boule.Ellipsoid` while
+    guaranteeing no singularities due to zero flattening (and thus zero
+    eccentricity) in gravity calculations.
 
     All parameters are in SI units.
 
@@ -56,6 +56,13 @@ class Sphere(Ellipsoid):
     1.00
     >>> print("{:.2f}".format(sphere.mean_radius))
     1.00
+    >>> print(sphere.normal_gravity(latitude=0, height=1))
+    25000.0
+    >>> print(sphere.normal_gravity(latitude=90, height=1))
+    25000.0
+
+    The flattening and eccentricities will all be zero:
+
     >>> print("{:.2f}".format(sphere.flattening))
     0.00
     >>> print("{:.2f}".format(sphere.linear_eccentricity))
@@ -64,10 +71,7 @@ class Sphere(Ellipsoid):
     0.00
     >>> print("{:.2f}".format(sphere.second_eccentricity))
     0.00
-    >>> print(sphere.normal_gravity(latitude=0, height=1))
-    25000.0
-    >>> print(sphere.normal_gravity(latitude=90, height=1))
-    25000.0
+
     """
 
     name = attr.ib()
@@ -88,19 +92,39 @@ class Sphere(Ellipsoid):
         return self.radius
 
     def normal_gravity(self, latitude, height):
-        """
+        r"""
         Calculate normal gravity at any latitude and height
 
         Computes the magnitude of the gradient of the gravity potential
         (gravitational + centrifugal) generated by the sphere at the given
-        latitude and height.
+        latitude :math:`\theta` and height :math:`h`:
+
+        .. math::
+
+            \gamma(\theta, h) =
+            \sqrt{\left( \frac{GM}{(R + h)^2} \right)^2
+            + \left(\omega^2 (R + h) - 2\frac{GM}{(R + h)^2} \right)
+            \omega^2 (R + h) \cos^2 \theta}
+
+        in which :math:`R` is the sphere radius, :math:`G` is the gravitational
+        constant, :math:`M` is the mass of the sphere, and :math:`\omega` is
+        the angular velocity.
+
+        .. note::
+
+            A sphere under rotation is not in hydrostatic equilibrium.
+            Therefore, it is not it's own equipotential gravity surface (as is
+            the case for the ellipsoid) and the normal gravity vector is not
+            normal to the surface of the sphere.
 
         Parameters
         ----------
         latitude : float or array
-            The latitude where the normal gravity will be computed (in degrees).
+            The latitude where the normal gravity will be computed (in
+            degrees). For a reference sphere there is no difference between
+            geodetic and spherical latitudes.
         height : float or array
-            The height (above the sphere) of computation point (in meters).
+            The height (above the sphere) of the computation point (in meters).
 
         Returns
         -------
@@ -110,24 +134,17 @@ class Sphere(Ellipsoid):
         References
         ----------
         [Heiskanen-Moritz]_
-        """
-        return self._gravity_sphere(height) + self._centrifugal_force(latitude, height)
 
-    def _gravity_sphere(self, height):
-        """
-        Calculate the gravity generated by a solid sphere (mGal)
-        """
-        return 1e5 * self.geocentric_grav_const / (self.radius + height) ** 2
-
-    def _centrifugal_force(self, latitude, height):
-        """
-        Calculate the centrifugal force due to the rotation of the sphere (mGal)
         """
-        return 1e5 * (
-            (-1)
+        radial_distance = self.radius + height
+        gravity_acceleration = self.geocentric_grav_const / (radial_distance) ** 2
+        return 1e5 * np.sqrt(
+            gravity_acceleration ** 2
+            + (self.angular_velocity ** 2 * radial_distance - 2 * gravity_acceleration)
             * self.angular_velocity ** 2
-            * (self.radius + height)
-            * np.cos(np.radians(latitude))
+            * radial_distance
+            # replace cos^2 with (1 - sin^2) for more accurate results on the pole
+            * (1 - np.sin(np.radians(latitude)) ** 2)
         )
 
     @property


Test patch:
diff --git a/boule/tests/test_ellipsoid.py b/boule/tests/test_ellipsoid.py
index bc74dac..cf833f9 100644
--- a/boule/tests/test_ellipsoid.py
+++ b/boule/tests/test_ellipsoid.py
@@ -248,7 +248,10 @@ def test_normal_gravity_against_somigliana(ellipsoid):
     Check if normal gravity on the surface satisfies Somigliana equation
     """
     latitude = np.linspace(-90, 90, 181)
-    npt.assert_allclose(
-        ellipsoid.normal_gravity(latitude, height=0),
-        normal_gravity_surface(latitude, ellipsoid),
-    )
+    # Somigliana equation applies only to ellipsoids that are their own
+    # equipotential gravity surface. Spheres (with zero flattening) aren't.
+    if ellipsoid.flattening != 0:
+        npt.assert_allclose(
+            ellipsoid.normal_gravity(latitude, height=0),
+            normal_gravity_surface(latitude, ellipsoid),
+        )
diff --git a/boule/tests/test_sphere.py b/boule/tests/test_sphere.py
index cb7676e..17f0c1a 100644
--- a/boule/tests/test_sphere.py
+++ b/boule/tests/test_sphere.py
@@ -13,7 +13,10 @@ from .. import Sphere
 def sphere():
     "A spherical ellipsoid"
     ellipsoid = Sphere(
-        name="unit_sphere", radius=1.0, geocentric_grav_const=2.0, angular_velocity=1.3,
+        name="unit_sphere",
+        radius=1.0,
+        geocentric_grav_const=2.0,
+        angular_velocity=1.3,
     )
     return ellipsoid
 
@@ -141,22 +144,29 @@ def test_normal_gravity_only_rotation():
     """
     radius = 1
     omega = 2
+    heights = [1, 100, 1000]
     sphere = Sphere(
         name="sphere", radius=radius, geocentric_grav_const=0, angular_velocity=omega
     )
     # Check normal gravity on the equator
-    for height in [1, 2, 3, 4]:
-        expected_value = -1e5 * (omega ** 2) * (radius + height)
+    # Expected value is positive because normal gravity is the norm of the
+    # vector.
+    for height in heights:
+        expected_value = 1e5 * (omega ** 2) * (radius + height)
         npt.assert_allclose(
-            expected_value, sphere.normal_gravity(latitude=0, height=height),
+            expected_value,
+            sphere.normal_gravity(latitude=0, height=height),
         )
     # Check normal gravity on the poles (must be equal to zero)
-    for height in [1, 2, 3, 4]:
+    for height in heights:
         assert sphere.normal_gravity(latitude=90, height=height) < 1e-15
         assert sphere.normal_gravity(latitude=-90, height=height) < 1e-15
     # Check normal gravity at 45 degrees latitude
-    for height in [1, 2, 3, 4]:
-        expected_value = -1e5 * (omega ** 2) * (radius + height) * np.sqrt(2) / 2
+    # Expected value is positive because normal gravity is the norm of the
+    # vector.
+    for height in heights:
+        expected_value = 1e5 * (omega ** 2) * (radius + height) * np.sqrt(2) / 2
         npt.assert_allclose(
-            expected_value, sphere.normal_gravity(latitude=45, height=height),
+            expected_value,
+            sphere.normal_gravity(latitude=45, height=height),
         )