Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

additions to sphere module #271

Merged
merged 12 commits into from
Sep 21, 2023
169 changes: 168 additions & 1 deletion clouddrift/sphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
"""

import numpy as np
from typing import Optional, Tuple
from typing import Optional, Tuple, Union
import xarray as xr
import warnings

EARTH_RADIUS_METERS = 6.3781e6
EARTH_ROTATION_RATE = 7.2921159e-5
philippemiron marked this conversation as resolved.
Show resolved Hide resolved


def distance(
Expand Down Expand Up @@ -566,3 +568,168 @@ def cartesian_to_spherical(
lat = np.rad2deg(np.arcsin(z))

return lon, lat


def cartesian_to_tangentplane(
u: Union[float, np.ndarray],
v: Union[float, np.ndarray],
w: Union[float, np.ndarray],
longitude: Union[float, np.ndarray],
latitude: Union[float, np.ndarray],
) -> Union[Tuple[float], Tuple[np.ndarray]]:
"""
Project a three-dimensional Cartesian vector on a plane tangent to
a spherical Earth.

The Cartesian coordinate system is a right-handed system whose
origin lies at the center of a sphere. It is oriented with the
Z-axis passing through the north pole at lat = 90, the X-axis passing through
the point lon = 0, lat = 0, and the Y-axis passing through the point lon = 90,
lat = 0.

Parameters
----------
u : float or np.ndarray
First component of Cartesian vector.
v : float or np.ndarray
Second component of Cartesian vector.
w : float or np.ndarray
Third component of Cartesian vector.
longitude : float or np.ndarray
Longitude in degrees of tangent point of plane.
latitude : float or np.ndarray
Latitude in degrees of tangent point of plane.

Returns
-------

milancurcic marked this conversation as resolved.
Show resolved Hide resolved
up: float or np.ndarray
First component of projected vector on tangent plane (positive eastward).
vp: float or np.ndarray
Second component of projected vector on tangent plane (positive northward).

Raises
------
Warning
Raised if the input latitude is not in the expected range [-90, 90].

Examples
--------
>>> u, v = cartesian_to_tangentplane(1, 1, 1, 45, 90)

See Also
--------
:func:`tangentplane_to_cartesian`
"""
if np.any(latitude < -90) or np.any(latitude > 90):
warnings.warn("Input latitude outside of range [-90,90].")

phi = np.radians(latitude)
theta = np.radians(longitude)
u_projected = v * np.cos(theta) - u * np.sin(theta)
v_projected = (
w * np.cos(phi)
- u * np.cos(theta) * np.sin(phi)
- v * np.sin(theta) * np.sin(phi)
)
# JML says vh = w.*cos(phi)-u.*cos(theta).*sin(phi)-v.*sin(theta).*sin(phi) but vh=w./cos(phi) is the same
return u_projected, v_projected


def tangentplane_to_cartesian(
up: Union[float, np.ndarray],
vp: Union[float, np.ndarray],
longitude: Union[float, np.ndarray],
latitude: Union[float, np.ndarray],
) -> Union[Tuple[float], Tuple[np.ndarray]]:
"""
Return the three-dimensional Cartesian components of a vector conatined in
milancurcic marked this conversation as resolved.
Show resolved Hide resolved
a plane tangent to a spherical Earth.
milancurcic marked this conversation as resolved.
Show resolved Hide resolved

The Cartesian coordinate system is a right-handed system whose
origin lies at the center of a sphere. It is oriented with the
Z-axis passing through the north pole at lat = 90, the X-axis passing through
the point lon = 0, lat = 0, and the Y-axis passing through the point lon = 90,
lat = 0.

Parameters
----------
up: float or np.ndarray
First component of vector on tangent plane (positive eastward).
vp: float or np.ndarray
Second component of vector on tangent plane (positive northward).
longitude : float or np.ndarray
Longitude in degrees of tangent point of plane.
latitude : float or np.ndarray
Latitude in degrees of tangent point of plane.

Returns
-------
u : float or np.ndarray
First component of Cartesian vector.
v : float or np.ndarray
Second component of Cartesian vector.
w : float or np.ndarray
Third component of Cartesian vector.

Raises
------
Warning
Raised if the input latitude is not in the expected range [-90, 90].

milancurcic marked this conversation as resolved.
Show resolved Hide resolved
Examples
--------
>>> u, v, w = tangentplane_to_cartesian(1, 1, 45, 90)

Notes
-----
This function is inverted by `cartesian_to_tangetplane`.

See Also
--------
:func:`cartesian_to_tangentplane`
"""
if np.any(latitude < -90) or np.any(latitude > 90):
warnings.warn("Input latitude outside of range [-90,90].")
selipot marked this conversation as resolved.
Show resolved Hide resolved

phi = np.radians(latitude)
theta = np.radians(longitude)
u = -up * np.sin(theta) - vp * np.sin(phi) * np.cos(theta)
v = up * np.cos(theta) - vp * np.sin(phi) * np.sin(theta)
w = vp * np.cos(phi)

return u, v, w


def coriolis_frequency(
latitude: Union[float, np.ndarray],
) -> Union[float, np.ndarray]:
"""
Return the Coriolis frequency or commonly known `f` parameter in geophysical fluid dynamics.

Parameters
----------
latitude : float or np.ndarray
Latitude in degrees.

Returns
------
f : float or np.ndarray
Signed Coriolis frequency in radian per seconds.

Raises
------
Warning
Raised if the input latitudes are not in the expected range [-90, 90].

Examples
--------
>>> f = coriolis_frequency(np.array([0, 45, 90]))

"""
f = 2 * EARTH_ROTATION_RATE * np.sin(np.radians(latitude))

if np.any(latitude < -90) or np.any(latitude > 90):
warnings.warn("Input latitudes outside of range [-90,90].")
selipot marked this conversation as resolved.
Show resolved Hide resolved

return f
3 changes: 2 additions & 1 deletion clouddrift/wavelet.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ def morse_logspace_freq(

>>> radian_frequency = morse_logspace_freq(3,5,1024,highset=(0.2,np.pi),lowset=(5,0),density=10)

See Also
See Also
--------
:func:`morse_wavelet`, `morse_freq`, `morse_properties`.
"""
Expand Down Expand Up @@ -725,6 +725,7 @@ def morse_properties(
kurt: np.ndarray or float
Normalized fourth moment of the time-domain demodulate,
or 'demodulate kurtosis'.

See Also
--------
:func:`morse_wavelet`, `morse_freq`, `morse_amplitude`, `morse_logspace_freq`.
Expand Down
11 changes: 9 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,22 @@ RaggedArray
:undoc-members:

Sphere
---------
------

.. automodule:: clouddrift.sphere
:members:
:undoc-members:

Signal
---------
------

.. automodule:: clouddrift.signal
:members:
:undoc-members:

Wavelet
-------

.. automodule:: clouddrift.wavelet
:members:
:undoc-members:
73 changes: 73 additions & 0 deletions tests/sphere_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
position_from_distance_and_bearing,
spherical_to_cartesian,
cartesian_to_spherical,
tangentplane_to_cartesian,
cartesian_to_tangentplane,
coriolis_frequency,
EARTH_RADIUS_METERS,
)
import unittest
Expand Down Expand Up @@ -354,3 +357,73 @@ def test_cartesian_to_spherical_invert(self):
self.assertTrue(np.allclose(x, x2))
self.assertTrue(np.allclose(y, y2))
self.assertTrue(np.allclose(z, z2))


class cartesian_to_tangentplane_tests(unittest.TestCase):
def test_cartesian_to_tangentplane_values(self):
up, vp = cartesian_to_tangentplane(1.0, 1.0, 1.0, 0.0, 0.0)
self.assertTrue(np.allclose((up, vp), (1.0, 1.0)))
up, vp = cartesian_to_tangentplane(1.0, 1.0, 1.0, 90.0, 0.0)
self.assertTrue(np.allclose((up, vp), (-1.0, 1.0)))
up, vp = cartesian_to_tangentplane(1.0, 1.0, 1.0, 180.0, 0.0)
self.assertTrue(np.allclose((up, vp), (-1.0, 1.0)))
up, vp = cartesian_to_tangentplane(1.0, 1.0, 1.0, 270.0, 0.0)
self.assertTrue(np.allclose((up, vp), (1.0, 1.0)))
up, vp = cartesian_to_tangentplane(1.0, 1.0, 1.0, 0.0, 90.0)
self.assertTrue(np.allclose((up, vp), (1.0, -1.0)))
up, vp = cartesian_to_tangentplane(1.0, 1.0, 1.0, 0.0, -90.0)
self.assertTrue(np.allclose((up, vp), (1.0, 1.0)))

def test_cartesian_to_tangentplane_warning(self):
with self.assertWarns(Warning):
cartesian_to_tangentplane(1, 1, 1, 0, 91)
with self.assertWarns(Warning):
cartesian_to_tangentplane(1, 1, 1, 0, -91)


class tangentplane_to_cartesian_tests(unittest.TestCase):
def test_tangentplane_to_cartesian_values(self):
uvw = tangentplane_to_cartesian(1, 1, 0, 0)
self.assertTrue(np.allclose(uvw, (0, 1, 1)))
uvw = tangentplane_to_cartesian(1, 1, 90, 0)
self.assertTrue(np.allclose(uvw, (-1, 0, 1)))
uvw = tangentplane_to_cartesian(1, 1, 180, 0)
self.assertTrue(np.allclose(uvw, (0, -1, 1)))
uvw = tangentplane_to_cartesian(1, 1, 270, 0)
self.assertTrue(np.allclose(uvw, (1, 0, 1)))
uvw = tangentplane_to_cartesian(1, 1, 0, 90)
self.assertTrue(np.allclose(uvw, (-1, 1, 0)))
uvw = tangentplane_to_cartesian(1, 1, 0, -90)
self.assertTrue(np.allclose(uvw, (1, 1, 0)))

def test_tangentplane_to_cartesian_inverse(self):
u, v, w = tangentplane_to_cartesian(1, 1, 45, 45)
self.assertTrue(np.allclose((1, 1), cartesian_to_tangentplane(u, v, w, 45, 45)))

def test_tangentplane_to_cartesian_warning(self):
with self.assertWarns(Warning):
tangentplane_to_cartesian(1, 1, 0, 91)
with self.assertWarns(Warning):
tangentplane_to_cartesian(1, 1, 0, -91)


class coriolis_frequency_tests(unittest.TestCase):
def test_coriolis_frequency_values(self):
f = coriolis_frequency(np.array([-90, -60, -30, 0, 45, 90]))
f_expected = np.array(
[
-0.000145842318,
-0.000126303152,
-7.2921159e-05,
0,
0.000103126092,
0.000145842318,
]
)
self.assertTrue(np.allclose(f, f_expected))

def test_coriolis_frequency_warning(self):
with self.assertWarns(Warning):
coriolis_frequency(91)
with self.assertWarns(Warning):
coriolis_frequency(-91)