Skip to content
1 change: 1 addition & 0 deletions pygmt/clib/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
np.float64: "GMT_DOUBLE",
np.str_: "GMT_TEXT",
np.datetime64: "GMT_DATETIME",
np.timedelta64: "GMT_LONG",
}


Expand Down
34 changes: 25 additions & 9 deletions pygmt/helpers/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
arguments, insert common text into docstrings, transform arguments to strings,
etc.
"""
import datetime
import functools
import textwrap
import warnings
Expand Down Expand Up @@ -673,14 +674,19 @@ def kwargs_to_strings(**conversions):
>>> module(123, bla=(1, 2, 3), foo=True, A=False, i=(5, 6))
{'A': False, 'bla': (1, 2, 3), 'foo': True, 'i': '5,6'}
args: 123

>>> # Test that region accepts arguments with datetime or timedelta type
>>> import datetime
>>> module(
... R=[
... np.datetime64("2010-01-01T16:00:00"),
... datetime.datetime(2020, 1, 1, 12, 23, 45),
... np.timedelta64(0, "h"),
... np.timedelta64(24, "h"),
... ]
... )
{'R': '2010-01-01T16:00:00/2020-01-01T12:23:45.000000'}
{'R': '2010-01-01T16:00:00/2020-01-01T12:23:45.000000/0/24'}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently Python has a built-in datetime.timedelta objects, but it's super hard to cast it to an integer type while preserving the orignal unit (e.g. day, hour, minute, etc). E.g.

import datetime

td = datetime.timedelta(hours=24)

print(np.timedelta64(td))
# numpy.timedelta64(86400000000,'us')
print(np.timedelta64(td).astype(int))
# 86400000000

Should we still support datetime.timedelta? Or only np.timedelta64?


>>> import pandas as pd
>>> import xarray as xr
>>> module(
Expand All @@ -690,6 +696,7 @@ def kwargs_to_strings(**conversions):
... ]
... )
{'R': '2005-01-01T08:00:00.000000000/2015-01-01T12:00:00.123456'}

>>> # Here is a more realistic example
>>> # See https://github.com/GenericMappingTools/pygmt/issues/2361
>>> @kwargs_to_strings(
Expand Down Expand Up @@ -760,14 +767,23 @@ def new_module(*args, **kwargs):
if fmt in separators and is_nonstr_iter(value):
for index, item in enumerate(value):
if " " in str(item):
# Check if there is a space " " when converting
# a pandas.Timestamp/xr.DataArray to a string.
# If so, use np.datetime_as_string instead.
# Convert datetime-like item to ISO 8601
# string format like YYYY-MM-DDThh:mm:ss.ffffff.
value[index] = np.datetime_as_string(
np.asarray(item, dtype=np.datetime64)
)
# Check if there is a space " " in the item, which
# is typically present in objects such as
# np.timedelta64, pd.Timestamp, or xr.DataArray.
# If so, convert the item to a numerical or string
# type that is understood by GMT as follows:
if getattr(
getattr(item, "dtype", ""), "name", ""
).startswith("timedelta"):
# A np.timedelta64 item is cast to integer
value[index] = item.astype("int")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This double getattr to obtain item.dtype.name isn't very nice, wondering if there's a good alternative.

else:
# A pandas.Timestamp/xr.DataArray containing
# a datetime-like object is cast to ISO 8601
# string format like YYYY-MM-DDThh:mm:ss.ffffff
value[index] = np.datetime_as_string(
np.asarray(item, dtype=np.datetime64)
)
Copy link
Member Author

@weiji14 weiji14 Dec 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if-then logic here might get a lot more complicated if we also decide to support passing PyArrow time/date types (xref #2800) into region/-R. At such, it might be good to isolate this logic into a separate function instead, so I'm thinking of maybe reverting commit d5bedc2 for now, but cherry-pick that change into a separate PR.

Specifically, we might want to create a dedicated _cast_datetime_to_str function. There's actually something similar used in solar (see 6406b43), so we could potentially re-use the function in many places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, commit reverted in 849feda. Should be ready to review now.

newvalue = separators[fmt].join(f"{item}" for item in value)
# Changes in bound.arguments will reflect in bound.args
# and bound.kwargs.
Expand Down
4 changes: 4 additions & 0 deletions pygmt/tests/baseline/test_plot_timedelta64.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
outs:
- md5: a045e84c478a7ebc5a62c5b39b8229a5
size: 13016
path: test_plot_timedelta64.png
31 changes: 31 additions & 0 deletions pygmt/tests/test_clib_put_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,37 @@ def test_put_vector_string_dtype():
npt.assert_array_equal(output["y"], expected_vectors[j])


def test_put_vector_timedelta64_dtype():
"""
Passing timedelta64 type vectors with various time units (year, month,
week, day, hour, minute, second, millisecond, microsecond) to a dataset.
"""
for unit in ["Y", "M", "W", "D", "h", "m", "s", "ms", "μs"]:
with clib.Session() as lib, GMTTempFile() as tmp_file:
dataset = lib.create_data(
family="GMT_IS_DATASET|GMT_VIA_VECTOR",
geometry="GMT_IS_POINT",
mode="GMT_CONTAINER_ONLY",
dim=[1, 5, 1, 0], # columns, rows, layers, dtype
)
timedata = np.arange(np.timedelta64(0, unit), np.timedelta64(5, unit))
lib.put_vector(dataset, column=0, vector=timedata)
# Turns out wesn doesn't matter for Datasets
wesn = [0] * 6
# Save the data to a file to see if it's being accessed correctly
lib.write_data(
family="GMT_IS_VECTOR",
geometry="GMT_IS_POINT",
mode="GMT_WRITE_SET",
wesn=wesn,
output=tmp_file.name,
data=dataset,
)
# Load the data and check that it's correct
newtimedata = tmp_file.loadtxt(unpack=True, dtype=f"timedelta64[{unit}]")
npt.assert_equal(actual=newtimedata, desired=timedata)


def test_put_vector_invalid_dtype():
"""
Check that it fails with an exception for invalid data types.
Expand Down
20 changes: 20 additions & 0 deletions pygmt/tests/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,26 @@ def test_plot_datetime():
return fig


@pytest.mark.mpl_image_compare
def test_plot_timedelta64():
"""
Test plotting numpy.timedelta64 input data.
"""
fig = Figure()
fig.basemap(
projection="X8c/5c",
region=[np.timedelta64(0, "D"), np.timedelta64(8, "D"), 0, 10],
frame=["WSne", "xaf+lForecast Days", "yaf+lRMSE"],
)
fig.plot(
x=np.arange(np.timedelta64(0, "D"), np.timedelta64(8, "D")),
y=np.geomspace(start=0.1, stop=9, num=8),
style="c0.2c",
pen="1p",
)
return fig


@pytest.mark.mpl_image_compare(
filename="test_plot_ogrgmt_file_multipoint_default_style.png"
)
Expand Down