Repository: fatiando/harmonica

Issue:
Function to transform geodetic to geocentric spherical coordinates
**Description of the desired feature**

The tesseroid modeling is done in geocentric spherical coordinates (longitude, geocentric latitude, radius) but the data is usually in geodetic coordinates (longitude, geodetic latitude, geometric height). Proj4 apparently only converts the latitudes (https://proj4.org/operations/conversions/geoc.html) but not the height to radius. We need a function that does this conversion so that we can use tesseroids appropriately. 

The function will use the current ellipsoid (`get_ellipsoid`) to do the conversion. Tests can use simplified ellipsoids to check that the computations are correct (like setting the flattening to 1 and axis to an integer). It could go into a new `harmonica/coordinates.py` module and should look like:

```python
def geodetic_to_geocentric(latitude, height):
    ...
    return geocentric_latitude, radius
``` 

Comments:

Patch:
diff --git a/doc/api/index.rst b/doc/api/index.rst
index ae14745..431219c 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -26,6 +26,14 @@ Reference Ellipsoids
     get_ellipsoid
     print_ellipsoids
 
+Coordinates Conversions
+-----------------------
+
+.. autosummary::
+   :toctree: generated/
+
+    geodetic_to_spherical
+
 Input and Output
 ----------------
 
diff --git a/doc/references.rst b/doc/references.rst
index 094e5ee..aef6372 100644
--- a/doc/references.rst
+++ b/doc/references.rst
@@ -6,3 +6,4 @@ References
 .. [Forste_etal2014] Förste, Christoph; Bruinsma, Sean.L.; Abrikosov, Oleg; Lemoine, Jean-Michel; Marty, Jean Charles; Flechtner, Frank; Balmino, G.; Barthelmes, F.; Biancale, R. (2014): EIGEN-6C4 The latest combined global gravity field model including GOCE data up to degree and order 2190 of GFZ Potsdam and GRGS Toulouse. GFZ Data Services. doi:`10.5880/icgem.2015.1 <http://doi.org/10.5880/icgem.2015.1>`__
 .. [Hofmann-WellenhofMoritz2006] Hofmann-Wellenhof, B., & Moritz, H. (2006). Physical Geodesy (2nd, corr. ed. 2006 edition ed.). Wien ; New York: Springer.
 .. [LiGotze2001] Li, X. and H. J. Gotze, 2001, Tutorial: Ellipsoid, geoid, gravity, geodesy, and geophysics, Geophysics, 66(6), p. 1660-1668, doi:`10.1190/1.1487109 <https://doi.org/10.1190/1.1487109>`__
+.. [Vermeille2002] Vermeille, H., 2002. Direct transformation from geocentric coordinates to geodetic coordinates. Journal of Geodesy. 76. 451-454. doi:`10.1007/s00190-002-0273-6 <https://doi.org/10.1007/s00190-002-0273-6>`__
diff --git a/harmonica/__init__.py b/harmonica/__init__.py
index 1945fba..5fa22ef 100644
--- a/harmonica/__init__.py
+++ b/harmonica/__init__.py
@@ -10,6 +10,7 @@ from .ellipsoid import (
 )
 from .io import load_icgem_gdf
 from .gravity_corrections import normal_gravity
+from .coordinates import geodetic_to_spherical
 
 
 # Get the version number through versioneer
diff --git a/harmonica/coordinates.py b/harmonica/coordinates.py
new file mode 100644
index 0000000..cad1259
--- /dev/null
+++ b/harmonica/coordinates.py
@@ -0,0 +1,46 @@
+"""
+Geographic coordinate conversion.
+"""
+import numpy as np
+
+from . import get_ellipsoid
+
+
+def geodetic_to_spherical(latitude, height):
+    """
+    Convert from geodetic to geocentric spherical coordinates.
+
+    The geodetic datum is defined by the default :class:`harmonica.ReferenceEllipsoid`
+    set by the :func:`harmonica.set_ellipsoid` function.
+    The coordinates are converted following [Vermeille2002]_.
+
+    Parameters
+    ----------
+    latitude : float or array
+        The geodetic latitude (in degrees).
+    height : float or array
+        The ellipsoidal (geometric or geodetic) height (in meters).
+
+    Returns
+    -------
+    geocentric_latitude : float or array
+        The latitude coordinate in the geocentric spherical reference system
+        (in degrees).
+    radius : float or array
+        The radial coordinate in the geocentric spherical reference system (in meters).
+    """
+    ellipsoid = get_ellipsoid()
+    # Convert latitude to radians
+    latitude_rad = np.radians(latitude)
+    prime_vertical_radius = ellipsoid.semimajor_axis / np.sqrt(
+        1 - ellipsoid.first_eccentricity ** 2 * np.sin(latitude_rad) ** 2
+    )
+    # Instead of computing X and Y, we only comupute the projection on the XY plane:
+    # xy_projection = sqrt( X**2 + Y**2 )
+    xy_projection = (height + prime_vertical_radius) * np.cos(latitude_rad)
+    z_cartesian = (
+        height + (1 - ellipsoid.first_eccentricity ** 2) * prime_vertical_radius
+    ) * np.sin(latitude_rad)
+    radius = np.sqrt(xy_projection ** 2 + z_cartesian ** 2)
+    geocentric_latitude = 180 / np.pi * np.arcsin(z_cartesian / radius)
+    return geocentric_latitude, radius


Test patch:
diff --git a/harmonica/tests/test_coordinates.py b/harmonica/tests/test_coordinates.py
new file mode 100644
index 0000000..1f8beb5
--- /dev/null
+++ b/harmonica/tests/test_coordinates.py
@@ -0,0 +1,56 @@
+"""
+Test coordinates conversions.
+"""
+import numpy as np
+import numpy.testing as npt
+
+from ..coordinates import geodetic_to_spherical
+from ..ellipsoid import (
+    KNOWN_ELLIPSOIDS,
+    set_ellipsoid,
+    get_ellipsoid,
+    ReferenceEllipsoid,
+)
+
+
+def test_geodetic_to_spherical_with_spherical_ellipsoid():
+    "Test geodetic to geocentric coordinates conversion if ellipsoid is a sphere."
+    rtol = 1e-10
+    sphere_radius = 1.0
+    # Define a "spherical" ellipsoid with radius equal to 1m.
+    # To do so, we define a zero flattening, thus an infinite inverse flattening.
+    spherical_ellipsoid = ReferenceEllipsoid(
+        "unit_sphere", sphere_radius, np.infty, 0, 0
+    )
+    with set_ellipsoid(spherical_ellipsoid):
+        latitude = np.linspace(-90, 90, 5)
+        height = np.linspace(-0.2, 0.2, 5)
+        geocentric_latitude, radius = geodetic_to_spherical(latitude, height)
+        npt.assert_allclose(geocentric_latitude, latitude, rtol=rtol)
+        npt.assert_allclose(radius, sphere_radius + height, rtol=rtol)
+
+
+def test_geodetic_to_spherical_on_equator():
+    "Test geodetic to geocentric coordinates conversion on equator."
+    rtol = 1e-10
+    height = np.linspace(-1e4, 1e4, 5)
+    latitude = np.full(height.shape, 0)
+    for ellipsoid_name in KNOWN_ELLIPSOIDS:
+        with set_ellipsoid(ellipsoid_name):
+            ellipsoid = get_ellipsoid()
+            geocentric_latitude, radius = geodetic_to_spherical(latitude, height)
+            npt.assert_allclose(geocentric_latitude, latitude, rtol=rtol)
+            npt.assert_allclose(radius, height + ellipsoid.semimajor_axis, rtol=rtol)
+
+
+def test_geodetic_to_spherical_on_poles():
+    "Test geodetic to geocentric coordinates conversion on poles."
+    rtol = 1e-10
+    height = np.hstack([np.linspace(-1e4, 1e4, 5)] * 2)
+    latitude = np.array([90.0] * 5 + [-90.0] * 5)
+    for ellipsoid_name in KNOWN_ELLIPSOIDS:
+        with set_ellipsoid(ellipsoid_name):
+            ellipsoid = get_ellipsoid()
+            geocentric_latitude, radius = geodetic_to_spherical(latitude, height)
+            npt.assert_allclose(geocentric_latitude, latitude, rtol=rtol)
+            npt.assert_allclose(radius, height + ellipsoid.semiminor_axis, rtol=rtol)