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

Simplify angle_between + refactor quadrilateral_area #65

Merged
merged 25 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ea9bbbe
simplify angle_between
navidcy Sep 5, 2023
812a302
black format
navidcy Sep 5, 2023
3d3179a
remove random unit vector; add 2 more tests
navidcy Sep 5, 2023
a8c9ad4
Merge branch 'main' into ncc/simplify-angle-between
navidcy Sep 5, 2023
6721c47
refactor quadrilateral_area
navidcy Sep 5, 2023
d414ea1
black format
navidcy Sep 5, 2023
30254bd
merge main
navidcy Sep 5, 2023
9ad060e
adds comment
navidcy Sep 5, 2023
ad69968
better docstring for quadilateral_areas
navidcy Sep 5, 2023
d34d2f8
add test for quadilateral_area
navidcy Sep 5, 2023
283778a
add latlon_to_cartesian + test
navidcy Sep 5, 2023
4cf6a3b
one more test for quadilateral_area
navidcy Sep 5, 2023
d4f8668
remove debuggint print statements
navidcy Sep 5, 2023
2366869
cleaner test_quadilateral_areas
navidcy Sep 5, 2023
b5877f3
add clarification in latlon_to_cartesian doc
navidcy Sep 5, 2023
00ad0b5
add clarification in latlon_to_cartesian doc
navidcy Sep 5, 2023
2ee793d
add clarification in angle_between docs
navidcy Sep 5, 2023
d091f32
latlon_to_cartesian, quadilateral_area(s) now know R
navidcy Sep 6, 2023
a18ff47
black formating
navidcy Sep 6, 2023
3147a51
correct phrasing in docstring
navidcy Sep 6, 2023
139ac70
better exception error
navidcy Sep 6, 2023
aa5ed7a
test the exception error by quadilateral_area
navidcy Sep 6, 2023
c423f52
Exception -> ValueError
navidcy Sep 6, 2023
dccdede
Implement quadrilateral_areas using broadcasting
angus-g Sep 6, 2023
8d45e19
Fix quadrilateral area exception test
angus-g Sep 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 66 additions & 36 deletions regional_mom6/regional_mom6.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from dask.diagnostics import ProgressBar
import datetime as dt
import warnings
from .utils import vecdot

warnings.filterwarnings("ignore")

Expand All @@ -21,7 +22,9 @@
"motu_requests",
"dz",
"angle_between",
"quadilateral_area",
"latlon_to_cartesian",
"quadrilateral_area",
"quadrilateral_areas",
"rectangular_hgrid",
"experiment",
"segment",
Expand Down Expand Up @@ -261,7 +264,6 @@ def dz(npoints, ratio, target_depth, min_dz=0.0001, tolerance=1):

Returns:
numpy.array: An array containing the thickness profile.

"""

profile = min_dz + 0.5 * (np.abs(ratio) * min_dz - min_dz) * (
Expand All @@ -279,47 +281,73 @@ def dz(npoints, ratio, target_depth, min_dz=0.0001, tolerance=1):
return dz(npoints, ratio, target_depth, min_dz * err_ratio)


# Borrowed from grid tools (GFDL)
def angle_between(v1, v2, v3):
"""Returns angle v2-v1-v3 i.e betweeen v1-v2 and v1-v3."""
"""Returns the angle v2-v1-v3 (in radians). That is the angle between vectors v1-v2 and v1-v3."""

v1xv2 = np.cross(v1, v2)
v1xv3 = np.cross(v1, v3)

cosangle = vecdot(v1xv2, v1xv3) / np.sqrt(
vecdot(v1xv2, v1xv2) * vecdot(v1xv3, v1xv3)
)

return np.arccos(cosangle)


def quadrilateral_area(v1, v2, v3, v4):
"""Returns area of a spherical quadrilateral on the unit sphere that
has vertices on 3-vectors `v1`, `v2`, `v3`, `v4` (counter-clockwise
orientation is implied). The area is computed via the excess of the
sum of the spherical angles of the quadrilateral from 2π."""

if not (
np.all(np.isclose(vecdot(v1, v1), vecdot(v2, v2)))
& np.all(np.isclose(vecdot(v1, v1), vecdot(v2, v2)))
& np.all(np.isclose(vecdot(v1, v1), vecdot(v3, v3)))
& np.all(np.isclose(vecdot(v1, v1), vecdot(v4, v4)))
):
raise ValueError("vectors provided must have the same length")

# vector product between v1 and v2
px = v1[1] * v2[2] - v1[2] * v2[1]
py = v1[2] * v2[0] - v1[0] * v2[2]
pz = v1[0] * v2[1] - v1[1] * v2[0]
# vector product between v1 and v3
qx = v1[1] * v3[2] - v1[2] * v3[1]
qy = v1[2] * v3[0] - v1[0] * v3[2]
qz = v1[0] * v3[1] - v1[1] * v3[0]
R = np.sqrt(vecdot(v1, v1))

ddd = (px * px + py * py + pz * pz) * (qx * qx + qy * qy + qz * qz)
ddd = (px * qx + py * qy + pz * qz) / np.sqrt(ddd)
angle = np.arccos(ddd)
a1 = angle_between(v1, v2, v4)
a2 = angle_between(v2, v3, v1)
a3 = angle_between(v3, v4, v2)
a4 = angle_between(v4, v1, v3)

return angle
return (a1 + a2 + a3 + a4 - 2 * np.pi) * R**2


# Borrowed from grid tools (GFDL)
def quadilateral_area(lat, lon):
"""Returns area of spherical quadrilaterals on the unit sphere that are formed
by constant latitude and longitude lines on the `lat`-`lon` grid provided."""
def latlon_to_cartesian(lat, lon, R=1):
"""Convert latitude-longitude (in degrees) to Cartesian coordinates on a sphere of radius `R`.
By default `R = 1`."""

# x, y, z are 3D coordinates on the unit sphere
x = np.cos(np.deg2rad(lat)) * np.cos(np.deg2rad(lon))
y = np.cos(np.deg2rad(lat)) * np.sin(np.deg2rad(lon))
z = np.sin(np.deg2rad(lat))
x = R * np.cos(np.deg2rad(lat)) * np.cos(np.deg2rad(lon))
y = R * np.cos(np.deg2rad(lat)) * np.sin(np.deg2rad(lon))
z = R * np.sin(np.deg2rad(lat))

c1 = (x[:-1, :-1], y[:-1, :-1], z[:-1, :-1])
c2 = (x[:-1, 1:], y[:-1, 1:], z[:-1, 1:])
c3 = (x[1:, 1:], y[1:, 1:], z[1:, 1:])
c4 = (x[1:, :-1], y[1:, :-1], z[1:, :-1])
return x, y, z

a1 = angle_between(c2, c1, c3)
a2 = angle_between(c3, c2, c4)
a3 = angle_between(c4, c3, c1)
a4 = angle_between(c1, c4, c2)

return a1 + a2 + a3 + a4 - 2.0 * np.pi
def quadrilateral_areas(lat, lon, R=1):
"""Returns area of spherical quadrilaterals on a sphere of radius `R`. By default, `R = 1`.
The quadrilaterals are formed by constant latitude and longitude lines on the `lat`-`lon` grid provided.

Args:
lat (array): Array of latitude points (in degrees)
lon (array): Array of longitude points (in degrees)

Returns:
areas (array): Array with the areas of the quadrilaterals defined by the
`lat`-`lon` grid provided. If the `lat`-`lon` are `m x n`
then `areas` is `(m-1) x (n-1)`.
"""

coords = np.dstack(latlon_to_cartesian(lat, lon, R))

return quadrilateral_area(
coords[:-1, :-1, :], coords[:-1, 1:, :], coords[1:, 1:, :], coords[1:, :-1, :]
)


def rectangular_hgrid(λ, φ):
Expand All @@ -346,18 +374,20 @@ def rectangular_hgrid(λ, φ):

dλ = λ[1] - λ[0] # assuming that longitude is uniformly spaced

# dx = R * cos(φ) * np.deg2rad(dλ) / 2. Note, we divide dy by 2 because we're on the supergrid
# dx = R * cos(φ) * np.deg2rad(dλ) / 2
# Note: division by 2 because we're on the supergrid
dx = np.broadcast_to(
R * np.cos(np.deg2rad(φ)) * np.deg2rad(dλ) / 2,
(λ.shape[0] - 1, φ.shape[0]),
).T

# dy = R * np.deg2rad(dφ) / 2. Note, we divide dy by 2 because we're on the supergrid
# dy = R * np.deg2rad(dφ) / 2
# Note: division by 2 because we're on the supergrid
dy = np.broadcast_to(R * np.deg2rad(np.diff(φ)) / 2, (λ.shape[0], φ.shape[0] - 1)).T

lon, lat = np.meshgrid(λ, φ)

area = quadilateral_area(lat, lon) * R**2
area = quadrilateral_areas(lat, lon, R)

attrs = {
"tile": {
Expand Down
5 changes: 5 additions & 0 deletions regional_mom6/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import numpy as np


def vecdot(v1, v2):
return np.sum(v1 * v2, axis=-1)
65 changes: 60 additions & 5 deletions tests/test_grid_generation.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,93 @@
import numpy as np
import pytest
from regional_mom6 import angle_between
from regional_mom6 import quadilateral_area
from regional_mom6 import latlon_to_cartesian
from regional_mom6 import quadrilateral_area
from regional_mom6 import quadrilateral_areas
from regional_mom6 import rectangular_hgrid
import xarray as xr


@pytest.mark.parametrize(
("lat", "lon", "true_xyz"),
[
(0, 0, (1, 0, 0)),
(90, 0, (0, 0, 1)),
(0, 90, (0, 1, 0)),
(-90, 0, (0, 0, -1)),
],
)
def test_latlon_to_cartesian(lat, lon, true_xyz):
assert np.isclose(latlon_to_cartesian(lat, lon), true_xyz).all()


@pytest.mark.parametrize(
("v1", "v2", "v3", "true_angle"),
[
([1, 0, 0], [0, 1, 0], [0, 0, 1], np.pi / 2),
([1, 0, 0], [1, 1, 0], [0, 1, 1], np.pi / 4),
([1, 0, 0], [1, 1, 1], [0, 0, 1], np.pi / 4),
([1, 1, 1], [1, 1, 0], [0, 1, 1], 2 * np.pi / 3),
],
)
def test_angle_between(v1, v2, v3, true_angle):
assert np.isclose(angle_between(v1, v2, v3), true_angle)


@pytest.mark.parametrize(
("v1", "v2", "v3", "v4", "true_area"),
[
(
np.dstack(latlon_to_cartesian(0, 0)),
np.dstack(latlon_to_cartesian(0, 90)),
np.dstack(latlon_to_cartesian(90, 0)),
np.dstack(latlon_to_cartesian(0, -90)),
np.pi,
),
(
np.dstack(latlon_to_cartesian(0, 0)),
np.dstack(latlon_to_cartesian(90, 0)),
np.dstack(latlon_to_cartesian(0, 90)),
np.dstack(latlon_to_cartesian(-90, 0)),
np.pi,
),
],
)
def test_quadrilateral_area(v1, v2, v3, v4, true_area):
assert np.isclose(quadrilateral_area(v1, v2, v3, v4), true_area)


v1 = np.dstack(latlon_to_cartesian(0, 0, R=2))
v2 = np.dstack(latlon_to_cartesian(90, 0, R=2))
v3 = np.dstack(latlon_to_cartesian(0, 90, R=2))
v4 = np.dstack(latlon_to_cartesian(-90, 0, R=2.1))


def test_quadrilateral_area_exception():
with pytest.raises(ValueError) as excinfo:
quadrilateral_area(v1, v2, v3, v4)

assert str(excinfo.value) == "vectors provided must have the same length"


# create a lat-lon mesh that covers 1/4 of the North Hemisphere
lon1, lat1 = np.meshgrid(np.linspace(0, 90, 5), np.linspace(0, 90, 5))
area1 = 1 / 8 * (4 * np.pi)

# create a lat-lon mesh that covers 1/4 of the whole globe
lon2, lat2 = np.meshgrid(np.linspace(-45, 45, 5), np.linspace(-90, 90, 5))
area2 = 1 / 4 * (4 * np.pi)


@pytest.mark.parametrize(
("lat", "lon", "true_area"),
[
(lat1, lon1, 0.5 * np.pi),
(lat2, lon2, np.pi),
(lat1, lon1, area1),
(lat2, lon2, area2),
],
)
def test_quadilateral_area(lat, lon, true_area):
assert np.isclose(np.sum(quadilateral_area(lat, lon)), true_area)
def test_quadrilateral_areas(lat, lon, true_area):
assert np.isclose(np.sum(quadrilateral_areas(lat, lon)), true_area)


# a simple test that rectangular_hgrid runs without erroring
Expand Down