Skip to content

Commit 16f5a10

Browse files
authored
Wrap grdtrack (#308)
Implement GMT grdtrack function under sampling.py. Original GMT `grdtrack` documentation can be found at https://docs.generic-mapping-tools.org/latest/grdtrack.html. Implementation here takes in a pandas.DataFrame table and xarray.DataArray grid as input, and returns a pandas.DataFrame with an extra column for the sampled grid data. Also possible to use text file or netcdf inputs. Test cases checking proper pandas.DataFrame and xarray.DataArray inputs stored in test_grdtrack.py. Sample datasets for tests uses newly created load_ocean_ridge_points functions in datasets/tutorial.py. Added gallery example 'sampling along tracks'.
1 parent dc178ed commit 16f5a10

File tree

10 files changed

+327
-4
lines changed

10 files changed

+327
-4
lines changed

doc/api/index.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Operations on grids:
6969
:toctree: generated
7070

7171
grdinfo
72+
grdtrack
7273

7374

7475
Miscellaneous
@@ -97,10 +98,10 @@ and store them in the GMT cache folder.
9798
:toctree: generated
9899

99100
datasets.load_earth_relief
100-
datasets.load_usgs_quakes
101-
datasets.load_sample_bathymetry
102101
datasets.load_japan_quakes
103-
102+
datasets.load_ocean_ridge_points
103+
datasets.load_sample_bathymetry
104+
datasets.load_usgs_quakes
104105

105106
.. automodule:: pygmt.exceptions
106107

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
[
6363
"../examples/gallery/coast",
6464
"../examples/gallery/plot",
65+
"../examples/gallery/grid",
6566
"../examples/projections/azim",
6667
"../examples/projections/conic",
6768
"../examples/projections/cyl",

examples/gallery/grid/README.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Grids
2+
-----
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
Sampling along tracks
3+
---------------------
4+
5+
The :func:`pygmt.grdtrack` function samples a raster grid's value along specified
6+
points. We will need to input a 2D raster to ``grid`` which can be an
7+
``xarray.DataArray``. The ``points`` parameter can be a ``pandas.DataFrame`` table where
8+
the first two columns are x and y (or longitude and latitude). Note also that there is a
9+
``newcolname`` parameter that will be used to name the new column of values we sampled
10+
from the grid.
11+
12+
Alternatively, we can provide a NetCDF file path to ``grid``. An ASCII file path can
13+
also be accepted for ``points``, but an ``outfile`` parameter will then need to be set
14+
to name the resulting output ASCII file.
15+
"""
16+
17+
import pygmt
18+
19+
# Load sample grid and point datasets
20+
grid = pygmt.datasets.load_earth_relief()
21+
points = pygmt.datasets.load_ocean_ridge_points()
22+
# Sample the bathymetry along the world's ocean ridges at specified track points
23+
track = pygmt.grdtrack(points=points, grid=grid, newcolname="bathymetry")
24+
25+
fig = pygmt.Figure()
26+
# Plot the earth relief grid on Cylindrical Stereographic projection, masking land areas
27+
fig.basemap(region="g", frame=True, projection="Cyl_stere/150/-20/8i")
28+
fig.grdimage(grid=grid, cmap="gray")
29+
fig.coast(land="#666666")
30+
# Plot using circles (c) of 0.15cm, the sampled bathymetry points
31+
# Points are colored using elevation values (normalized for visual purposes)
32+
fig.plot(
33+
x=track.longitude,
34+
y=track.latitude,
35+
style="c0.15c",
36+
cmap="terra",
37+
color=(track.bathymetry - track.bathymetry.mean()) / track.bathymetry.std(),
38+
)
39+
fig.show()

pygmt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .session_management import begin as _begin, end as _end
1616
from .figure import Figure
1717
from .gridding import surface
18+
from .sampling import grdtrack
1819
from .mathops import makecpt
1920
from .modules import info, grdinfo, which
2021
from . import datasets

pygmt/datasets/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
#
33
# Load sample data included with GMT (downloaded from the GMT cache server).
44

5-
from .tutorial import load_japan_quakes, load_sample_bathymetry, load_usgs_quakes
5+
from .tutorial import (
6+
load_japan_quakes,
7+
load_ocean_ridge_points,
8+
load_sample_bathymetry,
9+
load_usgs_quakes,
10+
)
611
from .earth_relief import load_earth_relief

pygmt/datasets/tutorial.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ def load_japan_quakes():
3838
return data
3939

4040

41+
def load_ocean_ridge_points():
42+
"""
43+
Load a table of ocean ridge points for the entire world as a
44+
pandas.DataFrame.
45+
46+
This is the ``@ridge.txt`` dataset used in the GMT tutorials.
47+
48+
The data are downloaded to a cache directory (usually ``~/.gmt/cache``) the
49+
first time you invoke this function. Afterwards, it will load the data from
50+
the cache. So you'll need an internet connection the first time around.
51+
52+
Returns
53+
-------
54+
data : pandas.Dataframe
55+
The data table. Columns are longitude and latitude.
56+
"""
57+
fname = which("@ridge.txt", download="c")
58+
data = pd.read_csv(
59+
fname, sep=r"\s+", names=["longitude", "latitude"], skiprows=1, comment=">"
60+
)
61+
return data
62+
63+
4164
def load_sample_bathymetry():
4265
"""
4366
Load a table of ship observations of bathymetry off Baja California as a

pygmt/sampling.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
GMT modules for Sampling of 1-D and 2-D Data
3+
"""
4+
import pandas as pd
5+
6+
from .clib import Session
7+
from .helpers import (
8+
build_arg_string,
9+
fmt_docstring,
10+
GMTTempFile,
11+
data_kind,
12+
dummy_context,
13+
)
14+
from .exceptions import GMTInvalidInput
15+
16+
17+
@fmt_docstring
18+
def grdtrack(points, grid, newcolname=None, outfile=None, **kwargs):
19+
"""
20+
Sample grids at specified (x,y) locations.
21+
22+
Grdtrack reads one or more grid files and a table with (x,y) [or (lon,lat)]
23+
positions in the first two columns (more columns may be present). It
24+
interpolates the grid(s) at the positions in the table and writes out the
25+
table with the interpolated values added as (one or more) new columns. A
26+
bicubic [Default], bilinear, B-spline or nearest-neighbor (see -n)
27+
interpolation is used, requiring boundary conditions at the limits of the
28+
region.
29+
30+
Full option list at :gmt-docs:`grdtrack.html`
31+
32+
Parameters
33+
----------
34+
points: pandas.DataFrame or file (csv, txt, etc)
35+
Either a table with (x, y) or (lon, lat) values in the first two
36+
columns, or a data file name. More columns may be present.
37+
38+
grid: xarray.DataArray or file (netcdf)
39+
Gridded array from which to sample values from.
40+
41+
newcolname: str
42+
Required if 'points' is a pandas.DataFrame. The name for the new column
43+
in the track pandas.DataFrame table where the sampled values will be
44+
placed.
45+
46+
outfile: str
47+
Required if 'points' is a file. The file name for the output ASCII
48+
file.
49+
50+
Returns
51+
-------
52+
track: pandas.DataFrame or None
53+
Return type depends on whether the outfile parameter is set:
54+
- pandas.DataFrame table with (x, y, ..., newcolname) if outfile is not
55+
set
56+
- None if outfile is set (track output will be stored in outfile)
57+
58+
"""
59+
60+
with GMTTempFile(suffix=".csv") as tmpfile:
61+
with Session() as lib:
62+
# Store the pandas.DataFrame points table in virtualfile
63+
if data_kind(points) == "matrix":
64+
if newcolname is None:
65+
raise GMTInvalidInput("Please pass in a str to 'newcolname'")
66+
table_context = lib.virtualfile_from_matrix(points.values)
67+
elif data_kind(points) == "file":
68+
if outfile is None:
69+
raise GMTInvalidInput("Please pass in a str to 'outfile'")
70+
table_context = dummy_context(points)
71+
else:
72+
raise GMTInvalidInput(f"Unrecognized data type {type(points)}")
73+
74+
# Store the xarray.DataArray grid in virtualfile
75+
if data_kind(grid) == "grid":
76+
grid_context = lib.virtualfile_from_grid(grid)
77+
elif data_kind(grid) == "file":
78+
grid_context = dummy_context(grid)
79+
else:
80+
raise GMTInvalidInput(f"Unrecognized data type {type(grid)}")
81+
82+
# Run grdtrack on the temporary (csv) points table
83+
# and (netcdf) grid virtualfile
84+
with table_context as csvfile:
85+
with grid_context as grdfile:
86+
kwargs.update({"G": grdfile})
87+
if outfile is None: # Output to tmpfile if outfile is not set
88+
outfile = tmpfile.name
89+
arg_str = " ".join(
90+
[csvfile, build_arg_string(kwargs), "->" + outfile]
91+
)
92+
lib.call_module(module="grdtrack", args=arg_str)
93+
94+
# Read temporary csv output to a pandas table
95+
if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame
96+
column_names = points.columns.to_list() + [newcolname]
97+
result = pd.read_csv(tmpfile.name, sep="\t", names=column_names)
98+
elif outfile != tmpfile.name: # return None if outfile set, output in outfile
99+
result = None
100+
101+
return result

pygmt/tests/test_datasets.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ..datasets import (
99
load_japan_quakes,
1010
load_earth_relief,
11+
load_ocean_ridge_points,
1112
load_sample_bathymetry,
1213
load_usgs_quakes,
1314
)
@@ -27,6 +28,17 @@ def test_japan_quakes():
2728
assert summary.loc["max", "day"] == 31
2829

2930

31+
def test_ocean_ridge_points():
32+
"Check that the @ridge.txt dataset loads without errors"
33+
data = load_ocean_ridge_points()
34+
assert data.shape == (4146, 2)
35+
summary = data.describe()
36+
assert summary.loc["min", "longitude"] == -179.9401
37+
assert summary.loc["max", "longitude"] == 179.935
38+
assert summary.loc["min", "latitude"] == -65.6182
39+
assert summary.loc["max", "latitude"] == 86.8
40+
41+
3042
def test_sample_bathymetry():
3143
"Check that the @tut_ship.xyz dataset loads without errors"
3244
data = load_sample_bathymetry()

pygmt/tests/test_grdtrack.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""
2+
Tests for grdtrack
3+
"""
4+
import os
5+
6+
import numpy.testing as npt
7+
import pandas as pd
8+
import pytest
9+
10+
from .. import grdtrack
11+
from .. import which
12+
from ..datasets import load_earth_relief, load_ocean_ridge_points
13+
from ..exceptions import GMTInvalidInput
14+
from ..helpers import data_kind
15+
16+
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
17+
TEMP_TRACK = os.path.join(TEST_DATA_DIR, "tmp_track.txt")
18+
19+
20+
def test_grdtrack_input_dataframe_and_dataarray():
21+
"""
22+
Run grdtrack by passing in a pandas.DataFrame and xarray.DataArray as
23+
inputs
24+
"""
25+
dataframe = load_ocean_ridge_points()
26+
dataarray = load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107))
27+
28+
output = grdtrack(points=dataframe, grid=dataarray, newcolname="bathymetry")
29+
assert isinstance(output, pd.DataFrame)
30+
assert output.columns.to_list() == ["longitude", "latitude", "bathymetry"]
31+
npt.assert_allclose(output.iloc[0], [-110.9536, -42.2489, -2797.482959])
32+
33+
return output
34+
35+
36+
def test_grdtrack_input_csvfile_and_dataarray():
37+
"""
38+
Run grdtrack by passing in a csvfile and xarray.DataArray as inputs
39+
"""
40+
csvfile = which("@ridge.txt", download="c")
41+
dataarray = load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107))
42+
43+
try:
44+
output = grdtrack(points=csvfile, grid=dataarray, outfile=TEMP_TRACK)
45+
assert output is None # check that output is None since outfile is set
46+
assert os.path.exists(path=TEMP_TRACK) # check that outfile exists at path
47+
48+
track = pd.read_csv(TEMP_TRACK, sep="\t", header=None, comment=">")
49+
npt.assert_allclose(track.iloc[0], [-110.9536, -42.2489, -2797.482959])
50+
finally:
51+
os.remove(path=TEMP_TRACK)
52+
53+
return output
54+
55+
56+
def test_grdtrack_input_dataframe_and_ncfile():
57+
"""
58+
Run grdtrack by passing in a pandas.DataFrame and netcdf file as inputs
59+
"""
60+
dataframe = load_ocean_ridge_points()
61+
ncfile = which("@earth_relief_60m", download="c")
62+
63+
output = grdtrack(points=dataframe, grid=ncfile, newcolname="bathymetry")
64+
assert isinstance(output, pd.DataFrame)
65+
assert output.columns.to_list() == ["longitude", "latitude", "bathymetry"]
66+
npt.assert_allclose(output.iloc[0], [-32.2971, 37.4118, -1686.692142])
67+
68+
return output
69+
70+
71+
def test_grdtrack_input_csvfile_and_ncfile():
72+
"""
73+
Run grdtrack by passing in a csvfile and netcdf file as inputs
74+
"""
75+
csvfile = which("@ridge.txt", download="c")
76+
ncfile = which("@earth_relief_60m", download="c")
77+
78+
try:
79+
output = grdtrack(points=csvfile, grid=ncfile, outfile=TEMP_TRACK)
80+
assert output is None # check that output is None since outfile is set
81+
assert os.path.exists(path=TEMP_TRACK) # check that outfile exists at path
82+
83+
track = pd.read_csv(TEMP_TRACK, sep="\t", header=None, comment=">")
84+
npt.assert_allclose(track.iloc[0], [-32.2971, 37.4118, -1686.692142])
85+
finally:
86+
os.remove(path=TEMP_TRACK)
87+
88+
return output
89+
90+
91+
def test_grdtrack_wrong_kind_of_points_input():
92+
"""
93+
Run grdtrack using points input that is not a pandas.DataFrame (matrix) or
94+
file
95+
"""
96+
dataframe = load_ocean_ridge_points()
97+
invalid_points = dataframe.longitude.to_xarray()
98+
dataarray = load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107))
99+
100+
assert data_kind(invalid_points) == "grid"
101+
with pytest.raises(GMTInvalidInput):
102+
grdtrack(points=invalid_points, grid=dataarray, newcolname="bathymetry")
103+
104+
105+
def test_grdtrack_wrong_kind_of_grid_input():
106+
"""
107+
Run grdtrack using grid input that is not as xarray.DataArray (grid) or
108+
file
109+
"""
110+
dataframe = load_ocean_ridge_points()
111+
dataarray = load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107))
112+
invalid_grid = dataarray.to_dataset()
113+
114+
assert data_kind(invalid_grid) == "matrix"
115+
with pytest.raises(GMTInvalidInput):
116+
grdtrack(points=dataframe, grid=invalid_grid, newcolname="bathymetry")
117+
118+
119+
def test_grdtrack_without_newcolname_setting():
120+
"""
121+
Run grdtrack by not passing in newcolname parameter setting
122+
"""
123+
dataframe = load_ocean_ridge_points()
124+
dataarray = load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107))
125+
126+
with pytest.raises(GMTInvalidInput):
127+
grdtrack(points=dataframe, grid=dataarray)
128+
129+
130+
def test_grdtrack_without_outfile_setting():
131+
"""
132+
Run grdtrack by not passing in outfile parameter setting
133+
"""
134+
csvfile = which("@ridge.txt", download="c")
135+
dataarray = load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107))
136+
137+
with pytest.raises(GMTInvalidInput):
138+
grdtrack(points=csvfile, grid=dataarray)

0 commit comments

Comments
 (0)