Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 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,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)
<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 argument in kwargs:
return dtype(*kwargs[argument])
return None
if sequence is None:
return None
return (ctype * size)(*sequence)


def array_to_datetime(array):
Expand Down
31 changes: 19 additions & 12 deletions pygmt/clib/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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,
)

Expand All @@ -723,15 +731,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 @@ -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,
)
Expand Down