diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index 27cda9f971c..bd19a38d591 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -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 @@ -243,41 +245,55 @@ 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) - - >>> 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) + + >>> ctypes_array[:] + [1, 2, 3] + >>> ctypes_array = sequence_to_ctypes_array([1, 2], ctp.c_long, 5) + >>> type(ctypes_array) + + >>> 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 argument in kwargs: - return dtype(*kwargs[argument]) - return None + if sequence is None: + return None + return (ctype * size)(*sequence) def array_to_datetime(array): diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 1bea0e4b886..2caa8e55cd0 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -19,7 +19,7 @@ array_to_datetime, as_c_contiguous, dataarray_to_matrix, - kwargs_to_ctypes_array, + sequence_to_ctypes_array, vectors_to_arrays, ) from pygmt.clib.loading import load_libgmt @@ -628,7 +628,17 @@ def call_module(self, module, args): 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. @@ -692,15 +702,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 @@ -714,7 +722,7 @@ def create_data(self, family, geometry, mode, **kwargs): ranges, inc, registration_int, - self._parse_pad(family, kwargs), + self._parse_pad(family, pad), None, ) @@ -723,7 +731,7 @@ 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. @@ -731,7 +739,6 @@ def _parse_pad(self, family, kwargs): (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 @@ -1080,7 +1087,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, )