Skip to content

Commit 1c5df8c

Browse files
committed
Wrap GMT's standard data type GMT_CUBE for cubes
1 parent 65cc190 commit 1c5df8c

File tree

3 files changed

+101
-4
lines changed

3 files changed

+101
-4
lines changed

pygmt/clib/session.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
vectors_to_arrays,
2525
)
2626
from pygmt.clib.loading import load_libgmt
27-
from pygmt.datatypes import _GMT_DATASET, _GMT_GRID
27+
from pygmt.datatypes import _GMT_CUBE, _GMT_DATASET, _GMT_GRID
2828
from pygmt.exceptions import (
2929
GMTCLibError,
3030
GMTCLibNoSessionError,
@@ -1648,7 +1648,9 @@ def virtualfile_in( # noqa: PLR0912
16481648

16491649
@contextlib.contextmanager
16501650
def virtualfile_out(
1651-
self, kind: Literal["dataset", "grid"] = "dataset", fname: str | None = None
1651+
self,
1652+
kind: Literal["dataset", "grid", "cube"] = "dataset",
1653+
fname: str | None = None,
16521654
):
16531655
r"""
16541656
Create a virtual file or an actual file for storing output data.
@@ -1705,12 +1707,13 @@ def virtualfile_out(
17051707
family, geometry = {
17061708
"dataset": ("GMT_IS_DATASET", "GMT_IS_PLP"),
17071709
"grid": ("GMT_IS_GRID", "GMT_IS_SURFACE"),
1710+
"cube": ("GMT_IS_CUBE", "GMT_IS_VOLUME"),
17081711
}[kind]
17091712
with self.open_virtualfile(family, geometry, "GMT_OUT", None) as vfile:
17101713
yield vfile
17111714

17121715
def read_virtualfile(
1713-
self, vfname: str, kind: Literal["dataset", "grid", None] = None
1716+
self, vfname: str, kind: Literal["dataset", "grid", "cube", None] = None
17141717
):
17151718
"""
17161719
Read data from a virtual file and optionally cast into a GMT data container.
@@ -1769,7 +1772,7 @@ def read_virtualfile(
17691772
# _GMT_DATASET).
17701773
if kind is None: # Return the ctypes void pointer
17711774
return pointer
1772-
dtype = {"dataset": _GMT_DATASET, "grid": _GMT_GRID}[kind]
1775+
dtype = {"dataset": _GMT_DATASET, "grid": _GMT_GRID, "cube": _GMT_CUBE}[kind]
17731776
return ctp.cast(pointer, ctp.POINTER(dtype))
17741777

17751778
def virtualfile_to_dataset(

pygmt/datatypes/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
Wrappers for GMT data types.
33
"""
44

5+
from pygmt.datatypes.cube import _GMT_CUBE
56
from pygmt.datatypes.dataset import _GMT_DATASET
67
from pygmt.datatypes.grid import _GMT_GRID

pygmt/datatypes/cube.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Wrapper for the GMT_CUBE data type.
3+
"""
4+
5+
import ctypes as ctp
6+
from typing import ClassVar
7+
8+
import numpy as np
9+
import xarray as xr
10+
from pygmt.datatypes.header import (
11+
_GMT_GRID_HEADER,
12+
GMT_GRID_UNIT_LEN80,
13+
GMT_GRID_VARNAME_LEN80,
14+
_parse_nameunits,
15+
gmt_grdfloat,
16+
)
17+
18+
19+
class _GMT_CUBE(ctp.Structure): # noqa: N801
20+
"""
21+
GMT cube data structure for 3D data.
22+
"""
23+
24+
_fields_: ClassVar = [
25+
# Pointer to full GMT 2-D header for a layer (common to all layers)
26+
("header", ctp.POINTER(_GMT_GRID_HEADER)),
27+
# Pointer to the gmt_grdfloat 3-D cube - a stack of 2-D padded grids
28+
("data", ctp.POINTER(gmt_grdfloat)),
29+
# Vector of x coordinates common to all layers
30+
("x", ctp.POINTER(ctp.c_double)),
31+
# Vector of y coordinates common to all layers
32+
("y", ctp.POINTER(ctp.c_double)),
33+
# Low-level information for GMT use only
34+
("hidden", ctp.c_void_p),
35+
# GMT_CUBE_IS_STACK if input dataset was a list of 2-D grids rather than a
36+
# single cube
37+
("mode", ctp.c_uint),
38+
# Minimum/max z values (complements header->wesn[4])
39+
("z_range", ctp.c_double * 2),
40+
# z increment (complements inc[2]) (0 if variable z spacing)
41+
("z_inc", ctp.c_double),
42+
# Array of z values (complements x, y)
43+
("z", ctp.POINTER(ctp.c_double)),
44+
# Name of the 3-D variable, if read from file (or empty if just one)
45+
("name", ctp.c_char * GMT_GRID_VARNAME_LEN80),
46+
# Units in 3rd direction (complements x_units, y_units, z_units)
47+
("units", ctp.c_char * GMT_GRID_UNIT_LEN80),
48+
]
49+
50+
def to_dataarray(self):
51+
"""
52+
Convert the GMT_CUBE to an xarray.DataArray.
53+
54+
Returns
55+
-------
56+
xarray.DataArray: The data array representation of the GMT_CUBE.
57+
"""
58+
# The grid header
59+
header = self.header.contents
60+
61+
name = "cube"
62+
# Dimensions and attributes
63+
dims = header.dims
64+
dim_attrs = header.dim_attrs
65+
66+
# Patch for the 3rd dimension
67+
dims.append("z")
68+
z_attrs = {"actual_range": np.array(self.z_range[:]), "axis": "Z"}
69+
long_name, units = _parse_nameunits(self.units.decode())
70+
if long_name:
71+
z_attrs["long_name"] = long_name
72+
if units:
73+
z_attrs["units"] = units
74+
dim_attrs.append(z_attrs)
75+
76+
# The coordinates, given as a tuple of the form (dims, data, attrs)
77+
coords = [
78+
(dims[0], self.y[: header.n_rows], dim_attrs[0]),
79+
(dims[1], self.x[: header.n_columns], dim_attrs[1]),
80+
# header->n_bands is used for the number of layers for 3-D cubes
81+
(dims[2], self.z[: header.n_bands], dim_attrs[1]),
82+
]
83+
84+
# The data array without paddings
85+
pad = header.pad[:]
86+
data = np.reshape(
87+
self.data[: header.mx * header.my * header.n_bands],
88+
(header.my, header.mx, header.n_bands),
89+
)[pad[2] : header.my - pad[3], pad[0] : header.mx - pad[1], :]
90+
91+
# Create the xarray.DataArray object
92+
grid = xr.DataArray(data, coords=coords, name=name, attrs=header.dataA_attrs)
93+
return grid

0 commit comments

Comments
 (0)