Skip to content

Commit 2bb82f4

Browse files
authored
Merge branch 'master' into testing
2 parents 8b78614 + 4939ee2 commit 2bb82f4

File tree

10 files changed

+264
-26
lines changed

10 files changed

+264
-26
lines changed

.github/workflows/ci_tests_dev.yaml

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# This workflow installs PyGMT dependencies, builds documentation and runs tests on GMT master
1+
# This workflow installs PyGMT dependencies, builds documentation and runs tests on GMT latest
22
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
33

4-
name: GMT Master Tests
4+
name: GMT Latest Tests
55

66
on:
77
# push:
@@ -14,13 +14,14 @@ on:
1414

1515
jobs:
1616
test_gmt_master:
17-
name: ${{ matrix.os }} - Python ${{ matrix.python-version }}
17+
name: ${{ matrix.os }} - GMT ${{ matrix.gmt_git_ref }}
1818
runs-on: ${{ matrix.os }}
1919
strategy:
2020
fail-fast: false
2121
matrix:
2222
python-version: [3.8]
2323
os: [ubuntu-20.04, macOS-10.15]
24+
gmt_git_ref: [6.1, master]
2425
env:
2526
# LD_LIBRARY_PATH: ${{ github.workspace }}/gmt/lib:$LD_LIBRARY_PATH
2627
GMT_INSTALL_DIR: ${{ github.workspace }}/gmt-install-dir
@@ -54,9 +55,11 @@ jobs:
5455
- name: Install build dependencies
5556
run: conda install cmake libblas libcblas liblapack fftw gdal ghostscript libnetcdf hdf5 zlib curl pcre ipython pytest pytest-cov pytest-mpl
5657

57-
# Install GMT master branch
58-
- name: Install GMT from master
58+
# Build and install latest GMT from GitHub
59+
- name: Install GMT ${{ matrix.gmt_git_ref }} branch
5960
run: curl https://raw.githubusercontent.com/GenericMappingTools/gmt/master/ci/build-gmt.sh | bash
61+
env:
62+
GMT_GIT_REF: ${{ matrix.gmt_git_ref }}
6063

6164
# Download cached remote files (artifacts) from Github
6265
- name: Download remote data from Github

AUTHORS.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The following people have contributed code to the project (alphabetical by last
99
and are considered the "PyGMT Developers":
1010

1111
* [Wei Ji Leong](https://github.com/weiji14)
12+
* [Tyler Newton](http://www.tnewton.com/)
1213
* [Dongdong Tian](https://seisman.info/)
1314
* [Liam Toney](https://liam.earth/)
1415
* [Leonardo Uieda](http://www.leouieda.com/)

MAINTENANCE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ There are 3 configuration files located in `.github/workflows`:
5454
This is ran on every commit on the *master* and Pull Request branches.
5555
It is also scheduled to run daily on the *master* branch.
5656

57-
2. `ci_tests_dev.yaml` (GMT Master Tests on Linux/macOS).
57+
2. `ci_tests_dev.yaml` (GMT Latest Tests on Linux/macOS).
5858

5959
This is only triggered when a review is requested or re-requested on a PR.
6060
It is also scheduled to run daily on the *master* branch.

README.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ PyGMT
1515
.. image:: https://github.com/GenericMappingTools/pygmt/workflows/Tests/badge.svg
1616
:alt: GitHub Actions Tests status
1717
:target: https://github.com/GenericMappingTools/pygmt/actions?query=workflow%3ATests
18-
.. image:: https://github.com/GenericMappingTools/pygmt/workflows/GMT%20Master%20Tests/badge.svg
19-
:alt: GitHub Actions GMT Master Tests status
20-
:target: https://github.com/GenericMappingTools/pygmt/actions?query=workflow%3A"GMT+Master+Tests"
18+
.. image:: https://github.com/GenericMappingTools/pygmt/workflows/GMT%20Latest%20Tests/badge.svg
19+
:alt: GitHub Actions GMT Latest Tests status
20+
:target: https://github.com/GenericMappingTools/pygmt/actions?query=workflow%3A%22GMT+Latest+Tests%22
2121
.. image:: https://img.shields.io/codecov/c/github/GenericMappingTools/pygmt/master.svg?style=flat-square
2222
:alt: Test coverage status
2323
:target: https://codecov.io/gh/GenericMappingTools/pygmt

pygmt/clib/session.py

+97-12
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
"GMT_IS_SURFACE",
4545
]
4646

47+
METHODS = ["GMT_IS_DUPLICATE", "GMT_IS_REFERENCE"]
48+
4749
MODES = ["GMT_CONTAINER_ONLY", "GMT_IS_OUTPUT"]
4850

4951
REGISTRATIONS = ["GMT_GRID_PIXEL_REG", "GMT_GRID_NODE_REG"]
@@ -235,7 +237,7 @@ def __getitem__(self, name):
235237
value = c_get_enum(session, name.encode())
236238

237239
if value is None or value == -99999:
238-
raise GMTCLibError("Constant '{}' doesn't exits in libgmt.".format(name))
240+
raise GMTCLibError(f"Constant '{name}' doesn't exist in libgmt.")
239241

240242
return value
241243

@@ -733,7 +735,7 @@ def put_vector(self, dataset, column, vector):
733735
"""
734736
Attach a numpy 1D array as a column on a GMT dataset.
735737
736-
Use this functions to attach numpy array data to a GMT dataset and pass
738+
Use this function to attach numpy array data to a GMT dataset and pass
737739
it to GMT modules. Wraps ``GMT_Put_Vector``.
738740
739741
The dataset must be created by :meth:`~gmt.clib.Session.create_data`
@@ -793,11 +795,72 @@ def put_vector(self, dataset, column, vector):
793795
)
794796
)
795797

798+
def put_strings(self, dataset, family, strings):
799+
"""
800+
Attach a numpy 1D array of dtype str as a column on a GMT dataset.
801+
802+
Use this function to attach string type numpy array data to a GMT
803+
dataset and pass it to GMT modules. Wraps ``GMT_Put_Strings``.
804+
805+
The dataset must be created by :meth:`~gmt.clib.Session.create_data`
806+
first.
807+
808+
.. warning::
809+
The numpy array must be C contiguous in memory. If it comes from a
810+
column slice of a 2d array, for example, you will have to make a
811+
copy. Use :func:`numpy.ascontiguousarray` to make sure your vector
812+
is contiguous (it won't copy if it already is).
813+
814+
Parameters
815+
----------
816+
dataset : :class:`ctypes.c_void_p`
817+
The ctypes void pointer to a ``GMT_Dataset``. Create it with
818+
:meth:`~gmt.clib.Session.create_data`.
819+
family : str
820+
The family type of the dataset. Can be either ``GMT_IS_VECTOR`` or
821+
``GMT_IS_MATRIX``.
822+
strings : numpy 1d-array
823+
The array that will be attached to the dataset. Must be a 1d C
824+
contiguous array.
825+
826+
Raises
827+
------
828+
GMTCLibError
829+
If given invalid input or ``GMT_Put_Strings`` exits with status !=
830+
0.
831+
832+
"""
833+
c_put_strings = self.get_libgmt_func(
834+
"GMT_Put_Strings",
835+
argtypes=[
836+
ctp.c_void_p,
837+
ctp.c_uint,
838+
ctp.c_void_p,
839+
ctp.POINTER(ctp.c_char_p),
840+
],
841+
restype=ctp.c_int,
842+
)
843+
844+
strings_pointer = (ctp.c_char_p * len(strings))()
845+
strings_pointer[:] = np.char.encode(strings)
846+
847+
family_int = self._parse_constant(
848+
family, valid=FAMILIES, valid_modifiers=METHODS
849+
)
850+
851+
status = c_put_strings(
852+
self.session_pointer, family_int, dataset, strings_pointer
853+
)
854+
if status != 0:
855+
raise GMTCLibError(
856+
f"Failed to put strings of type {strings.dtype} into dataset"
857+
)
858+
796859
def put_matrix(self, dataset, matrix, pad=0):
797860
"""
798861
Attach a numpy 2D array to a GMT dataset.
799862
800-
Use this functions to attach numpy array data to a GMT dataset and pass
863+
Use this function to attach numpy array data to a GMT dataset and pass
801864
it to GMT modules. Wraps ``GMT_Put_Matrix``.
802865
803866
The dataset must be created by :meth:`~gmt.clib.Session.create_data`
@@ -1002,9 +1065,7 @@ def open_virtual_file(self, family, geometry, direction, data):
10021065
family_int = self._parse_constant(family, valid=FAMILIES, valid_modifiers=VIAS)
10031066
geometry_int = self._parse_constant(geometry, valid=GEOMETRIES)
10041067
direction_int = self._parse_constant(
1005-
direction,
1006-
valid=["GMT_IN", "GMT_OUT"],
1007-
valid_modifiers=["GMT_IS_REFERENCE", "GMT_IS_DUPLICATE"],
1068+
direction, valid=["GMT_IN", "GMT_OUT"], valid_modifiers=METHODS,
10081069
)
10091070

10101071
buff = ctp.create_string_buffer(self["GMT_VF_LEN"])
@@ -1079,14 +1140,23 @@ def virtualfile_from_vectors(self, *vectors):
10791140
10801141
"""
10811142
# Conversion to a C-contiguous array needs to be done here and not in
1082-
# put_matrix because we need to maintain a reference to the copy while
1083-
# it is being used by the C API. Otherwise, the array would be garbage
1084-
# collected and the memory freed. Creating it in this context manager
1085-
# guarantees that the copy will be around until the virtual file is
1086-
# closed. The conversion is implicit in vectors_to_arrays.
1143+
# put_vector or put_strings because we need to maintain a reference to
1144+
# the copy while it is being used by the C API. Otherwise, the array
1145+
# would be garbage collected and the memory freed. Creating it in this
1146+
# context manager guarantees that the copy will be around until the
1147+
# virtual file is closed. The conversion is implicit in
1148+
# vectors_to_arrays.
10871149
arrays = vectors_to_arrays(vectors)
10881150

10891151
columns = len(arrays)
1152+
# Find arrays that are of string dtype from column 3 onwards
1153+
# Assumes that first 2 columns contains coordinates like longitude
1154+
# latitude, or datetime string types.
1155+
for col, array in enumerate(arrays[2:]):
1156+
if np.issubdtype(array.dtype, np.str_):
1157+
columns = col + 2
1158+
break
1159+
10901160
rows = len(arrays[0])
10911161
if not all(len(i) == rows for i in arrays):
10921162
raise GMTInvalidInput("All arrays must have same size.")
@@ -1098,9 +1168,24 @@ def virtualfile_from_vectors(self, *vectors):
10981168
family, geometry, mode="GMT_CONTAINER_ONLY", dim=[columns, rows, 1, 0]
10991169
)
11001170

1101-
for col, array in enumerate(arrays):
1171+
# Use put_vector for columns with numerical type data
1172+
for col, array in enumerate(arrays[:columns]):
11021173
self.put_vector(dataset, column=col, vector=array)
11031174

1175+
# Use put_strings for last column(s) with string type data
1176+
# Have to use modifier "GMT_IS_DUPLICATE" to duplicate the strings
1177+
string_arrays = arrays[columns:]
1178+
if string_arrays:
1179+
if len(string_arrays) == 1:
1180+
strings = string_arrays[0]
1181+
elif len(string_arrays) > 1:
1182+
strings = np.apply_along_axis(
1183+
func1d=" ".join, axis=0, arr=string_arrays
1184+
)
1185+
self.put_strings(
1186+
dataset, family="GMT_IS_VECTOR|GMT_IS_DUPLICATE", strings=strings
1187+
)
1188+
11041189
with self.open_virtual_file(
11051190
family, geometry, "GMT_IN|GMT_IS_REFERENCE", dataset
11061191
) as vfile:

pygmt/helpers/decorators.py

+32-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import textwrap
99
import functools
1010

11+
import numpy as np
12+
1113
from .utils import is_nonstr_iter
1214
from ..exceptions import GMTInvalidInput
1315

@@ -302,6 +304,23 @@ def kwargs_to_strings(convert_bools=True, **conversions):
302304
>>> module(123, bla=(1, 2, 3), foo=True, A=False, i=(5, 6))
303305
{'bla': (1, 2, 3), 'foo': '', 'i': '5,6'}
304306
args: 123
307+
>>> import datetime
308+
>>> module(
309+
... R=[
310+
... np.datetime64("2010-01-01T16:00:00"),
311+
... datetime.datetime(2020, 1, 1, 12, 23, 45),
312+
... ]
313+
... )
314+
{'R': '2010-01-01T16:00:00/2020-01-01T12:23:45.000000'}
315+
>>> import pandas as pd
316+
>>> import xarray as xr
317+
>>> module(
318+
... R=[
319+
... xr.DataArray(data=np.datetime64("2005-01-01T08:00:00")),
320+
... pd.Timestamp("2015-01-01T12:00:00.123456789"),
321+
... ]
322+
... )
323+
{'R': '2005-01-01T08:00:00.000000000/2015-01-01T12:00:00.123456'}
305324
306325
"""
307326
valid_conversions = [
@@ -338,9 +357,19 @@ def new_module(*args, **kwargs):
338357
value = kwargs[arg]
339358
issequence = fmt in separators
340359
if issequence and is_nonstr_iter(value):
341-
kwargs[arg] = separators[fmt].join(
342-
"{}".format(item) for item in value
343-
)
360+
for index, item in enumerate(value):
361+
try:
362+
# check if there is a space " " when converting
363+
# a pandas.Timestamp/xr.DataArray to a string.
364+
# If so, use np.datetime_as_string instead.
365+
assert " " not in str(item)
366+
except AssertionError:
367+
# convert datetime-like item to ISO 8601
368+
# string format like YYYY-MM-DDThh:mm:ss.ffffff
369+
value[index] = np.datetime_as_string(
370+
np.asarray(item, dtype=np.datetime64)
371+
)
372+
kwargs[arg] = separators[fmt].join(f"{item}" for item in value)
344373
# Execute the original function and return its output
345374
return module_func(*args, **kwargs)
346375

pygmt/tests/test_clib.py

+44
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
2929

30+
with clib.Session() as _lib:
31+
gmt_version = Version(_lib.info["version"])
32+
3033

3134
@contextmanager
3235
def mock(session, func, returns=None, mock_func=None):
@@ -399,6 +402,47 @@ def test_virtualfile_from_vectors():
399402
assert output == expected
400403

401404

405+
@pytest.mark.xfail(
406+
condition=gmt_version < Version("6.1.1"),
407+
reason="GMT_Put_Strings only works for GMT 6.1.1 and above",
408+
)
409+
def test_virtualfile_from_vectors_one_string_column():
410+
"Test passing in one column with string dtype into virtual file dataset"
411+
size = 5
412+
x = np.arange(size, dtype=np.int32)
413+
y = np.arange(size, size * 2, 1, dtype=np.int32)
414+
strings = np.array(["a", "bc", "defg", "hijklmn", "opqrst"], dtype=np.str)
415+
with clib.Session() as lib:
416+
with lib.virtualfile_from_vectors(x, y, strings) as vfile:
417+
with GMTTempFile() as outfile:
418+
lib.call_module("convert", f"{vfile} ->{outfile.name}")
419+
output = outfile.read(keep_tabs=True)
420+
expected = "".join(f"{i}\t{j}\t{k}\n" for i, j, k in zip(x, y, strings))
421+
assert output == expected
422+
423+
424+
@pytest.mark.xfail(
425+
condition=gmt_version < Version("6.1.1"),
426+
reason="GMT_Put_Strings only works for GMT 6.1.1 and above",
427+
)
428+
def test_virtualfile_from_vectors_two_string_columns():
429+
"Test passing in two columns of string dtype into virtual file dataset"
430+
size = 5
431+
x = np.arange(size, dtype=np.int32)
432+
y = np.arange(size, size * 2, 1, dtype=np.int32)
433+
strings1 = np.array(["a", "bc", "def", "ghij", "klmno"], dtype=np.str)
434+
strings2 = np.array(["pqrst", "uvwx", "yz!", "@#", "$"], dtype=np.str)
435+
with clib.Session() as lib:
436+
with lib.virtualfile_from_vectors(x, y, strings1, strings2) as vfile:
437+
with GMTTempFile() as outfile:
438+
lib.call_module("convert", f"{vfile} ->{outfile.name}")
439+
output = outfile.read(keep_tabs=True)
440+
expected = "".join(
441+
f"{h}\t{i}\t{j} {k}\n" for h, i, j, k in zip(x, y, strings1, strings2)
442+
)
443+
assert output == expected
444+
445+
402446
def test_virtualfile_from_vectors_transpose():
403447
"Test transforming matrix columns to virtual file dataset"
404448
dtypes = "float32 float64 int32 int64 uint32 uint64".split()

pygmt/tests/test_clib_put_matrix.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_put_matrix_grid():
7676
with GMTTempFile() as tmp_file:
7777
lib.write_data(
7878
"GMT_IS_MATRIX",
79-
"GMT_IS_SURFACE",
79+
"GMT_IS_POINT",
8080
"GMT_CONTAINER_AND_DATA",
8181
wesn,
8282
tmp_file.name,

0 commit comments

Comments
 (0)