Skip to content

Commit

Permalink
Merge branch 'main' into pass-arg-list-to-module
Browse files Browse the repository at this point in the history
  • Loading branch information
seisman committed Mar 26, 2024
2 parents 13334d0 + 62eb5d6 commit fe41846
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 45 deletions.
2 changes: 1 addition & 1 deletion examples/gallery/embellishments/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
The colormap is set via the ``cmap`` parameter. A full list of available
color palette tables can be found at :gmt-docs:`reference/cpts.html`.
Use the ``frame`` parameter to add labels to the **x** and **y** axes
of the colorbar by appending **+l** followed by the desired text. To Add
of the colorbar by appending **+l** followed by the desired text. To add
and adjust the annotations (**a**) and ticks (**f**) append the letter
followed by the desired interval. The placement of the colorbar is set
via the ``position`` parameter. There are the following options:
Expand Down
86 changes: 64 additions & 22 deletions pygmt/clib/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Functions to convert data types into ctypes friendly formats.
"""

import ctypes as ctp
import warnings
from collections.abc import Sequence

import numpy as np
from pygmt.exceptions import GMTInvalidInput
Expand Down Expand Up @@ -243,41 +245,81 @@ def as_c_contiguous(array):
return array


def kwargs_to_ctypes_array(argument, kwargs, dtype):
def sequence_to_ctypes_array(sequence: Sequence, ctype, size: int) -> ctp.Array | None:
"""
Convert an iterable argument from kwargs into a ctypes array variable.
Convert a sequence of numbers into a ctypes array variable.
If the argument is not present in kwargs, returns ``None``.
If the sequence is ``None``, returns ``None``. Otherwise, returns a ctypes array.
The function only works for sequences of numbers. For converting a sequence of
strings, use ``strings_to_ctypes_array`` instead.
Parameters
----------
argument : str
The name of the argument.
kwargs : dict
Dictionary of keyword arguments.
dtype : ctypes type
The ctypes array type (e.g., ``ctypes.c_double*4``)
sequence
The sequence to convert. If ``None``, returns ``None``. Otherwise, it must be a
sequence (e.g., list, tuple, numpy array).
ctype
The ctypes type of the array (e.g., ``ctypes.c_int``).
size
The size of the array. If the sequence is smaller than the size, the remaining
elements will be filled with zeros. If the sequence is larger than the size, an
exception will be raised.
Returns
-------
ctypes_value : ctypes array or None
ctypes_array
The ctypes array variable.
Examples
--------
>>> import ctypes as ct
>>> value = kwargs_to_ctypes_array("bla", {"bla": [10, 10]}, ct.c_long * 2)
>>> type(value)
<class 'pygmt.clib.conversion.c_long_Array_2'>
>>> should_be_none = kwargs_to_ctypes_array(
... "swallow", {"bla": 1, "foo": [20, 30]}, ct.c_int * 2
... )
>>> print(should_be_none)
>>> import ctypes as ctp
>>> ctypes_array = sequence_to_ctypes_array([1, 2, 3], ctp.c_long, 3)
>>> type(ctypes_array)
<class 'pygmt.clib.conversion.c_long_Array_3'>
>>> ctypes_array[:]
[1, 2, 3]
>>> ctypes_array = sequence_to_ctypes_array([1, 2], ctp.c_long, 5)
>>> type(ctypes_array)
<class 'pygmt.clib.conversion.c_long_Array_5'>
>>> ctypes_array[:]
[1, 2, 0, 0, 0]
>>> ctypes_array = sequence_to_ctypes_array(None, ctp.c_long, 5)
>>> print(ctypes_array)
None
>>> ctypes_array = sequence_to_ctypes_array([1, 2, 3, 4, 5, 6], ctp.c_long, 5)
Traceback (most recent call last):
...
IndexError: invalid index
"""
if sequence is None:
return None
return (ctype * size)(*sequence)


def strings_to_ctypes_array(strings: Sequence[str]) -> ctp.Array:
"""
Convert a sequence (e.g., a list) of strings into a ctypes array.
Parameters
----------
strings
A sequence of strings.
Returns
-------
ctypes_array
A ctypes array of strings.
Examples
--------
>>> strings = ["first", "second", "third"]
>>> ctypes_array = strings_to_ctypes_array(strings)
>>> type(ctypes_array)
<class 'pygmt.clib.conversion.c_char_p_Array_3'>
>>> [s.decode() for s in ctypes_array]
['first', 'second', 'third']
"""
if argument in kwargs:
return dtype(*kwargs[argument])
return None
return (ctp.c_char_p * len(strings))(*[s.encode() for s in strings])


def array_to_datetime(array):
Expand Down
45 changes: 24 additions & 21 deletions pygmt/clib/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
array_to_datetime,
as_c_contiguous,
dataarray_to_matrix,
kwargs_to_ctypes_array,
sequence_to_ctypes_array,
strings_to_ctypes_array,
vectors_to_arrays,
)
from pygmt.clib.loading import load_libgmt
Expand Down Expand Up @@ -656,7 +657,17 @@ def call_module(self, module: str, args: list[str]):
f"Module '{module}' failed with status code {status}:\n{self._error_message}"
)

def create_data(self, family, geometry, mode, **kwargs):
def create_data(
self,
family,
geometry,
mode,
dim=None,
ranges=None,
inc=None,
registration="GMT_GRID_NODE_REG",
pad=None,
):
"""
Create an empty GMT data container.
Expand Down Expand Up @@ -720,15 +731,13 @@ def create_data(self, family, geometry, mode, **kwargs):
valid_modifiers=["GMT_GRID_IS_CARTESIAN", "GMT_GRID_IS_GEO"],
)
geometry_int = self._parse_constant(geometry, valid=GEOMETRIES)
registration_int = self._parse_constant(
kwargs.get("registration", "GMT_GRID_NODE_REG"), valid=REGISTRATIONS
)
registration_int = self._parse_constant(registration, valid=REGISTRATIONS)

# Convert dim, ranges, and inc to ctypes arrays if given (will be None
# if not given to represent NULL pointers)
dim = kwargs_to_ctypes_array("dim", kwargs, ctp.c_uint64 * 4)
ranges = kwargs_to_ctypes_array("ranges", kwargs, ctp.c_double * 4)
inc = kwargs_to_ctypes_array("inc", kwargs, ctp.c_double * 2)
dim = sequence_to_ctypes_array(dim, ctp.c_uint64, 4)
ranges = sequence_to_ctypes_array(ranges, ctp.c_double, 4)
inc = sequence_to_ctypes_array(inc, ctp.c_double, 2)

# Use a NULL pointer (None) for existing data to indicate that the
# container should be created empty. Fill it in later using put_vector
Expand All @@ -742,7 +751,7 @@ def create_data(self, family, geometry, mode, **kwargs):
ranges,
inc,
registration_int,
self._parse_pad(family, kwargs),
self._parse_pad(family, pad),
None,
)

Expand All @@ -751,15 +760,14 @@ def create_data(self, family, geometry, mode, **kwargs):

return data_ptr

def _parse_pad(self, family, kwargs):
def _parse_pad(self, family, pad):
"""
Parse and return an appropriate value for pad if none is given.
Pad is a bit tricky because, for matrix types, pad control the matrix ordering
(row or column major). Using the default pad will set it to column major and
mess things up with the numpy arrays.
"""
pad = kwargs.get("pad", None)
if pad is None:
pad = 0 if "MATRIX" in family else self["GMT_PAD_DEFAULT"]
return pad
Expand Down Expand Up @@ -918,13 +926,9 @@ def put_vector(self, dataset, column, vector):

gmt_type = self._check_dtype_and_dim(vector, ndim=1)
if gmt_type in (self["GMT_TEXT"], self["GMT_DATETIME"]):
vector_pointer = (ctp.c_char_p * len(vector))()
if gmt_type == self["GMT_DATETIME"]:
vector_pointer[:] = np.char.encode(
np.datetime_as_string(array_to_datetime(vector))
)
else:
vector_pointer[:] = np.char.encode(vector)
vector = np.datetime_as_string(array_to_datetime(vector))
vector_pointer = strings_to_ctypes_array(vector)
else:
vector_pointer = vector.ctypes.data_as(ctp.c_void_p)
status = c_put_vector(
Expand Down Expand Up @@ -981,13 +985,12 @@ def put_strings(self, dataset, family, strings):
restype=ctp.c_int,
)

strings_pointer = (ctp.c_char_p * len(strings))()
strings_pointer[:] = np.char.encode(strings)

family_int = self._parse_constant(
family, valid=FAMILIES, valid_modifiers=METHODS
)

strings_pointer = strings_to_ctypes_array(strings)

status = c_put_strings(
self.session_pointer, family_int, dataset, strings_pointer
)
Expand Down Expand Up @@ -1108,7 +1111,7 @@ def write_data(self, family, geometry, mode, wesn, output, data):
self["GMT_IS_FILE"],
geometry_int,
self[mode],
(ctp.c_double * 6)(*wesn),
sequence_to_ctypes_array(wesn, ctp.c_double, 6),
output.encode(),
data,
)
Expand Down
1 change: 1 addition & 0 deletions pygmt/tests/test_blockm.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def test_blockmean_input_filename():
data="@tut_ship.xyz",
spacing="5m",
region=[245, 255, 20, 30],
output_type="file",
outfile=tmpfile.name,
)
assert output is None # check that output is None since outfile is set
Expand Down
1 change: 1 addition & 0 deletions pygmt/tests/test_blockmedian.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def test_blockmedian_input_filename():
data="@tut_ship.xyz",
spacing="5m",
region=[245, 255, 20, 30],
output_type="file",
outfile=tmpfile.name,
)
assert output is None # check that output is None since outfile is set
Expand Down
4 changes: 3 additions & 1 deletion pygmt/tests/test_grdtrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def test_grdtrack_input_csvfile_and_dataarray(dataarray, expected_array):
Run grdtrack by passing in a csvfile and xarray.DataArray as inputs.
"""
with GMTTempFile() as tmpfile:
output = grdtrack(points=POINTS_DATA, grid=dataarray, outfile=tmpfile.name)
output = grdtrack(
points=POINTS_DATA, grid=dataarray, output_type="file", outfile=tmpfile.name
)
assert output is None # check that output is None since outfile is set
assert Path(tmpfile.name).stat().st_size > 0 # check that outfile exists
output = np.loadtxt(tmpfile.name)
Expand Down
1 change: 1 addition & 0 deletions pygmt/tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def test_project_output_filename(dataframe):
center=[0, -1],
azimuth=45,
flat_earth=True,
output_type="file",
outfile=tmpfile.name,
)
assert output is None # check that output is None since outfile is set
Expand Down
1 change: 1 addition & 0 deletions pygmt/tests/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def test_select_input_filename():
data="@tut_ship.xyz",
region=[250, 251, 26, 27],
z_subregion=["-/-630", "-120/0+a"],
output_type="file",
outfile=tmpfile.name,
)
assert output is None # check that output is None since outfile is set
Expand Down

0 comments on commit fe41846

Please sign in to comment.