Skip to content

Commit 8ad3c1c

Browse files
Merge branch 'main' into add-params-sphdistance
2 parents e1b12c8 + 6f930ac commit 8ad3c1c

File tree

6 files changed

+223
-15
lines changed

6 files changed

+223
-15
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Operations on grids:
104104
grdproject
105105
grdsample
106106
grdtrack
107+
grdvolume
107108

108109
Crossover analysis with x2sys:
109110

pygmt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
grdproject,
4747
grdsample,
4848
grdtrack,
49+
grdvolume,
4950
info,
5051
makecpt,
5152
nearneighbor,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from pygmt.src.grdsample import grdsample
2525
from pygmt.src.grdtrack import grdtrack
2626
from pygmt.src.grdview import grdview
27+
from pygmt.src.grdvolume import grdvolume
2728
from pygmt.src.histogram import histogram
2829
from pygmt.src.image import image
2930
from pygmt.src.info import info

pygmt/src/grdvolume.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
grdvolume - Calculate grid volume and area constrained by a contour.
3+
"""
4+
import pandas as pd
5+
from pygmt.clib import Session
6+
from pygmt.exceptions import GMTInvalidInput
7+
from pygmt.helpers import (
8+
GMTTempFile,
9+
build_arg_string,
10+
fmt_docstring,
11+
kwargs_to_strings,
12+
use_alias,
13+
)
14+
15+
16+
@fmt_docstring
17+
@use_alias(
18+
C="contour",
19+
R="region",
20+
S="unit",
21+
V="verbose",
22+
)
23+
@kwargs_to_strings(C="sequence", R="sequence")
24+
def grdvolume(grid, output_type="pandas", outfile=None, **kwargs):
25+
r"""
26+
Determine the volume between the surface of a grid and a plane.
27+
28+
Read a 2-D grid file and calculate the volume contained below the surface
29+
and above the plane specified by the given contour (or zero if not given)
30+
and return the contour, area, volume, and maximum mean height
31+
(volume/area). Alternatively, a range of contours can be specified to
32+
return the volume and area inside the contour for all contour values.
33+
34+
Full option list at :gmt-docs:`grdvolume.html`
35+
36+
{aliases}
37+
38+
Parameters
39+
----------
40+
grid : str or xarray.DataArray
41+
The file name of the input grid or the grid loaded as a DataArray.
42+
output_type : str
43+
Determine the format the output data will be returned in [Default is
44+
``pandas``]:
45+
46+
- ``numpy`` - :class:`numpy.ndarray`
47+
- ``pandas``- :class:`pandas.DataFrame`
48+
- ``file`` - ASCII file (requires ``outfile``)
49+
outfile : str
50+
The file name for the output ASCII file.
51+
contour : str or int or float or list
52+
*cval*\|\ *low/high/delta*\|\ **r**\ *low/high*\|\ **r**\ *cval*.
53+
Find area, volume and mean height (volume/area) inside and above the
54+
*cval* contour. Alternatively, search using all contours from *low* to
55+
*high* in steps of *delta*. [Default returns area, volume and mean
56+
height of the entire grid]. The area is measured in the plane of the
57+
contour. Adding the **r** prefix computes the volume below the grid
58+
surface and above the planes defined by *low* and *high*, or below
59+
*cval* and grid's minimum. Note that this is an *outside* volume
60+
whilst the other forms compute an *inside* (below the surface) area
61+
volume. Use this form to compute for example the volume of water
62+
between two contours. If no *contour* is given then there is no contour
63+
and the entire grid area, volume and the mean height is returned and
64+
*cval* will be reported as 0.
65+
{R}
66+
{V}
67+
68+
Returns
69+
-------
70+
ret : pandas.DataFrame or numpy.ndarray or None
71+
Return type depends on ``outfile`` and ``output_type``:
72+
73+
- None if ``outfile`` is set (output will be stored in file set by
74+
``outfile``)
75+
- :class:`pandas.DataFrame` or :class:`numpy.ndarray` if ``outfile``
76+
is not set (depends on ``output_type`` [Default is
77+
class:`pandas.DataFrame`])
78+
"""
79+
if output_type not in ["numpy", "pandas", "file"]:
80+
raise GMTInvalidInput(
81+
"""Must specify format as either numpy, pandas, or file."""
82+
)
83+
if output_type == "file" and outfile is None:
84+
raise GMTInvalidInput("""Must specify outfile for ASCII output.""")
85+
86+
with GMTTempFile() as tmpfile:
87+
with Session() as lib:
88+
file_context = lib.virtualfile_from_data(check_kind="raster", data=grid)
89+
with file_context as infile:
90+
if outfile is None:
91+
outfile = tmpfile.name
92+
arg_str = " ".join([infile, build_arg_string(kwargs), "->" + outfile])
93+
lib.call_module("grdvolume", arg_str)
94+
95+
# Read temporary csv output to a pandas table
96+
if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame
97+
result = pd.read_csv(tmpfile.name, sep="\t", header=None, comment=">")
98+
elif outfile != tmpfile.name: # return None if outfile set, output in outfile
99+
result = None
100+
101+
if output_type == "numpy":
102+
result = result.to_numpy()
103+
return result

pygmt/tests/test_grdvolume.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""
2+
Tests for grdvolume.
3+
"""
4+
import os
5+
6+
import numpy as np
7+
import numpy.testing as npt
8+
import pandas as pd
9+
import pytest
10+
from pygmt import grdvolume
11+
from pygmt.datasets import load_earth_relief
12+
from pygmt.exceptions import GMTInvalidInput
13+
from pygmt.helpers import GMTTempFile
14+
15+
16+
@pytest.fixture(scope="module", name="grid")
17+
def fixture_grid():
18+
"""
19+
Load the grid data from the sample earth_relief file.
20+
"""
21+
return load_earth_relief(resolution="01d", region=[-100, -95, 34, 39])
22+
23+
24+
@pytest.fixture(scope="module", name="data")
25+
def fixture_data():
26+
"""
27+
Load the expected grdvolume data result as a numpy array.
28+
"""
29+
data = np.array(
30+
[
31+
[
32+
2.00000000e02,
33+
1.59920815e11,
34+
3.16386172e13,
35+
1.97839269e02,
36+
],
37+
[
38+
2.50000000e02,
39+
1.44365835e11,
40+
2.38676788e13,
41+
1.65327751e02,
42+
],
43+
[
44+
3.00000000e02,
45+
1.23788259e11,
46+
1.71278707e13,
47+
1.38364259e02,
48+
],
49+
[
50+
3.50000000e02,
51+
9.79597525e10,
52+
1.15235913e13,
53+
1.17635978e02,
54+
],
55+
[
56+
4.00000000e02,
57+
7.26646663e10,
58+
7.22303463e12,
59+
9.94022955e01,
60+
],
61+
]
62+
)
63+
return data
64+
65+
66+
def test_grdvolume_format(grid):
67+
"""
68+
Test that correct formats are returned.
69+
"""
70+
grdvolume_default = grdvolume(grid=grid)
71+
assert isinstance(grdvolume_default, pd.DataFrame)
72+
grdvolume_array = grdvolume(grid=grid, output_type="numpy")
73+
assert isinstance(grdvolume_array, np.ndarray)
74+
grdvolume_df = grdvolume(grid=grid, output_type="pandas")
75+
assert isinstance(grdvolume_df, pd.DataFrame)
76+
77+
78+
def test_grdvolume_invalid_format(grid):
79+
"""
80+
Test that grdvolume fails with incorrect output_type argument.
81+
"""
82+
with pytest.raises(GMTInvalidInput):
83+
grdvolume(grid=grid, output_type=1)
84+
85+
86+
def test_grdvolume_no_outfile(grid):
87+
"""
88+
Test that grdvolume fails when output_type set to 'file' but no outfile is
89+
specified.
90+
"""
91+
with pytest.raises(GMTInvalidInput):
92+
grdvolume(grid=grid, output_type="file")
93+
94+
95+
def test_grdvolume_no_outgrid(grid, data):
96+
"""
97+
Test the expected output of grdvolume with no output file set.
98+
"""
99+
test_output = grdvolume(grid=grid, contour=[200, 400, 50], output_type="numpy")
100+
npt.assert_allclose(test_output, data)
101+
102+
103+
def test_grdvolume_outgrid(grid):
104+
"""
105+
Test the expected output of grdvolume with an output file set.
106+
"""
107+
with GMTTempFile(suffix=".csv") as tmpfile:
108+
result = grdvolume(
109+
grid=grid, contour=[200, 400, 50], output_type="file", outfile=tmpfile.name
110+
)
111+
assert result is None # return value is None
112+
assert os.path.exists(path=tmpfile.name) # check that outfile exists

pygmt/tests/test_x2sys_cross.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,13 @@ def test_x2sys_cross_input_file_output_file(mock_x2sys_home):
4444
x2sys_init(tag=tag, fmtfile="xyz", force=True)
4545
outfile = os.path.join(tmpdir, "tmp_coe.txt")
4646
output = x2sys_cross(
47-
tracks=["@tut_ship.xyz"], tag=tag, coe="i", outfile=outfile, verbose="i"
47+
tracks=["@tut_ship.xyz"], tag=tag, coe="i", outfile=outfile
4848
)
4949

5050
assert output is None # check that output is None since outfile is set
5151
assert os.path.exists(path=outfile) # check that outfile exists at path
5252
_ = pd.read_csv(outfile, sep="\t", header=2) # ensure ASCII text file loads ok
5353

54-
return output
55-
5654

5755
def test_x2sys_cross_input_file_output_dataframe(mock_x2sys_home):
5856
"""
@@ -62,16 +60,14 @@ def test_x2sys_cross_input_file_output_dataframe(mock_x2sys_home):
6260
with TemporaryDirectory(prefix="X2SYS", dir=os.getcwd()) as tmpdir:
6361
tag = os.path.basename(tmpdir)
6462
x2sys_init(tag=tag, fmtfile="xyz", force=True)
65-
output = x2sys_cross(tracks=["@tut_ship.xyz"], tag=tag, coe="i", verbose="i")
63+
output = x2sys_cross(tracks=["@tut_ship.xyz"], tag=tag, coe="i")
6664

6765
assert isinstance(output, pd.DataFrame)
6866
assert output.shape == (14294, 12)
6967
columns = list(output.columns)
7068
assert columns[:6] == ["x", "y", "i_1", "i_2", "dist_1", "dist_2"]
7169
assert columns[6:] == ["head_1", "head_2", "vel_1", "vel_2", "z_X", "z_M"]
7270

73-
return output
74-
7571

7672
def test_x2sys_cross_input_dataframe_output_dataframe(mock_x2sys_home, tracks):
7773
"""
@@ -82,7 +78,7 @@ def test_x2sys_cross_input_dataframe_output_dataframe(mock_x2sys_home, tracks):
8278
tag = os.path.basename(tmpdir)
8379
x2sys_init(tag=tag, fmtfile="xyz", force=True)
8480

85-
output = x2sys_cross(tracks=tracks, tag=tag, coe="i", verbose="i")
81+
output = x2sys_cross(tracks=tracks, tag=tag, coe="i")
8682

8783
assert isinstance(output, pd.DataFrame)
8884
assert output.shape == (14, 12)
@@ -92,8 +88,6 @@ def test_x2sys_cross_input_dataframe_output_dataframe(mock_x2sys_home, tracks):
9288
assert output.dtypes["i_1"].type == np.object_
9389
assert output.dtypes["i_2"].type == np.object_
9490

95-
return output
96-
9791

9892
def test_x2sys_cross_input_two_dataframes(mock_x2sys_home):
9993
"""
@@ -120,7 +114,7 @@ def test_x2sys_cross_input_two_dataframes(mock_x2sys_home):
120114
track["time"] = pd.date_range(start=f"2020-{i}1-01", periods=10, freq="ms")
121115
tracks.append(track)
122116

123-
output = x2sys_cross(tracks=tracks, tag=tag, coe="e", verbose="i")
117+
output = x2sys_cross(tracks=tracks, tag=tag, coe="e")
124118

125119
assert isinstance(output, pd.DataFrame)
126120
assert output.shape == (30, 12)
@@ -171,9 +165,7 @@ def test_x2sys_cross_input_two_filenames(mock_x2sys_home):
171165
) as fname:
172166
np.savetxt(fname=fname, X=np.random.rand(10, 3))
173167

174-
output = x2sys_cross(
175-
tracks=["track_0.xyz", "track_1.xyz"], tag=tag, coe="e", verbose="i"
176-
)
168+
output = x2sys_cross(tracks=["track_0.xyz", "track_1.xyz"], tag=tag, coe="e")
177169

178170
assert isinstance(output, pd.DataFrame)
179171
assert output.shape == (24, 12)
@@ -182,8 +174,6 @@ def test_x2sys_cross_input_two_filenames(mock_x2sys_home):
182174
assert columns[6:] == ["head_1", "head_2", "vel_1", "vel_2", "z_X", "z_M"]
183175
_ = [os.remove(f"track_{i}.xyz") for i in range(2)] # cleanup track files
184176

185-
return output
186-
187177

188178
def test_x2sys_cross_invalid_tracks_input_type(tracks):
189179
"""

0 commit comments

Comments
 (0)