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

Add enhanced support for geographic coordinates #181

Merged
merged 56 commits into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
9f9262b
Add latlon option to inside() function
santisoler Apr 16, 2019
81530a2
Fix wrong coordinate variable in latlon_continuity
santisoler Apr 16, 2019
94a5e8e
Add test case around zero meridian
santisoler Apr 16, 2019
db1e816
Remove unwrapping from latlon_continuity function
santisoler Apr 16, 2019
a43b415
Refactor latlon_continuity function
santisoler Apr 16, 2019
836fef2
Add tests for latlon_continuity
santisoler Apr 17, 2019
eea0d9b
Add latlon_continuity to doc and __init__.py
santisoler Apr 17, 2019
457f192
Improve docstring of latlon_continuity
santisoler Apr 17, 2019
11f691f
Add example for latlon_continuity
santisoler Apr 17, 2019
aa2ce52
Add example to inside function with latlon=True
santisoler Apr 17, 2019
123607e
Fix typo in docstring
santisoler Apr 17, 2019
ed9410f
Make latlon_continuity() a private function
santisoler Apr 22, 2019
bdbc6dc
Rewrite _latlon_continuity function
santisoler Apr 22, 2019
49d96cd
Remove latlon_continuity from docs index
santisoler Apr 22, 2019
0fc747f
Remove _latlon_continuity from __init__.py
santisoler Apr 22, 2019
5fb3c0e
Add case where region goes around the globe
santisoler Apr 22, 2019
88dab93
Add more test around the globe
santisoler Apr 22, 2019
d0b83e5
Add test around the poles
santisoler Apr 22, 2019
2327294
Add latlon_continuity test when w == e
santisoler Apr 22, 2019
7ad90e9
Improve check for around the globe region
santisoler Apr 23, 2019
4855882
Add test for two rounds to the globe
santisoler Apr 23, 2019
d3b86b2
Add latlon option to check_region()
santisoler Apr 24, 2019
7493f1d
Change the way geographic region is checked
santisoler Apr 24, 2019
7e527e9
Add function to check geographic coordinates
santisoler Apr 24, 2019
9b1021d
Replace all() for any()
santisoler Apr 24, 2019
0c3a067
Modify _latlon_continuity function
santisoler Apr 24, 2019
76fc0b9
Add docstring for latlon on check_region
santisoler Apr 24, 2019
b6dfa90
Add s > n error for both latlon True or False
santisoler Apr 24, 2019
98cf7fc
Extended tests for check_region when latlon=True
santisoler Apr 24, 2019
00fddb2
Update tests for the new code
santisoler Apr 24, 2019
7c5b9c3
Add test in case abs(e - w) > 360
santisoler Apr 24, 2019
5b90b51
Improve styling of tests
santisoler Apr 24, 2019
75077c9
Merge branch 'master' into geographic
santisoler Apr 24, 2019
25de366
Make latlon_continuity public
santisoler May 9, 2019
956b083
Refactor latlon_continuity to improve readability
santisoler May 9, 2019
f7ad34d
Add checks inside latlon_continuity
santisoler May 9, 2019
4f8c502
Replace .any() with np.any()
santisoler May 9, 2019
1c1a92d
Merge branch 'master' into geographic
santisoler May 9, 2019
157ce34
Merge branch 'master' into geographic
santisoler May 15, 2019
9dc288a
Rename latlon_continuity to longitude_continuity
santisoler May 15, 2019
3c45a6c
Rename latlon to geographic
santisoler May 15, 2019
9906366
Remove longitude_continuity from inside function
santisoler May 15, 2019
130c96e
Fix order of returns on docstring
santisoler May 15, 2019
614b9e2
Fix reference to longitude_continuity function
santisoler May 22, 2019
3ac3773
Improve comment on example
santisoler May 22, 2019
5235234
Improve longitude_continuity docstring
santisoler May 22, 2019
55e58b7
Merge branch 'master' into geographic
santisoler May 22, 2019
e2e4b7b
Merge branch 'master' into geographic
santisoler May 27, 2019
fdd1748
Double bracketing for intervals in docstring
santisoler Jun 21, 2019
7296f84
Merge branch 'master' into geographic
santisoler Jun 21, 2019
3e1f495
Remove trailing space on empty line
santisoler Jun 21, 2019
ca1f440
Add missing ` on docstring
santisoler Jun 21, 2019
d822d50
Add test function for invalid geographic region
santisoler Jun 21, 2019
9f3fd5f
Add test func for invalid geographic coordinates
santisoler Jun 21, 2019
a16b01c
Remove importing unused inside on test_coordinates
santisoler Jun 21, 2019
4108c6b
Merge branch 'master' into geographic
leouieda Jul 16, 2019
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
1 change: 1 addition & 0 deletions verde/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
get_region,
pad_region,
project_region,
longitude_continuity,
)
from .mask import distance_mask
from .utils import variance_to_weights, maxabs, grid_to_table
Expand Down
135 changes: 134 additions & 1 deletion verde/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ def check_region(region):
w, e, s, n = region
if w > e:
raise ValueError(
"Invalid region '{}' (W, E, S, N). Must have W =< E.".format(region)
"Invalid region '{}' (W, E, S, N). Must have W =< E. ".format(region)
+ "If working with geographic coordinates, don't forget to match geographic"
+ " region with coordinates using 'verde.longitude_continuity'."
)
if s > n:
raise ValueError(
Expand Down Expand Up @@ -635,6 +637,17 @@ def inside(coordinates, region):
[False True True]
[False False False]]

Geographic coordinates are also supported using :func:`verde.longitude_continuity`:

>>> from verde import longitude_continuity
>>> east, north = grid_coordinates([0, 350, -20, 20], spacing=10)
>>> region = [-10, 10, -10, 10]
>>> are_inside = inside(*longitude_continuity([east, north], region))
>>> print(east[are_inside])
[ 0. 10. 350. 0. 10. 350. 0. 10. 350.]
>>> print(north[are_inside])
[-10. -10. -10. 0. 0. 0. 10. 10. 10.]

"""
check_region(region)
w, e, s, n = region
Expand Down Expand Up @@ -751,3 +764,123 @@ def block_split(coordinates, spacing=None, adjust="spacing", region=None, shape=
tree = kdtree(block_coords)
labels = tree.query(np.transpose(n_1d_arrays(coordinates, 2)))[1]
return block_coords, labels


def longitude_continuity(coordinates, region):
"""
Modify coordinates and region boundaries to ensure longitude continuity.

Longitudinal boundaries of the region are moved to the `[0, 360)` or `[-180, 180)`
santisoler marked this conversation as resolved.
Show resolved Hide resolved
degrees interval depending which one is better suited for that specific region.

Parameters
----------
coordinates : list or array
Set of geographic coordinates that will be moved to the same degrees
interval as the one of the modified region.
region : list or array
List or array containing the boundary coordinates `w, `e`, `s`, `n` of the
santisoler marked this conversation as resolved.
Show resolved Hide resolved
region in degrees.

Returns
-------
modified_coordinates : array
Modified set of extra geographic coordinates.
modified_region : array
List containing the modified boundary coordinates `w, `e`, `s`, `n` of the
region.

Examples
--------

>>> # Modify region with west > east
>>> w, e, s, n = 350, 10, -10, 10
>>> print(longitude_continuity(coordinates=None, region=[w, e, s, n]))
[-10 10 -10 10]
>>> # Modify region and extra coordinates
>>> from verde import grid_coordinates
>>> region = [-70, -60, -40, -30]
>>> coordinates = grid_coordinates([270, 320, -50, -20], spacing=5)
>>> [longitude, latitude], region = longitude_continuity(coordinates, region)
>>> print(region)
[290 300 -40 -30]
>>> print(longitude.min(), longitude.max())
270.0 320.0
>>> # Another example
>>> region = [-20, 20, -20, 20]
>>> coordinates = grid_coordinates([0, 350, -90, 90], spacing=10)
>>> [longitude, latitude], region = longitude_continuity(coordinates, region)
>>> print(region)
[-20 20 -20 20]
>>> print(longitude.min(), longitude.max())
-180.0 170.0
"""
# Get longitudinal boundaries and check region
w, e, s, n = region[:4]
# Run sanity checks for region
_check_geographic_region([w, e, s, n])
# Check if region is defined all around the globe
all_globe = np.allclose(abs(e - w), 360)
# Move coordinates to [0, 360)
interval_360 = True
w = w % 360
e = e % 360
# Move west=0 and east=360 if region longitudes goes all around the globe
if all_globe:
w, e = 0, 360
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be problematic since the region might be very close but not exactly 360. Might lead to slightly off coordinates that could give errors down the line. Is there any way to avoid this?

Copy link
Member Author

@santisoler santisoler May 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this all_globe check in case you pass w, e = 0, 360 as region boundaries.
If we omit the all_globe, the algorithm will return w, e = 0, 0, which is problematic.
The slightly off coordinates you mention will be within an error of 1e-5 (according to numpy.allclose).
One simple hack would be to lower down this error, but I don't know if it's ideal.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking for this is "do we need to do this check?". If this is not necessary, then we could skip the check and avoid the issues with precision.

# Check if the [-180, 180) interval is better suited
if w > e:
interval_360 = False
e = ((e + 180) % 360) - 180
w = ((w + 180) % 360) - 180
region = np.array(region)
region[:2] = w, e
# Modify extra coordinates if passed
if coordinates:
# Run sanity checks for coordinates
_check_geographic_coordinates(coordinates)
longitude = coordinates[0]
if interval_360:
longitude = longitude % 360
else:
longitude = ((longitude + 180) % 360) - 180
coordinates = np.array(coordinates)
coordinates[0] = longitude
return coordinates, region
return region


def _check_geographic_coordinates(coordinates):
leouieda marked this conversation as resolved.
Show resolved Hide resolved
"Check if geographic coordinates are within accepted degrees intervals"
longitude, latitude = coordinates[:2]
if np.any(longitude > 360) or np.any(longitude < -180):
raise ValueError(
"Invalid longitude coordinates. They should be < 360 and > -180 degrees."
)
if np.any(latitude > 90) or np.any(latitude < -90):
raise ValueError(
"Invalid latitude coordinates. They should be < 90 and > -90 degrees."
)


def _check_geographic_region(region):
"Check if region in geographic coordinates are within accepted degree intervals"
w, e, s, n = region[:4]
# Check if coordinates are within accepted degrees intervals
if np.any(np.array([w, e]) > 360) or np.any(np.array([w, e]) < -180):
raise ValueError(
"Invalid region '{}' (W, E, S, N). ".format(region)
+ "Longitudinal coordinates should be < 360 and > -180 degrees."
)
if np.any(np.array([s, n]) > 90) or np.any(np.array([s, n]) < -90):
raise ValueError(
"Invalid region '{}' (W, E, S, N). ".format(region)
+ "Latitudinal coordinates should be < 90 and > -90 degrees."
)
# Check if longitude boundaries do not involve more than one spin around the globe
if abs(e - w) > 360:
raise ValueError(
"Invalid region '{}' (W, E, S, N). ".format(region)
+ "East and West boundaries must not be separated by an angle greater "
+ "than 360 degrees."
)
67 changes: 67 additions & 0 deletions verde/tests/test_coordinates.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Test the coordinate generation functions
"""
import numpy as np
import numpy.testing as npt
import pytest

Expand All @@ -9,6 +10,8 @@
spacing_to_shape,
profile_coordinates,
grid_coordinates,
inside,
longitude_continuity,
)


Expand Down Expand Up @@ -92,3 +95,67 @@ def test_profile_coordiantes_fails():
profile_coordinates((0, 1), (1, 2), size=0)
with pytest.raises(ValueError):
profile_coordinates((0, 1), (1, 2), size=-10)


def test_longitude_continuity():
"Test continuous boundary conditions in geographic coordinates."
# Define longitude coordinates around the globe for [0, 360) and [-180, 180)
longitude_360 = np.linspace(0, 350, 36)
longitude_180 = np.hstack((longitude_360[:18], longitude_360[18:] - 360))
latitude = np.linspace(-90, 90, 36)
s, n = -90, 90
# Check w, e in [0, 360)
w, e = 10.5, 20.3
for longitude in [longitude_360, longitude_180]:
coordinates = [longitude, latitude]
coordinates_new, region_new = longitude_continuity(coordinates, (w, e, s, n))
w_new, e_new = region_new[:2]
assert w_new == w
assert e_new == e
npt.assert_allclose(coordinates_new[0], longitude_360)
# Check w, e in [-180, 180)
w, e = -20, 20
for longitude in [longitude_360, longitude_180]:
coordinates = [longitude, latitude]
coordinates_new, region_new = longitude_continuity(coordinates, (w, e, s, n))
w_new, e_new = region_new[:2]
assert w_new == -20
assert e_new == 20
npt.assert_allclose(coordinates_new[0], longitude_180)
# Check region around the globe
for w, e in [[0, 360], [-180, 180], [-20, 340]]:
for longitude in [longitude_360, longitude_180]:
coordinates = [longitude, latitude]
coordinates_new, region_new = longitude_continuity(
coordinates, (w, e, s, n)
)
w_new, e_new = region_new[:2]
assert w_new == 0
assert e_new == 360
npt.assert_allclose(coordinates_new[0], longitude_360)
# Check w == e
w, e = 20, 20
for longitude in [longitude_360, longitude_180]:
coordinates = [longitude, latitude]
coordinates_new, region_new = longitude_continuity(coordinates, (w, e, s, n))
w_new, e_new = region_new[:2]
assert w_new == 20
assert e_new == 20
npt.assert_allclose(coordinates_new[0], longitude_360)
# Check angle greater than 180
w, e = 0, 200
for longitude in [longitude_360, longitude_180]:
coordinates = [longitude, latitude]
coordinates_new, region_new = longitude_continuity(coordinates, (w, e, s, n))
w_new, e_new = region_new[:2]
assert w_new == 0
assert e_new == 200
npt.assert_allclose(coordinates_new[0], longitude_360)
w, e = -160, 160
for longitude in [longitude_360, longitude_180]:
coordinates = [longitude, latitude]
coordinates_new, region_new = longitude_continuity(coordinates, (w, e, s, n))
w_new, e_new = region_new[:2]
assert w_new == -160
assert e_new == 160
npt.assert_allclose(coordinates_new[0], longitude_180)