From 47309616e40a78b180995379da0a9bf9057027ae Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:51:50 +1300 Subject: [PATCH 1/8] Support timedelta64 dtype as input Map np.timedelta64 to GMT_LONG in clib/session.py's DTYPES dictionary. Added a unit test in test_clib_put_vectors.py to test passing a numpy array with timedelta64 dtypes of various time units (year to microsecond). --- pygmt/clib/session.py | 1 + pygmt/tests/test_clib_put_vector.py | 31 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 362ba53b4a9..6e913c0dabe 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -85,6 +85,7 @@ np.float64: "GMT_DOUBLE", np.str_: "GMT_TEXT", np.datetime64: "GMT_DATETIME", + np.timedelta64: "GMT_LONG", } diff --git a/pygmt/tests/test_clib_put_vector.py b/pygmt/tests/test_clib_put_vector.py index 09a4235298b..ccd4a6cd86c 100644 --- a/pygmt/tests/test_clib_put_vector.py +++ b/pygmt/tests/test_clib_put_vector.py @@ -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. From d53138c29af478e7d206cbb1c03ff0d4d7ab6d37 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 16 Dec 2023 16:39:14 +1300 Subject: [PATCH 2/8] Add unit test for plotting timedelta64 data Make a 2D plot with Forecast Days (timedelta64) on the x-axis, and RMSE on the y-axis. --- .../baseline/test_plot_timedelta64.png.dvc | 4 ++++ pygmt/tests/test_plot.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 pygmt/tests/baseline/test_plot_timedelta64.png.dvc diff --git a/pygmt/tests/baseline/test_plot_timedelta64.png.dvc b/pygmt/tests/baseline/test_plot_timedelta64.png.dvc new file mode 100644 index 00000000000..03709367a04 --- /dev/null +++ b/pygmt/tests/baseline/test_plot_timedelta64.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 9b4172c3c587ab31b7dac5fad2de0718 + size: 13189 + path: test_plot_timedelta64.png diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 31d2c51326e..5832f90abf8 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -452,6 +452,25 @@ 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=[0, 8, 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" ) From 51bc55a9a0bbacb1e0198d41f8770f2c97bc5c4f Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 16 Dec 2023 16:43:00 +1300 Subject: [PATCH 3/8] Lint --- pygmt/tests/test_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 5832f90abf8..2815c37ba92 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -471,6 +471,7 @@ def test_plot_timedelta64(): ) return fig + @pytest.mark.mpl_image_compare( filename="test_plot_ogrgmt_file_multipoint_default_style.png" ) From 49dbe83c7c292be533c46ba5cb7c606a288fddeb Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 16 Dec 2023 20:39:07 +1300 Subject: [PATCH 4/8] Recreate baseline image using ghostscript 9.54.0 Was using 9.56.1 before. --- pygmt/tests/baseline/test_plot_timedelta64.png.dvc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/tests/baseline/test_plot_timedelta64.png.dvc b/pygmt/tests/baseline/test_plot_timedelta64.png.dvc index 03709367a04..176605a0fb8 100644 --- a/pygmt/tests/baseline/test_plot_timedelta64.png.dvc +++ b/pygmt/tests/baseline/test_plot_timedelta64.png.dvc @@ -1,4 +1,4 @@ outs: -- md5: 9b4172c3c587ab31b7dac5fad2de0718 - size: 13189 +- md5: a045e84c478a7ebc5a62c5b39b8229a5 + size: 13016 path: test_plot_timedelta64.png From d5bedc2ac71a337f2b2952843778d2e8c78c29a5 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 16 Dec 2023 21:42:33 +1300 Subject: [PATCH 5/8] Allow np.timedelta64 inputs to region parameter Cast np.timedelta64 inputs to int, so that they can be understood by GMT. --- pygmt/helpers/decorators.py | 34 +++++++++++++++++++++++++--------- pygmt/tests/test_plot.py | 2 +- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index 2db184efada..f7234a5f57a 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -5,6 +5,7 @@ arguments, insert common text into docstrings, transform arguments to strings, etc. """ +import datetime import functools import textwrap import warnings @@ -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'} + >>> import pandas as pd >>> import xarray as xr >>> module( @@ -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( @@ -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") + 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) + ) newvalue = separators[fmt].join(f"{item}" for item in value) # Changes in bound.arguments will reflect in bound.args # and bound.kwargs. diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 2815c37ba92..b21e316697a 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -460,7 +460,7 @@ def test_plot_timedelta64(): fig = Figure() fig.basemap( projection="X8c/5c", - region=[0, 8, 0, 10], + region=[np.timedelta64(0, "D"), np.timedelta64(8, "D"), 0, 10], frame=["WSne", "xaf+lForecast Days", "yaf+lRMSE"], ) fig.plot( From 44e62d619db7f9dc00e2ccab2dc46d45f30fcfdb Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 16 Dec 2023 21:51:39 +1300 Subject: [PATCH 6/8] Fix lint error --- pygmt/helpers/decorators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index f7234a5f57a..5addedbc3af 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -5,7 +5,6 @@ arguments, insert common text into docstrings, transform arguments to strings, etc. """ -import datetime import functools import textwrap import warnings From 849feda91c37a4ca2b449309b518c0a9310e5820 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:04:21 +1300 Subject: [PATCH 7/8] Revert "Allow np.timedelta64 inputs to region parameter" This reverts commit d5bedc2ac71a337f2b2952843778d2e8c78c29a5. --- pygmt/helpers/decorators.py | 33 +++++++++------------------------ pygmt/tests/test_plot.py | 2 +- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index ea967d8f021..3d8b3a6758e 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -673,19 +673,14 @@ 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/0/24'} - + {'R': '2010-01-01T16:00:00/2020-01-01T12:23:45.000000'} >>> import pandas as pd >>> import xarray as xr >>> module( @@ -695,7 +690,6 @@ 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( @@ -766,23 +760,14 @@ 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 " " 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") - 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) - ) + # 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) + ) newvalue = separators[fmt].join(f"{item}" for item in value) # Changes in bound.arguments will reflect in bound.args # and bound.kwargs. diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index b21e316697a..2815c37ba92 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -460,7 +460,7 @@ def test_plot_timedelta64(): fig = Figure() fig.basemap( projection="X8c/5c", - region=[np.timedelta64(0, "D"), np.timedelta64(8, "D"), 0, 10], + region=[0, 8, 0, 10], frame=["WSne", "xaf+lForecast Days", "yaf+lRMSE"], ) fig.plot( From 4426d951a753247746af5327408714aaf4d6f943 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:53:18 +1300 Subject: [PATCH 8/8] Update test_plot_timedelta64 baseline img with GMT 6.5.0 and gs 10.02.1 --- pygmt/tests/baseline/test_plot_timedelta64.png.dvc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygmt/tests/baseline/test_plot_timedelta64.png.dvc b/pygmt/tests/baseline/test_plot_timedelta64.png.dvc index 176605a0fb8..087a903145f 100644 --- a/pygmt/tests/baseline/test_plot_timedelta64.png.dvc +++ b/pygmt/tests/baseline/test_plot_timedelta64.png.dvc @@ -1,4 +1,5 @@ outs: -- md5: a045e84c478a7ebc5a62c5b39b8229a5 - size: 13016 +- md5: 8edddcec764d244053c4d675e98732b9 + size: 13201 path: test_plot_timedelta64.png + hash: md5