From b81aa06a1d4902777a61961c1df74371367c4adb Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:09:56 +1200 Subject: [PATCH 01/41] Implement gmtread xarray BackendEntrypoint Initial implementation of a 'gmtread' xarray BackendEntrypoint for decoding NetCDF files! Following instructions at https://docs.xarray.dev/en/v2025.03.1/internals/how-to-add-new-backend.html on registering a backend. Only a minimal implementation for now to read kind=grid data. --- pygmt/tests/test_xarray_backend.py | 20 +++++++++++ pygmt/xarray_backend.py | 56 ++++++++++++++++++++++++++++++ pyproject.toml | 3 ++ 3 files changed, 79 insertions(+) create mode 100644 pygmt/tests/test_xarray_backend.py create mode 100644 pygmt/xarray_backend.py diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py new file mode 100644 index 00000000000..3ba22dcaa27 --- /dev/null +++ b/pygmt/tests/test_xarray_backend.py @@ -0,0 +1,20 @@ +""" +Tests for xarray 'gmtread' backend engine. +""" + +import xarray as xr +from pygmt.enums import GridRegistration, GridType + + +# %% +def test_xarray_backend_gmtread(): + """ + Ensure that passing engine='gmtread' to xarray.open_dataarray works. + """ + with xr.open_dataarray( + filename_or_obj="@static_earth_relief.nc", engine="gmtread" + ) as da: + assert da.sizes == {"lat": 14, "lon": 8} + assert da.dtype == "float32" + assert da.gmt.registration == GridRegistration.GRIDLINE + assert da.gmt.gtype == GridType.CARTESIAN diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py new file mode 100644 index 00000000000..ad3807ea545 --- /dev/null +++ b/pygmt/xarray_backend.py @@ -0,0 +1,56 @@ +""" +An xarray backend for reading grid files using the 'gmtread' engine. +""" + +from pathlib import Path + +import xarray as xr +from pygmt.clib import Session +from pygmt.helpers import build_arg_list +from xarray.backends import BackendEntrypoint + + +class GMTReadBackendEntrypoint(BackendEntrypoint): + """ + Xarray backend to read grid files using 'gmtread' engine. + + Relies on the libgdal-netcdf driver used by GMT C. + """ + + description = "Use .nc files in Xarray" + open_dataset_parameters = ("filename_or_obj",) + url = "https://github.com/GenericMappingTools/pygmt" + + def open_dataset( + self, + filename_or_obj: str, + *, + drop_variables=None, # noqa: ARG002 + # other backend specific keyword arguments + # `chunks` and `cache` DO NOT go here, they are handled by xarray + ) -> xr.Dataset: + """ + Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. + """ + with Session() as lib: + with lib.virtualfile_out(kind="grid") as voutfile: + lib.call_module( + module="read", + args=[filename_or_obj, voutfile, *build_arg_list({"T": "g"})], + ) + + raster: xr.DataArray = lib.virtualfile_to_raster( + vfname=voutfile, kind="grid" + ) + _ = raster.gmt # Load GMTDataArray accessor information + return raster.to_dataset() + + def guess_can_open(self, filename_or_obj) -> bool: + """ + Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. + """ + try: + ext = Path(filename_or_obj).suffix + except TypeError: + return False + return ext in {".nc"} diff --git a/pyproject.toml b/pyproject.toml index 2ebe59e7f24..f07340f75ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,9 @@ all = [ "rioxarray", ] +[project.entry-points."xarray.backends"] +gmtread = "pygmt.xarray_backend:GMTReadBackendEntrypoint" + [project.urls] "Homepage" = "https://www.pygmt.org" "Documentation" = "https://www.pygmt.org" From 2b98a65594cb1873bbbbecc3b36438cd05690bc9 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:25:51 +1200 Subject: [PATCH 02/41] Add source encoding to raster xarray.DataArray So that GMTDataArray accessor knows the path to the original NetCDF file to retrieve correct metadata. --- pygmt/xarray_backend.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index ad3807ea545..641d483013a 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -7,6 +7,7 @@ import xarray as xr from pygmt.clib import Session from pygmt.helpers import build_arg_list +from pygmt.src.which import which from xarray.backends import BackendEntrypoint @@ -42,6 +43,11 @@ def open_dataset( raster: xr.DataArray = lib.virtualfile_to_raster( vfname=voutfile, kind="grid" ) + # Add "source" encoding + source = which(fname=filename_or_obj) + raster.encoding["source"] = ( + source[0] if isinstance(source, list) else source + ) _ = raster.gmt # Load GMTDataArray accessor information return raster.to_dataset() From f41a812ad9c3a0317be364032a9c7d6fadd997e2 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:30:00 +1200 Subject: [PATCH 03/41] Set load_dataarray's default engine to gmtread Use gmtread as default engine instead of netcdf4. --- pygmt/io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pygmt/io.py b/pygmt/io.py index 9451de36c8f..c1619827a34 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -5,7 +5,7 @@ import xarray as xr -def load_dataarray(filename_or_obj, **kwargs): +def load_dataarray(filename_or_obj, engine="gmtread", **kwargs): """ Open, load into memory, and close a DataArray from a file or file-like object containing a single data variable. @@ -32,6 +32,8 @@ def load_dataarray(filename_or_obj, **kwargs): ------- datarray : xarray.DataArray The newly created DataArray. + engine : str + Engine to use when reading files. Default engine is 'gmtread'. See Also -------- @@ -41,7 +43,7 @@ def load_dataarray(filename_or_obj, **kwargs): msg = "'cache' has no effect in this context." raise TypeError(msg) - with xr.open_dataarray(filename_or_obj, **kwargs) as dataarray: + with xr.open_dataarray(filename_or_obj, engine=engine, **kwargs) as dataarray: result = dataarray.load() _ = result.gmt # load GMTDataArray accessor information From b71dcf531ceda5bc39ac2d41225513cf9b51b9c5 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:04:04 +1200 Subject: [PATCH 04/41] Support reading GeoTIFF images via the kind='image' argument Default is still kind='grid', by allow use of kind='image' too. --- pygmt/io.py | 13 +++++++++++-- pygmt/tests/test_xarray_backend.py | 24 +++++++++++++++++++----- pygmt/xarray_backend.py | 22 +++++++++++++--------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/pygmt/io.py b/pygmt/io.py index c1619827a34..1367c1afcdf 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -2,10 +2,15 @@ PyGMT input/output (I/O) utilities. """ +from typing import Literal + import xarray as xr -def load_dataarray(filename_or_obj, engine="gmtread", **kwargs): + +def load_dataarray( + filename_or_obj, engine="gmtread", kind: Literal["grid", "image"] = "grid", **kwargs +): """ Open, load into memory, and close a DataArray from a file or file-like object containing a single data variable. @@ -34,6 +39,8 @@ def load_dataarray(filename_or_obj, engine="gmtread", **kwargs): The newly created DataArray. engine : str Engine to use when reading files. Default engine is 'gmtread'. + kind + The kind of data to read. Valid values are ``"grid"``, and ``"image"``. See Also -------- @@ -43,7 +50,9 @@ def load_dataarray(filename_or_obj, engine="gmtread", **kwargs): msg = "'cache' has no effect in this context." raise TypeError(msg) - with xr.open_dataarray(filename_or_obj, engine=engine, **kwargs) as dataarray: + with xr.open_dataarray( + filename_or_obj, engine=engine, kind=kind, **kwargs + ) as dataarray: result = dataarray.load() _ = result.gmt # load GMTDataArray accessor information diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 3ba22dcaa27..4e59a493922 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -6,15 +6,29 @@ from pygmt.enums import GridRegistration, GridType -# %% -def test_xarray_backend_gmtread(): +def test_xarray_backend_gmtread_grid(): """ - Ensure that passing engine='gmtread' to xarray.open_dataarray works. + Ensure that passing engine='gmtread' to xarray.open_dataarray works for reading + NetCDF grids. """ with xr.open_dataarray( filename_or_obj="@static_earth_relief.nc", engine="gmtread" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" - assert da.gmt.registration == GridRegistration.GRIDLINE - assert da.gmt.gtype == GridType.CARTESIAN + assert da.gmt.registration == GridRegistration.PIXEL + assert da.gmt.gtype == GridType.GEOGRAPHIC + + +def test_xarray_backend_gmtread_image(): + """ + Ensure that passing engine='gmtread' to xarray.open_dataarray works for reading + GeoTIFF images. + """ + with xr.open_dataarray( + filename_or_obj="@earth_day_01d", engine="gmtread", kind="image" + ) as da: + assert da.sizes == {"band": 3, "y": 180, "x": 360} + assert da.dtype == "uint8" + assert da.gmt.registration == GridRegistration.PIXEL + assert da.gmt.gtype == GridType.GEOGRAPHIC diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 641d483013a..a8835b2b19a 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -1,8 +1,9 @@ """ -An xarray backend for reading grid files using the 'gmtread' engine. +An xarray backend for reading grid/image files using the 'gmtread' engine. """ from pathlib import Path +from typing import Literal import xarray as xr from pygmt.clib import Session @@ -13,13 +14,14 @@ class GMTReadBackendEntrypoint(BackendEntrypoint): """ - Xarray backend to read grid files using 'gmtread' engine. + Xarray backend to read grid/image files using 'gmtread' engine. - Relies on the libgdal-netcdf driver used by GMT C. + Relies on the libgdal-netcdf driver used by GMT C for NetCDF files, and libgdal for + GeoTIFF files. """ - description = "Use .nc files in Xarray" - open_dataset_parameters = ("filename_or_obj",) + description = "Open .nc and .tif files in Xarray via GMT read." + open_dataset_parameters = ("filename_or_obj", "kind") url = "https://github.com/GenericMappingTools/pygmt" def open_dataset( @@ -27,6 +29,7 @@ def open_dataset( filename_or_obj: str, *, drop_variables=None, # noqa: ARG002 + kind: Literal["grid", "image"] = "grid", # other backend specific keyword arguments # `chunks` and `cache` DO NOT go here, they are handled by xarray ) -> xr.Dataset: @@ -34,14 +37,15 @@ def open_dataset( Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. """ with Session() as lib: - with lib.virtualfile_out(kind="grid") as voutfile: + with lib.virtualfile_out(kind=kind) as voutfile: + kwdict = {"T": {"grid": "g", "image": "i"}[kind]} lib.call_module( module="read", - args=[filename_or_obj, voutfile, *build_arg_list({"T": "g"})], + args=[filename_or_obj, voutfile, *build_arg_list(kwdict)], ) raster: xr.DataArray = lib.virtualfile_to_raster( - vfname=voutfile, kind="grid" + vfname=voutfile, kind=kind ) # Add "source" encoding source = which(fname=filename_or_obj) @@ -59,4 +63,4 @@ def guess_can_open(self, filename_or_obj) -> bool: ext = Path(filename_or_obj).suffix except TypeError: return False - return ext in {".nc"} + return ext in {".nc", ".tif"} From 60c67265f1b18ee903f2592d1eddcd97ee04343f Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:11:37 +1200 Subject: [PATCH 05/41] [skip ci] Satisfy type checking --- pygmt/io.py | 1 - pygmt/xarray_backend.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pygmt/io.py b/pygmt/io.py index 1367c1afcdf..5712e6a7a62 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -7,7 +7,6 @@ import xarray as xr - def load_dataarray( filename_or_obj, engine="gmtread", kind: Literal["grid", "image"] = "grid", **kwargs ): diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index a8835b2b19a..68956882451 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -2,6 +2,7 @@ An xarray backend for reading grid/image files using the 'gmtread' engine. """ +import os from pathlib import Path from typing import Literal @@ -10,6 +11,8 @@ from pygmt.helpers import build_arg_list from pygmt.src.which import which from xarray.backends import BackendEntrypoint +from xarray.backends.common import AbstractDataStore +from xarray.core.types import ReadBuffer class GMTReadBackendEntrypoint(BackendEntrypoint): @@ -26,7 +29,7 @@ class GMTReadBackendEntrypoint(BackendEntrypoint): def open_dataset( self, - filename_or_obj: str, + filename_or_obj: str | os.PathLike | ReadBuffer | AbstractDataStore, *, drop_variables=None, # noqa: ARG002 kind: Literal["grid", "image"] = "grid", From bf09a394739d8377c220890275c7582b82f8a5b7 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:29:43 +1200 Subject: [PATCH 06/41] Make 'kind' a required kwarg with no default value Users will need to set the 'kind' parameter explicitly to 'grid' or 'image'. --- pygmt/io.py | 2 +- pygmt/tests/test_xarray_backend.py | 25 ++++++++++++++++++++++++- pygmt/xarray_backend.py | 11 ++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/pygmt/io.py b/pygmt/io.py index 5712e6a7a62..9fc563af721 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -8,7 +8,7 @@ def load_dataarray( - filename_or_obj, engine="gmtread", kind: Literal["grid", "image"] = "grid", **kwargs + filename_or_obj, *, engine="gmtread", kind: Literal["grid", "image"], **kwargs ): """ Open, load into memory, and close a DataArray from a file or file-like object diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 4e59a493922..2f08e509d51 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -2,8 +2,12 @@ Tests for xarray 'gmtread' backend engine. """ +import re + +import pytest import xarray as xr from pygmt.enums import GridRegistration, GridType +from pygmt.exceptions import GMTInvalidInput def test_xarray_backend_gmtread_grid(): @@ -12,7 +16,7 @@ def test_xarray_backend_gmtread_grid(): NetCDF grids. """ with xr.open_dataarray( - filename_or_obj="@static_earth_relief.nc", engine="gmtread" + filename_or_obj="@static_earth_relief.nc", engine="gmtread", kind="grid" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" @@ -32,3 +36,22 @@ def test_xarray_backend_gmtread_image(): assert da.dtype == "uint8" assert da.gmt.registration == GridRegistration.PIXEL assert da.gmt.gtype == GridType.GEOGRAPHIC + + +def test_xarray_backend_gmtread_invalid_kind(): + """ + Check that xarray.open_dataarray(..., engine="gmtread") fails with missing or + incorrect 'kind'. + """ + with pytest.raises( + TypeError, + match=re.escape( + "GMTReadBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'kind'" + ), + ): + xr.open_dataarray("nokind.nc", engine="gmtread") + + with pytest.raises(GMTInvalidInput): + xr.open_dataarray( + filename_or_obj="invalid.tif", engine="gmtread", kind="invalid" + ) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 68956882451..4f23bfe7793 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -1,5 +1,5 @@ """ -An xarray backend for reading grid/image files using the 'gmtread' engine. +An xarray backend for reading raster grid/image files using the 'gmtread' engine. """ import os @@ -8,6 +8,7 @@ import xarray as xr from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import build_arg_list from pygmt.src.which import which from xarray.backends import BackendEntrypoint @@ -17,7 +18,7 @@ class GMTReadBackendEntrypoint(BackendEntrypoint): """ - Xarray backend to read grid/image files using 'gmtread' engine. + Xarray backend to read raster grid/image files using 'gmtread' engine. Relies on the libgdal-netcdf driver used by GMT C for NetCDF files, and libgdal for GeoTIFF files. @@ -32,13 +33,17 @@ def open_dataset( filename_or_obj: str | os.PathLike | ReadBuffer | AbstractDataStore, *, drop_variables=None, # noqa: ARG002 - kind: Literal["grid", "image"] = "grid", + kind: Literal["grid", "image"], # other backend specific keyword arguments # `chunks` and `cache` DO NOT go here, they are handled by xarray ) -> xr.Dataset: """ Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. """ + if kind not in {"grid", "image"}: + msg = f"Invalid raster kind: '{kind}'. Valid values are 'grid' or 'image'." + raise GMTInvalidInput(msg) + with Session() as lib: with lib.virtualfile_out(kind=kind) as voutfile: kwdict = {"T": {"grid": "g", "image": "i"}[kind]} From e622878e55cbcb2b1cef6cff21e0fa18f06e071a Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:35:44 +1200 Subject: [PATCH 07/41] Add .grd to set of supported extensions in guess_can_open Also set `kind='grid'` argument in _load_earth_relief_holes --- pygmt/datasets/samples.py | 2 +- pygmt/xarray_backend.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/datasets/samples.py b/pygmt/datasets/samples.py index 0869f8e6176..b5f982b5a47 100644 --- a/pygmt/datasets/samples.py +++ b/pygmt/datasets/samples.py @@ -204,7 +204,7 @@ def _load_earth_relief_holes() -> xr.DataArray: is in meters. """ fname = which("@earth_relief_20m_holes.grd", download="c") - return load_dataarray(fname, engine="netcdf4") + return load_dataarray(fname, engine="gmtread", kind="grid") class GMTSampleData(NamedTuple): diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 4f23bfe7793..f0fc7247a2c 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -71,4 +71,4 @@ def guess_can_open(self, filename_or_obj) -> bool: ext = Path(filename_or_obj).suffix except TypeError: return False - return ext in {".nc", ".tif"} + return ext in {".grd", ".nc", ".tif"} From b9e3eb4b006b448730217b9a602dbe8de179ef10 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:06:56 +1200 Subject: [PATCH 08/41] Rename 'kind' parameter to 'decode_kind' and mypy fix Need to set a default value of None for 'decode_kind' it seems, from reading https://docs.xarray.dev/en/stable/internals/how-to-add-new-backend.html#open-dataset --- pygmt/tests/test_xarray_backend.py | 15 +++++---------- pygmt/xarray_backend.py | 12 ++++++------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 2f08e509d51..98344ff1326 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -16,7 +16,7 @@ def test_xarray_backend_gmtread_grid(): NetCDF grids. """ with xr.open_dataarray( - filename_or_obj="@static_earth_relief.nc", engine="gmtread", kind="grid" + filename_or_obj="@static_earth_relief.nc", engine="gmtread", decode_kind="grid" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" @@ -30,7 +30,7 @@ def test_xarray_backend_gmtread_image(): GeoTIFF images. """ with xr.open_dataarray( - filename_or_obj="@earth_day_01d", engine="gmtread", kind="image" + filename_or_obj="@earth_day_01d", engine="gmtread", decode_kind="image" ) as da: assert da.sizes == {"band": 3, "y": 180, "x": 360} assert da.dtype == "uint8" @@ -41,17 +41,12 @@ def test_xarray_backend_gmtread_image(): def test_xarray_backend_gmtread_invalid_kind(): """ Check that xarray.open_dataarray(..., engine="gmtread") fails with missing or - incorrect 'kind'. + incorrect 'decode_kind'. """ - with pytest.raises( - TypeError, - match=re.escape( - "GMTReadBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'kind'" - ), - ): + with pytest.raises(GMTInvalidInput): xr.open_dataarray("nokind.nc", engine="gmtread") with pytest.raises(GMTInvalidInput): xr.open_dataarray( - filename_or_obj="invalid.tif", engine="gmtread", kind="invalid" + filename_or_obj="invalid.tif", engine="gmtread", decode_kind="invalid" ) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index f0fc7247a2c..bc46997195f 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -33,27 +33,27 @@ def open_dataset( filename_or_obj: str | os.PathLike | ReadBuffer | AbstractDataStore, *, drop_variables=None, # noqa: ARG002 - kind: Literal["grid", "image"], + decode_kind: Literal["grid", "image"] | None = None, # other backend specific keyword arguments # `chunks` and `cache` DO NOT go here, they are handled by xarray ) -> xr.Dataset: """ Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. """ - if kind not in {"grid", "image"}: - msg = f"Invalid raster kind: '{kind}'. Valid values are 'grid' or 'image'." + if decode_kind not in {"grid", "image"}: + msg = f"Invalid raster kind: '{decode_kind}'. Valid values are 'grid' or 'image'." raise GMTInvalidInput(msg) with Session() as lib: - with lib.virtualfile_out(kind=kind) as voutfile: - kwdict = {"T": {"grid": "g", "image": "i"}[kind]} + with lib.virtualfile_out(kind=decode_kind) as voutfile: + kwdict = {"T": {"grid": "g", "image": "i"}[decode_kind]} lib.call_module( module="read", args=[filename_or_obj, voutfile, *build_arg_list(kwdict)], ) raster: xr.DataArray = lib.virtualfile_to_raster( - vfname=voutfile, kind=kind + vfname=voutfile, kind=decode_kind ) # Add "source" encoding source = which(fname=filename_or_obj) From 3a109ac1ae1faf31fab4682ced7b84f3565e35c3 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:17:07 +1200 Subject: [PATCH 09/41] Rename parameter kind to decode_kind in load_dataarray --- pygmt/io.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pygmt/io.py b/pygmt/io.py index 9fc563af721..f49338ba1f3 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -8,7 +8,11 @@ def load_dataarray( - filename_or_obj, *, engine="gmtread", kind: Literal["grid", "image"], **kwargs + filename_or_obj, + *, + engine="gmtread", + decode_kind: Literal["grid", "image"], + **kwargs, ): """ Open, load into memory, and close a DataArray from a file or file-like object @@ -38,8 +42,8 @@ def load_dataarray( The newly created DataArray. engine : str Engine to use when reading files. Default engine is 'gmtread'. - kind - The kind of data to read. Valid values are ``"grid"``, and ``"image"``. + decode_kind + The kind of data to read. Valid values are ``"grid"`` or ``"image"``. See Also -------- @@ -50,7 +54,7 @@ def load_dataarray( raise TypeError(msg) with xr.open_dataarray( - filename_or_obj, engine=engine, kind=kind, **kwargs + filename_or_obj, engine=engine, decode_kind=decode_kind, **kwargs ) as dataarray: result = dataarray.load() _ = result.gmt # load GMTDataArray accessor information From d764f78585e1978e9caaecaf7eee62d3ad239c15 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:19:26 +1200 Subject: [PATCH 10/41] Specify decode_kind="grid" in load_ functions --- pygmt/datasets/samples.py | 2 +- pygmt/helpers/testing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/datasets/samples.py b/pygmt/datasets/samples.py index b5f982b5a47..c4a44723958 100644 --- a/pygmt/datasets/samples.py +++ b/pygmt/datasets/samples.py @@ -204,7 +204,7 @@ def _load_earth_relief_holes() -> xr.DataArray: is in meters. """ fname = which("@earth_relief_20m_holes.grd", download="c") - return load_dataarray(fname, engine="gmtread", kind="grid") + return load_dataarray(fname, engine="gmtread", decode_kind="grid") class GMTSampleData(NamedTuple): diff --git a/pygmt/helpers/testing.py b/pygmt/helpers/testing.py index 29dfd08df19..cc85402eba1 100644 --- a/pygmt/helpers/testing.py +++ b/pygmt/helpers/testing.py @@ -154,7 +154,7 @@ def load_static_earth_relief(): A grid of Earth relief for internal tests. """ fname = which("@static_earth_relief.nc", download="c") - return load_dataarray(fname) + return load_dataarray(fname, decode_kind="grid") def skip_if_no(package): From 98910f86bedf644f4ad465b1fa67ebae1ccb8fdf Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:21:52 +1200 Subject: [PATCH 11/41] [skip ci] format --- pygmt/tests/test_xarray_backend.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 98344ff1326..b5e3d9b397f 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -2,8 +2,6 @@ Tests for xarray 'gmtread' backend engine. """ -import re - import pytest import xarray as xr from pygmt.enums import GridRegistration, GridType From 126560e44bb4fb49939fe2229aeeeaf26c5eae23 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:28:09 +1200 Subject: [PATCH 12/41] Use type: ignore[override] Because xr.core.types.ReadBuffer is not available until xarray 2024.11.0, xref https://github.com/pydata/xarray/pull/9787 --- pygmt/xarray_backend.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index bc46997195f..fa0740c7eb2 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -12,8 +12,6 @@ from pygmt.helpers import build_arg_list from pygmt.src.which import which from xarray.backends import BackendEntrypoint -from xarray.backends.common import AbstractDataStore -from xarray.core.types import ReadBuffer class GMTReadBackendEntrypoint(BackendEntrypoint): @@ -24,13 +22,13 @@ class GMTReadBackendEntrypoint(BackendEntrypoint): GeoTIFF files. """ - description = "Open .nc and .tif files in Xarray via GMT read." + description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT read." open_dataset_parameters = ("filename_or_obj", "kind") url = "https://github.com/GenericMappingTools/pygmt" - def open_dataset( + def open_dataset( # type: ignore[override] self, - filename_or_obj: str | os.PathLike | ReadBuffer | AbstractDataStore, + filename_or_obj: str | os.PathLike, *, drop_variables=None, # noqa: ARG002 decode_kind: Literal["grid", "image"] | None = None, From 436f0fc99aa07428a160811d4a9789d2d89f03f6 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:33:17 +1200 Subject: [PATCH 13/41] Don't set default decode_kind in open_dataset Partially reverts b9e3eb4b006b448730217b9a602dbe8de179ef10 --- pygmt/tests/test_xarray_backend.py | 9 ++++++++- pygmt/xarray_backend.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index b5e3d9b397f..cde78379b70 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -2,6 +2,8 @@ Tests for xarray 'gmtread' backend engine. """ +import re + import pytest import xarray as xr from pygmt.enums import GridRegistration, GridType @@ -41,7 +43,12 @@ def test_xarray_backend_gmtread_invalid_kind(): Check that xarray.open_dataarray(..., engine="gmtread") fails with missing or incorrect 'decode_kind'. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises( + TypeError, + match=re.escape( + "GMTReadBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'decode_kind'" + ), + ): xr.open_dataarray("nokind.nc", engine="gmtread") with pytest.raises(GMTInvalidInput): diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index fa0740c7eb2..53a6be3ac44 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -31,7 +31,7 @@ def open_dataset( # type: ignore[override] filename_or_obj: str | os.PathLike, *, drop_variables=None, # noqa: ARG002 - decode_kind: Literal["grid", "image"] | None = None, + decode_kind: Literal["grid", "image"], # other backend specific keyword arguments # `chunks` and `cache` DO NOT go here, they are handled by xarray ) -> xr.Dataset: From 328b6abf1b43146b4b5653eb0e274896e0b74be6 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 19:23:39 +1200 Subject: [PATCH 14/41] Rename engine="gmtread" to engine="gmt" --- pygmt/datasets/samples.py | 2 +- pygmt/io.py | 8 ++------ pygmt/tests/test_xarray_backend.py | 26 +++++++++++++------------- pygmt/xarray_backend.py | 6 +++--- pyproject.toml | 2 +- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/pygmt/datasets/samples.py b/pygmt/datasets/samples.py index c4a44723958..2b65f324f64 100644 --- a/pygmt/datasets/samples.py +++ b/pygmt/datasets/samples.py @@ -204,7 +204,7 @@ def _load_earth_relief_holes() -> xr.DataArray: is in meters. """ fname = which("@earth_relief_20m_holes.grd", download="c") - return load_dataarray(fname, engine="gmtread", decode_kind="grid") + return load_dataarray(fname, engine="gmt", decode_kind="grid") class GMTSampleData(NamedTuple): diff --git a/pygmt/io.py b/pygmt/io.py index f49338ba1f3..fe14ab7afdc 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -8,11 +8,7 @@ def load_dataarray( - filename_or_obj, - *, - engine="gmtread", - decode_kind: Literal["grid", "image"], - **kwargs, + filename_or_obj, *, engine="gmt", decode_kind: Literal["grid", "image"], **kwargs ): """ Open, load into memory, and close a DataArray from a file or file-like object @@ -41,7 +37,7 @@ def load_dataarray( datarray : xarray.DataArray The newly created DataArray. engine : str - Engine to use when reading files. Default engine is 'gmtread'. + Engine to use when reading files. Default engine is 'gmt'. decode_kind The kind of data to read. Valid values are ``"grid"`` or ``"image"``. diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index cde78379b70..d129b8d03d5 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -1,5 +1,5 @@ """ -Tests for xarray 'gmtread' backend engine. +Tests for xarray 'gmt' backend engine. """ import re @@ -10,13 +10,13 @@ from pygmt.exceptions import GMTInvalidInput -def test_xarray_backend_gmtread_grid(): +def test_xarray_backend_gmt_read_grid(): """ - Ensure that passing engine='gmtread' to xarray.open_dataarray works for reading + Ensure that passing engine='gmt' to xarray.open_dataarray works for reading NetCDF grids. """ with xr.open_dataarray( - filename_or_obj="@static_earth_relief.nc", engine="gmtread", decode_kind="grid" + filename_or_obj="@static_earth_relief.nc", engine="gmt", decode_kind="grid" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" @@ -24,13 +24,13 @@ def test_xarray_backend_gmtread_grid(): assert da.gmt.gtype == GridType.GEOGRAPHIC -def test_xarray_backend_gmtread_image(): +def test_xarray_backend_gmt_read_image(): """ - Ensure that passing engine='gmtread' to xarray.open_dataarray works for reading + Ensure that passing engine='gmt' to xarray.open_dataarray works for reading GeoTIFF images. """ with xr.open_dataarray( - filename_or_obj="@earth_day_01d", engine="gmtread", decode_kind="image" + filename_or_obj="@earth_day_01d", engine="gmt", decode_kind="image" ) as da: assert da.sizes == {"band": 3, "y": 180, "x": 360} assert da.dtype == "uint8" @@ -38,20 +38,20 @@ def test_xarray_backend_gmtread_image(): assert da.gmt.gtype == GridType.GEOGRAPHIC -def test_xarray_backend_gmtread_invalid_kind(): +def test_xarray_backend_gmt_read_invalid_kind(): """ - Check that xarray.open_dataarray(..., engine="gmtread") fails with missing or - incorrect 'decode_kind'. + Check that xarray.open_dataarray(..., engine="gmt") fails with missing or incorrect + 'decode_kind'. """ with pytest.raises( TypeError, match=re.escape( - "GMTReadBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'decode_kind'" + "GMTBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'decode_kind'" ), ): - xr.open_dataarray("nokind.nc", engine="gmtread") + xr.open_dataarray("nokind.nc", engine="gmt") with pytest.raises(GMTInvalidInput): xr.open_dataarray( - filename_or_obj="invalid.tif", engine="gmtread", decode_kind="invalid" + filename_or_obj="invalid.tif", engine="gmt", decode_kind="invalid" ) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 53a6be3ac44..2cf5fa74075 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -1,5 +1,5 @@ """ -An xarray backend for reading raster grid/image files using the 'gmtread' engine. +An xarray backend for reading raster grid/image files using the 'gmt' engine. """ import os @@ -14,9 +14,9 @@ from xarray.backends import BackendEntrypoint -class GMTReadBackendEntrypoint(BackendEntrypoint): +class GMTBackendEntrypoint(BackendEntrypoint): """ - Xarray backend to read raster grid/image files using 'gmtread' engine. + Xarray backend to read raster grid/image files using 'gmt' engine. Relies on the libgdal-netcdf driver used by GMT C for NetCDF files, and libgdal for GeoTIFF files. diff --git a/pyproject.toml b/pyproject.toml index f07340f75ca..abd52dee5ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ all = [ ] [project.entry-points."xarray.backends"] -gmtread = "pygmt.xarray_backend:GMTReadBackendEntrypoint" +gmt = "pygmt.xarray_backend:GMTBackendEntrypoint" [project.urls] "Homepage" = "https://www.pygmt.org" From 06aa39d84e37096d8e7e9a496b951327ddfcc2db Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 17 Apr 2025 19:38:16 +1200 Subject: [PATCH 15/41] Add GMTBackendEntrypoint to doc/api/index.rst --- doc/api/index.rst | 1 + pygmt/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 25de6d44adf..85cad504f55 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -173,6 +173,7 @@ Input/output .. autosummary:: :toctree: generated + GMTBackendEntrypoint load_dataarray GMT Defaults diff --git a/pygmt/__init__.py b/pygmt/__init__.py index f6d1040851f..5cbd36c7686 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -65,6 +65,7 @@ x2sys_init, xyz2grd, ) +from pygmt.xarray_backend import GMTBackendEntrypoint # Start our global modern mode session _begin() From c79e50a70d0e6855736ecbca81f28ceaf559c910 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:10:21 +1200 Subject: [PATCH 16/41] Apply suggestions from code review Co-authored-by: Dongdong Tian --- pygmt/xarray_backend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 2cf5fa74075..1348dfab1e8 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -22,8 +22,8 @@ class GMTBackendEntrypoint(BackendEntrypoint): GeoTIFF files. """ - description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT read." - open_dataset_parameters = ("filename_or_obj", "kind") + description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT." + open_dataset_parameters = ("filename_or_obj", "decode_kind") url = "https://github.com/GenericMappingTools/pygmt" def open_dataset( # type: ignore[override] @@ -69,4 +69,4 @@ def guess_can_open(self, filename_or_obj) -> bool: ext = Path(filename_or_obj).suffix except TypeError: return False - return ext in {".grd", ".nc", ".tif"} + return ext in {".grd", ".nc", ".tif", ".tiff"} From c0ccd4b0cdbf1708ad7b6adca0750b0907a7005f Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 20 Apr 2025 08:26:28 +1200 Subject: [PATCH 17/41] Add test for xr.load_dataarray with a GRD grid --- pygmt/tests/test_xarray_backend.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index d129b8d03d5..cbcea1bdfdb 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -4,15 +4,18 @@ import re +import numpy as np import pytest import xarray as xr from pygmt.enums import GridRegistration, GridType from pygmt.exceptions import GMTInvalidInput -def test_xarray_backend_gmt_read_grid(): + + +def test_xarray_backend_gmt_open_nc_grid(): """ - Ensure that passing engine='gmt' to xarray.open_dataarray works for reading + Ensure that passing engine='gmt' to xarray.open_dataarray works for opening NetCDF grids. """ with xr.open_dataarray( @@ -24,9 +27,9 @@ def test_xarray_backend_gmt_read_grid(): assert da.gmt.gtype == GridType.GEOGRAPHIC -def test_xarray_backend_gmt_read_image(): +def test_xarray_backend_gmt_open_tif_image(): """ - Ensure that passing engine='gmt' to xarray.open_dataarray works for reading + Ensure that passing engine='gmt' to xarray.open_dataarray works for opening GeoTIFF images. """ with xr.open_dataarray( @@ -38,6 +41,24 @@ def test_xarray_backend_gmt_read_image(): assert da.gmt.gtype == GridType.GEOGRAPHIC +def test_xarray_backend_gmt_load_grd_grid(): + """ + Ensure that passing engine='gmt' to xarray.open_dataarray works for loading + GRD grids. + """ + with xr.load_dataarray( + filename_or_obj="@earth_relief_20m_holes.grd", engine="gmt", decode_kind="grid" + ) as da: + assert isinstance( + da.data, + np.ndarray, # ensure data is in memory and not a dask array + ) + assert da.sizes == {"lat": 31, "lon": 31} + assert da.dtype == "float32" + assert da.gmt.registration == GridRegistration.GRIDLINE + assert da.gmt.gtype == GridType.GEOGRAPHIC + + def test_xarray_backend_gmt_read_invalid_kind(): """ Check that xarray.open_dataarray(..., engine="gmt") fails with missing or incorrect From c5ebf9f27b8cecbbdb998ffe833df838f3cdd200 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 20 Apr 2025 08:29:16 +1200 Subject: [PATCH 18/41] Update GMTBackendEntrypoint description to say NetCDF C library is used --- pygmt/xarray_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 1348dfab1e8..732054b389c 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -18,8 +18,8 @@ class GMTBackendEntrypoint(BackendEntrypoint): """ Xarray backend to read raster grid/image files using 'gmt' engine. - Relies on the libgdal-netcdf driver used by GMT C for NetCDF files, and libgdal for - GeoTIFF files. + Internally, GMT uses the NetCDF C library to read NetCDF files, and GDAL for GeoTIFF + files. """ description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT." From 17fd9d7c4561ee3af79753d3ffda18e2aa04b70a Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 20 Apr 2025 08:36:20 +1200 Subject: [PATCH 19/41] lint --- pygmt/tests/test_xarray_backend.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index cbcea1bdfdb..90128b09b9e 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -11,8 +11,6 @@ from pygmt.exceptions import GMTInvalidInput - - def test_xarray_backend_gmt_open_nc_grid(): """ Ensure that passing engine='gmt' to xarray.open_dataarray works for opening From 1c8af3a9e555bc3437e851e359de64c2032689d3 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 20 Apr 2025 08:39:19 +1200 Subject: [PATCH 20/41] Move GMTBackendEntrypoint to a new "Xarray Integration" API section --- doc/api/index.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 85cad504f55..8568483c1c5 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -173,7 +173,6 @@ Input/output .. autosummary:: :toctree: generated - GMTBackendEntrypoint load_dataarray GMT Defaults @@ -198,6 +197,14 @@ Getting metadata from tabular or grid data: info grdinfo +Xarray integration +------------------ + +.. autosummary:: + :toctree: generated + + GMTBackendEntrypoint + Enums ----- From 1d9f21a53752601ac97ae70a1deff06282ef551f Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 20 Apr 2025 09:08:15 +1200 Subject: [PATCH 21/41] Revert changes to datasets/samples.py and helpers/testing.py Cherry-picked to c2011d76fb1de8c0612ceab02bbfc07ac7cfe455 in #3922. --- pygmt/datasets/samples.py | 2 +- pygmt/helpers/testing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/datasets/samples.py b/pygmt/datasets/samples.py index 2b65f324f64..0869f8e6176 100644 --- a/pygmt/datasets/samples.py +++ b/pygmt/datasets/samples.py @@ -204,7 +204,7 @@ def _load_earth_relief_holes() -> xr.DataArray: is in meters. """ fname = which("@earth_relief_20m_holes.grd", download="c") - return load_dataarray(fname, engine="gmt", decode_kind="grid") + return load_dataarray(fname, engine="netcdf4") class GMTSampleData(NamedTuple): diff --git a/pygmt/helpers/testing.py b/pygmt/helpers/testing.py index cc85402eba1..29dfd08df19 100644 --- a/pygmt/helpers/testing.py +++ b/pygmt/helpers/testing.py @@ -154,7 +154,7 @@ def load_static_earth_relief(): A grid of Earth relief for internal tests. """ fname = which("@static_earth_relief.nc", download="c") - return load_dataarray(fname, decode_kind="grid") + return load_dataarray(fname) def skip_if_no(package): From ca9132fa0e56f9968608cb0cf550f0135b2250cd Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:06:16 +1200 Subject: [PATCH 22/41] Apply suggestions from code review Co-authored-by: Dongdong Tian --- doc/api/index.rst | 2 +- pygmt/tests/test_xarray_backend.py | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 8568483c1c5..f1d480655b7 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -197,7 +197,7 @@ Getting metadata from tabular or grid data: info grdinfo -Xarray integration +Xarray Integration ------------------ .. autosummary:: diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 90128b09b9e..636350494aa 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -17,7 +17,7 @@ def test_xarray_backend_gmt_open_nc_grid(): NetCDF grids. """ with xr.open_dataarray( - filename_or_obj="@static_earth_relief.nc", engine="gmt", decode_kind="grid" + "@static_earth_relief.nc", engine="gmt", decode_kind="grid" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" @@ -30,9 +30,7 @@ def test_xarray_backend_gmt_open_tif_image(): Ensure that passing engine='gmt' to xarray.open_dataarray works for opening GeoTIFF images. """ - with xr.open_dataarray( - filename_or_obj="@earth_day_01d", engine="gmt", decode_kind="image" - ) as da: + with xr.open_dataarray("@earth_day_01d", engine="gmt", decode_kind="image") as da: assert da.sizes == {"band": 3, "y": 180, "x": 360} assert da.dtype == "uint8" assert da.gmt.registration == GridRegistration.PIXEL @@ -45,12 +43,10 @@ def test_xarray_backend_gmt_load_grd_grid(): GRD grids. """ with xr.load_dataarray( - filename_or_obj="@earth_relief_20m_holes.grd", engine="gmt", decode_kind="grid" + "@earth_relief_20m_holes.grd", engine="gmt", decode_kind="grid" ) as da: - assert isinstance( - da.data, - np.ndarray, # ensure data is in memory and not a dask array - ) + # ensure data is in memory and not a dask array + assert isinstance(da.data, np.ndarray) assert da.sizes == {"lat": 31, "lon": 31} assert da.dtype == "float32" assert da.gmt.registration == GridRegistration.GRIDLINE From 9669bcfe22aa32d7aca05a6121d335631eba7100 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:12:09 +1200 Subject: [PATCH 23/41] lint --- pygmt/tests/test_xarray_backend.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 636350494aa..00206ef60bb 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -13,11 +13,11 @@ def test_xarray_backend_gmt_open_nc_grid(): """ - Ensure that passing engine='gmt' to xarray.open_dataarray works for opening - NetCDF grids. + Ensure that passing engine='gmt' to xarray.open_dataarray works for opening NetCDF + grids. """ with xr.open_dataarray( - "@static_earth_relief.nc", engine="gmt", decode_kind="grid" + "@static_earth_relief.nc", engine="gmt", decode_kind="grid" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" @@ -27,8 +27,8 @@ def test_xarray_backend_gmt_open_nc_grid(): def test_xarray_backend_gmt_open_tif_image(): """ - Ensure that passing engine='gmt' to xarray.open_dataarray works for opening - GeoTIFF images. + Ensure that passing engine='gmt' to xarray.open_dataarray works for opening GeoTIFF + images. """ with xr.open_dataarray("@earth_day_01d", engine="gmt", decode_kind="image") as da: assert da.sizes == {"band": 3, "y": 180, "x": 360} @@ -39,8 +39,8 @@ def test_xarray_backend_gmt_open_tif_image(): def test_xarray_backend_gmt_load_grd_grid(): """ - Ensure that passing engine='gmt' to xarray.open_dataarray works for loading - GRD grids. + Ensure that passing engine='gmt' to xarray.open_dataarray works for loading GRD + grids. """ with xr.load_dataarray( "@earth_relief_20m_holes.grd", engine="gmt", decode_kind="grid" From f722b479903458f4349a3e0afb75d6c6fcae461f Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:14:37 +1200 Subject: [PATCH 24/41] Revert changes in pygmt/io.py --- pygmt/io.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/pygmt/io.py b/pygmt/io.py index fe14ab7afdc..9451de36c8f 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -2,14 +2,10 @@ PyGMT input/output (I/O) utilities. """ -from typing import Literal - import xarray as xr -def load_dataarray( - filename_or_obj, *, engine="gmt", decode_kind: Literal["grid", "image"], **kwargs -): +def load_dataarray(filename_or_obj, **kwargs): """ Open, load into memory, and close a DataArray from a file or file-like object containing a single data variable. @@ -36,10 +32,6 @@ def load_dataarray( ------- datarray : xarray.DataArray The newly created DataArray. - engine : str - Engine to use when reading files. Default engine is 'gmt'. - decode_kind - The kind of data to read. Valid values are ``"grid"`` or ``"image"``. See Also -------- @@ -49,9 +41,7 @@ def load_dataarray( msg = "'cache' has no effect in this context." raise TypeError(msg) - with xr.open_dataarray( - filename_or_obj, engine=engine, decode_kind=decode_kind, **kwargs - ) as dataarray: + with xr.open_dataarray(filename_or_obj, **kwargs) as dataarray: result = dataarray.load() _ = result.gmt # load GMTDataArray accessor information From a952df979166cc5ee2b24c2bd23c3175811321e8 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:42:14 +1200 Subject: [PATCH 25/41] Improve docstring of guess_can_open Based on https://github.com/Unidata/MetPy/blob/v1.6.3/src/metpy/io/gini.py#L403-L406 --- pygmt/xarray_backend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 732054b389c..99dd510882f 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -63,7 +63,10 @@ def open_dataset( # type: ignore[override] def guess_can_open(self, filename_or_obj) -> bool: """ - Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. + Try to guess whether we can read this file. + + This allows files ending in '.grd', '.nc', or '.tif(f)' to be automatically + opened by xarray. """ try: ext = Path(filename_or_obj).suffix From 0c6b8a3f225058dbf401daf365b815307760d9ab Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:42:34 +1200 Subject: [PATCH 26/41] Add example usage to docstring of GMTBackendEntrypoint --- pygmt/xarray_backend.py | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 99dd510882f..97fb3624d46 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -20,6 +20,51 @@ class GMTBackendEntrypoint(BackendEntrypoint): Internally, GMT uses the NetCDF C library to read NetCDF files, and GDAL for GeoTIFF files. + + When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with + ``engine='gmt'``, pass the ``decode_kind`` parameter that can be either: + + - ``grid`` - for reading single-band raster grids + - ``image`` - for reading multi-band raster images + + Examples + -------- + Read a single-band NetCDF file using ``decode_kind="grid"`` + + >>> import pygmt + >>> import xarray as xr + >>> + >>> da_grid: xr.Dataset = xr.open_dataarray( + ... "@static_earth_relief.nc", engine="gmt", decode_kind="grid" + ... ) + >>> da_grid + Size: 448B + [112 values with dtype=float32] + Coordinates: + * lat (lat) float64 112B -23.5 -22.5 -21.5 -20.5 ... -12.5 -11.5 -10.5 + * lon (lon) float64 64B -54.5 -53.5 -52.5 -51.5 -50.5 -49.5 -48.5 -47.5 + Attributes: + Conventions: CF-1.7 + title: Produced by grdcut + history: grdcut @earth_relief_01d_p -R-55/-47/-24/-10 -Gstatic_eart... + description: Reduced by Gaussian Cartesian filtering (111.2 km fullwidt... + actual_range: [190. 981.] + long_name: elevation (m) + + Read a multi-band GeoTIFF file using ``decode_kind="image"`` + + >>> da_image: xr.Dataset = xr.open_dataarray( + ... "@earth_night_01d", engine="gmt", decode_kind="image" + ... ) + >>> da_image + Size: 194kB + [194400 values with dtype=uint8] + Coordinates: + * y (y) float64 1kB 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 + * x (x) float64 3kB -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 + * band (band) uint8 3B 1 2 3 + Attributes: + long_name: z """ description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT." From 3a256a65496595a5c12c9a3264ff557b99fc4b21 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:27:31 +1200 Subject: [PATCH 27/41] Apply suggestions from code review Co-authored-by: Dongdong Tian --- pygmt/tests/test_xarray_backend.py | 19 ++++++++++--------- pygmt/xarray_backend.py | 12 ++++++------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index 00206ef60bb..d419086c911 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -39,18 +39,19 @@ def test_xarray_backend_gmt_open_tif_image(): def test_xarray_backend_gmt_load_grd_grid(): """ - Ensure that passing engine='gmt' to xarray.open_dataarray works for loading GRD + Ensure that passing engine='gmt' to xarray.load_dataarray works for loading GRD grids. """ - with xr.load_dataarray( + da = xr.load_dataarray( "@earth_relief_20m_holes.grd", engine="gmt", decode_kind="grid" - ) as da: - # ensure data is in memory and not a dask array - assert isinstance(da.data, np.ndarray) - assert da.sizes == {"lat": 31, "lon": 31} - assert da.dtype == "float32" - assert da.gmt.registration == GridRegistration.GRIDLINE - assert da.gmt.gtype == GridType.GEOGRAPHIC + ) + # Ensure data is in memory. + assert isinstance(da.data, np.ndarray) + assert da.data.min() == -4929.5 + assert da.sizes == {"lat": 31, "lon": 31} + assert da.dtype == "float32" + assert da.gmt.registration == GridRegistration.GRIDLINE + assert da.gmt.gtype == GridType.GEOGRAPHIC def test_xarray_backend_gmt_read_invalid_kind(): diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 97fb3624d46..651b08fbb77 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -19,13 +19,13 @@ class GMTBackendEntrypoint(BackendEntrypoint): Xarray backend to read raster grid/image files using 'gmt' engine. Internally, GMT uses the NetCDF C library to read NetCDF files, and GDAL for GeoTIFF - files. + and other raster formats. When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with - ``engine='gmt'``, pass the ``decode_kind`` parameter that can be either: + ``engine="gmt"``, pass the ``decode_kind`` parameter that can be either: - - ``grid`` - for reading single-band raster grids - - ``image`` - for reading multi-band raster images + - ``"grid"`` - for reading single-band raster grids + - ``"image"`` - for reading multi-band raster images Examples -------- @@ -34,7 +34,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): >>> import pygmt >>> import xarray as xr >>> - >>> da_grid: xr.Dataset = xr.open_dataarray( + >>> da_grid = xr.open_dataarray( ... "@static_earth_relief.nc", engine="gmt", decode_kind="grid" ... ) >>> da_grid @@ -53,7 +53,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): Read a multi-band GeoTIFF file using ``decode_kind="image"`` - >>> da_image: xr.Dataset = xr.open_dataarray( + >>> da_image = xr.open_dataarray( ... "@earth_night_01d", engine="gmt", decode_kind="image" ... ) >>> da_image From e6ee6faa98ea45f463695047a11c1263cd161825 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:30:54 +1200 Subject: [PATCH 28/41] Change type-hint from str | os.PathLike to pygmt._typing.PathLike --- pygmt/xarray_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 651b08fbb77..b74c5282621 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -2,11 +2,11 @@ An xarray backend for reading raster grid/image files using the 'gmt' engine. """ -import os from pathlib import Path from typing import Literal import xarray as xr +from pygmt._typing import PathLike from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import build_arg_list @@ -73,7 +73,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): def open_dataset( # type: ignore[override] self, - filename_or_obj: str | os.PathLike, + filename_or_obj: PathLike, *, drop_variables=None, # noqa: ARG002 decode_kind: Literal["grid", "image"], From 5b90a8aa95412b1bcefe218ca1933317446aa455 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:35:31 +1200 Subject: [PATCH 29/41] Use numpy.testing.assert_allclose to check min value --- pygmt/tests/test_xarray_backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index d419086c911..aef61480db5 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -5,6 +5,7 @@ import re import numpy as np +import numpy.testing as npt import pytest import xarray as xr from pygmt.enums import GridRegistration, GridType @@ -47,7 +48,7 @@ def test_xarray_backend_gmt_load_grd_grid(): ) # Ensure data is in memory. assert isinstance(da.data, np.ndarray) - assert da.data.min() == -4929.5 + npt.assert_allclose(da.min(), -4929.5) assert da.sizes == {"lat": 31, "lon": 31} assert da.dtype == "float32" assert da.gmt.registration == GridRegistration.GRIDLINE From 31d19992acf9904f8e9eff6883bda9cc0269c4d0 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:43:54 +1200 Subject: [PATCH 30/41] Use doctest: ELLIPSIS to represent byte size of xarray coordinates Make docstring output compatible with older versions of xarray. --- pygmt/xarray_backend.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index b74c5282621..63402ae63b6 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -37,12 +37,12 @@ class GMTBackendEntrypoint(BackendEntrypoint): >>> da_grid = xr.open_dataarray( ... "@static_earth_relief.nc", engine="gmt", decode_kind="grid" ... ) - >>> da_grid - Size: 448B + >>> da_grid # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS + ... [112 values with dtype=float32] Coordinates: - * lat (lat) float64 112B -23.5 -22.5 -21.5 -20.5 ... -12.5 -11.5 -10.5 - * lon (lon) float64 64B -54.5 -53.5 -52.5 -51.5 -50.5 -49.5 -48.5 -47.5 + * lat (lat) float64... -23.5 -22.5 -21.5 -20.5 ... -12.5 -11.5 -10.5 + * lon (lon) float64... -54.5 -53.5 -52.5 -51.5 -50.5 -49.5 -48.5 -47.5 Attributes: Conventions: CF-1.7 title: Produced by grdcut @@ -56,13 +56,13 @@ class GMTBackendEntrypoint(BackendEntrypoint): >>> da_image = xr.open_dataarray( ... "@earth_night_01d", engine="gmt", decode_kind="image" ... ) - >>> da_image - Size: 194kB + >>> da_image # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS + ... [194400 values with dtype=uint8] Coordinates: - * y (y) float64 1kB 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 - * x (x) float64 3kB -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 - * band (band) uint8 3B 1 2 3 + * y (y) float64... 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 + * x (x) float64... -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 + * band (band) uint8... 1 2 3 Attributes: long_name: z """ From 6a4e1a4b83a4a491ec633e36c41158a9e880cbdb Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:55:49 +1200 Subject: [PATCH 31/41] Rename decode_kind to raster_kind --- pygmt/tests/test_xarray_backend.py | 12 ++++++------ pygmt/xarray_backend.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pygmt/tests/test_xarray_backend.py b/pygmt/tests/test_xarray_backend.py index aef61480db5..b04128bce07 100644 --- a/pygmt/tests/test_xarray_backend.py +++ b/pygmt/tests/test_xarray_backend.py @@ -18,7 +18,7 @@ def test_xarray_backend_gmt_open_nc_grid(): grids. """ with xr.open_dataarray( - "@static_earth_relief.nc", engine="gmt", decode_kind="grid" + "@static_earth_relief.nc", engine="gmt", raster_kind="grid" ) as da: assert da.sizes == {"lat": 14, "lon": 8} assert da.dtype == "float32" @@ -31,7 +31,7 @@ def test_xarray_backend_gmt_open_tif_image(): Ensure that passing engine='gmt' to xarray.open_dataarray works for opening GeoTIFF images. """ - with xr.open_dataarray("@earth_day_01d", engine="gmt", decode_kind="image") as da: + with xr.open_dataarray("@earth_day_01d", engine="gmt", raster_kind="image") as da: assert da.sizes == {"band": 3, "y": 180, "x": 360} assert da.dtype == "uint8" assert da.gmt.registration == GridRegistration.PIXEL @@ -44,7 +44,7 @@ def test_xarray_backend_gmt_load_grd_grid(): grids. """ da = xr.load_dataarray( - "@earth_relief_20m_holes.grd", engine="gmt", decode_kind="grid" + "@earth_relief_20m_holes.grd", engine="gmt", raster_kind="grid" ) # Ensure data is in memory. assert isinstance(da.data, np.ndarray) @@ -58,17 +58,17 @@ def test_xarray_backend_gmt_load_grd_grid(): def test_xarray_backend_gmt_read_invalid_kind(): """ Check that xarray.open_dataarray(..., engine="gmt") fails with missing or incorrect - 'decode_kind'. + 'raster_kind'. """ with pytest.raises( TypeError, match=re.escape( - "GMTBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'decode_kind'" + "GMTBackendEntrypoint.open_dataset() missing 1 required keyword-only argument: 'raster_kind'" ), ): xr.open_dataarray("nokind.nc", engine="gmt") with pytest.raises(GMTInvalidInput): xr.open_dataarray( - filename_or_obj="invalid.tif", engine="gmt", decode_kind="invalid" + filename_or_obj="invalid.tif", engine="gmt", raster_kind="invalid" ) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 63402ae63b6..f1c00d7924e 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -22,20 +22,20 @@ class GMTBackendEntrypoint(BackendEntrypoint): and other raster formats. When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with - ``engine="gmt"``, pass the ``decode_kind`` parameter that can be either: + ``engine="gmt"``, pass the ``raster_kind`` parameter that can be either: - ``"grid"`` - for reading single-band raster grids - ``"image"`` - for reading multi-band raster images Examples -------- - Read a single-band NetCDF file using ``decode_kind="grid"`` + Read a single-band NetCDF file using ``raster_kind="grid"`` >>> import pygmt >>> import xarray as xr >>> >>> da_grid = xr.open_dataarray( - ... "@static_earth_relief.nc", engine="gmt", decode_kind="grid" + ... "@static_earth_relief.nc", engine="gmt", raster_kind="grid" ... ) >>> da_grid # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS ... @@ -51,10 +51,10 @@ class GMTBackendEntrypoint(BackendEntrypoint): actual_range: [190. 981.] long_name: elevation (m) - Read a multi-band GeoTIFF file using ``decode_kind="image"`` + Read a multi-band GeoTIFF file using ``raster_kind="image"`` >>> da_image = xr.open_dataarray( - ... "@earth_night_01d", engine="gmt", decode_kind="image" + ... "@earth_night_01d", engine="gmt", raster_kind="image" ... ) >>> da_image # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS ... @@ -68,7 +68,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): """ description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT." - open_dataset_parameters = ("filename_or_obj", "decode_kind") + open_dataset_parameters = ("filename_or_obj", "raster_kind") url = "https://github.com/GenericMappingTools/pygmt" def open_dataset( # type: ignore[override] @@ -76,27 +76,27 @@ def open_dataset( # type: ignore[override] filename_or_obj: PathLike, *, drop_variables=None, # noqa: ARG002 - decode_kind: Literal["grid", "image"], + raster_kind: Literal["grid", "image"], # other backend specific keyword arguments # `chunks` and `cache` DO NOT go here, they are handled by xarray ) -> xr.Dataset: """ Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. """ - if decode_kind not in {"grid", "image"}: - msg = f"Invalid raster kind: '{decode_kind}'. Valid values are 'grid' or 'image'." + if raster_kind not in {"grid", "image"}: + msg = f"Invalid raster kind: '{raster_kind}'. Valid values are 'grid' or 'image'." raise GMTInvalidInput(msg) with Session() as lib: - with lib.virtualfile_out(kind=decode_kind) as voutfile: - kwdict = {"T": {"grid": "g", "image": "i"}[decode_kind]} + with lib.virtualfile_out(kind=raster_kind) as voutfile: + kwdict = {"T": {"grid": "g", "image": "i"}[raster_kind]} lib.call_module( module="read", args=[filename_or_obj, voutfile, *build_arg_list(kwdict)], ) raster: xr.DataArray = lib.virtualfile_to_raster( - vfname=voutfile, kind=decode_kind + vfname=voutfile, kind=raster_kind ) # Add "source" encoding source = which(fname=filename_or_obj) From 91df0eeb7276db6b0c4d22bd25cc3c19e39dba54 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:56:28 +1200 Subject: [PATCH 32/41] Put url to GMTBackendEntrypoint docs Pointing to the dev docs for now. --- pygmt/xarray_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index f1c00d7924e..c9c99906497 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -69,7 +69,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): description = "Open raster (.grd, .nc or .tif) files in Xarray via GMT." open_dataset_parameters = ("filename_or_obj", "raster_kind") - url = "https://github.com/GenericMappingTools/pygmt" + url = "https://pygmt.org/dev/api/generated/pygmt.GMTBackendEntrypoint.html" def open_dataset( # type: ignore[override] self, From c97bd82448225fd8feea57d9aab24e10fb844710 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:00:59 +1200 Subject: [PATCH 33/41] Remove guess_can_open method No auto-detection of engine based on file extension, so users will have to pass `engine="gmt"` explicitly. --- pygmt/xarray_backend.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index c9c99906497..b0a26b3888b 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -105,16 +105,3 @@ def open_dataset( # type: ignore[override] ) _ = raster.gmt # Load GMTDataArray accessor information return raster.to_dataset() - - def guess_can_open(self, filename_or_obj) -> bool: - """ - Try to guess whether we can read this file. - - This allows files ending in '.grd', '.nc', or '.tif(f)' to be automatically - opened by xarray. - """ - try: - ext = Path(filename_or_obj).suffix - except TypeError: - return False - return ext in {".grd", ".nc", ".tif", ".tiff"} From 2e5fb6bf4ea8b64419df2def8976128fa4505618 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:03:29 +1200 Subject: [PATCH 34/41] [skip ci] lint --- pygmt/xarray_backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index b0a26b3888b..b750326a21b 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -2,7 +2,6 @@ An xarray backend for reading raster grid/image files using the 'gmt' engine. """ -from pathlib import Path from typing import Literal import xarray as xr From 19c5252a6a60def48cba45a708e57e25f1aadca7 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:08:06 +1200 Subject: [PATCH 35/41] NetCDF to netCDF Xref https://github.com/GenericMappingTools/pygmt/pull/2620 --- pygmt/xarray_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index b750326a21b..265e3d7f4f8 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -17,7 +17,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): """ Xarray backend to read raster grid/image files using 'gmt' engine. - Internally, GMT uses the NetCDF C library to read NetCDF files, and GDAL for GeoTIFF + Internally, GMT uses the netCDF C library to read netCDF files, and GDAL for GeoTIFF and other raster formats. When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with @@ -28,7 +28,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): Examples -------- - Read a single-band NetCDF file using ``raster_kind="grid"`` + Read a single-band netCDF file using ``raster_kind="grid"`` >>> import pygmt >>> import xarray as xr From 3bb1f44191edf1e43df4ed4a006c21d9ceb81226 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:19:58 +1200 Subject: [PATCH 36/41] Add Parameters section to docs of open_dataset Also cross-referencing to https://docs.generic-mapping-tools.org/6.5/reference/features.html#grid-file-format --- pygmt/xarray_backend.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pygmt/xarray_backend.py b/pygmt/xarray_backend.py index 265e3d7f4f8..2ef16d73227 100644 --- a/pygmt/xarray_backend.py +++ b/pygmt/xarray_backend.py @@ -18,7 +18,8 @@ class GMTBackendEntrypoint(BackendEntrypoint): Xarray backend to read raster grid/image files using 'gmt' engine. Internally, GMT uses the netCDF C library to read netCDF files, and GDAL for GeoTIFF - and other raster formats. + and other raster formats. See also + :gmt-docs:`reference/features.html#grid-file-format`. When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with ``engine="gmt"``, pass the ``raster_kind`` parameter that can be either: @@ -81,6 +82,15 @@ def open_dataset( # type: ignore[override] ) -> xr.Dataset: """ Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. + + Parameters + ---------- + filename_or_obj + File path to a netCDF (.nc), GeoTIFF (.tif) or other grid/image file format + that can be read by GMT via the netCDF or GDAL C libraries. See also + :gmt-docs:`reference/features.html#grid-file-format`. + raster_kind + Whether to read the file as a "grid" (single-band) or "image" (multi-band). """ if raster_kind not in {"grid", "image"}: msg = f"Invalid raster kind: '{raster_kind}'. Valid values are 'grid' or 'image'." From 8984c1c84ff0e6ffa3b3704c4d841e7fb141b93e Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:31:18 +1200 Subject: [PATCH 37/41] Move pygmt/xarray_backend.py to pygmt/xarray/backend.py --- pygmt/__init__.py | 2 +- pygmt/xarray/__init__.py | 5 +++++ pygmt/{xarray_backend.py => xarray/backend.py} | 0 pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 pygmt/xarray/__init__.py rename pygmt/{xarray_backend.py => xarray/backend.py} (100%) diff --git a/pygmt/__init__.py b/pygmt/__init__.py index 5cbd36c7686..d1f81094301 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -65,7 +65,7 @@ x2sys_init, xyz2grd, ) -from pygmt.xarray_backend import GMTBackendEntrypoint +from pygmt.xarray import GMTBackendEntrypoint # Start our global modern mode session _begin() diff --git a/pygmt/xarray/__init__.py b/pygmt/xarray/__init__.py new file mode 100644 index 00000000000..758b58b35dd --- /dev/null +++ b/pygmt/xarray/__init__.py @@ -0,0 +1,5 @@ +""" +PyGMT integration with Xarray accessors and backends. +""" + +from pygmt.xarray.backend import GMTBackendEntrypoint diff --git a/pygmt/xarray_backend.py b/pygmt/xarray/backend.py similarity index 100% rename from pygmt/xarray_backend.py rename to pygmt/xarray/backend.py diff --git a/pyproject.toml b/pyproject.toml index abd52dee5ec..dbacb85b392 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ all = [ ] [project.entry-points."xarray.backends"] -gmt = "pygmt.xarray_backend:GMTBackendEntrypoint" +gmt = "pygmt.xarray:GMTBackendEntrypoint" [project.urls] "Homepage" = "https://www.pygmt.org" From f2446fc9c3fed7a7b051bebad1818e905a291927 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:40:44 +1200 Subject: [PATCH 38/41] Workaround sphinx Attributes ivar using ellipsis --- pygmt/xarray/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/xarray/backend.py b/pygmt/xarray/backend.py index 2ef16d73227..01400425e61 100644 --- a/pygmt/xarray/backend.py +++ b/pygmt/xarray/backend.py @@ -43,7 +43,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): Coordinates: * lat (lat) float64... -23.5 -22.5 -21.5 -20.5 ... -12.5 -11.5 -10.5 * lon (lon) float64... -54.5 -53.5 -52.5 -51.5 -50.5 -49.5 -48.5 -47.5 - Attributes: + Attributes: ... Conventions: CF-1.7 title: Produced by grdcut history: grdcut @earth_relief_01d_p -R-55/-47/-24/-10 -Gstatic_eart... @@ -63,7 +63,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): * y (y) float64... 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 * x (x) float64... -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 * band (band) uint8... 1 2 3 - Attributes: + Attributes: ... long_name: z """ From 52c0b7ec5e962af8d5112db024cc6480e59cdcf9 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:46:43 +1200 Subject: [PATCH 39/41] Remove extra blankspace --- pygmt/xarray/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/xarray/backend.py b/pygmt/xarray/backend.py index 01400425e61..cda61565661 100644 --- a/pygmt/xarray/backend.py +++ b/pygmt/xarray/backend.py @@ -43,7 +43,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): Coordinates: * lat (lat) float64... -23.5 -22.5 -21.5 -20.5 ... -12.5 -11.5 -10.5 * lon (lon) float64... -54.5 -53.5 -52.5 -51.5 -50.5 -49.5 -48.5 -47.5 - Attributes: ... + Attributes:... Conventions: CF-1.7 title: Produced by grdcut history: grdcut @earth_relief_01d_p -R-55/-47/-24/-10 -Gstatic_eart... @@ -63,7 +63,7 @@ class GMTBackendEntrypoint(BackendEntrypoint): * y (y) float64... 89.5 88.5 87.5 86.5 ... -86.5 -87.5 -88.5 -89.5 * x (x) float64... -179.5 -178.5 -177.5 -176.5 ... 177.5 178.5 179.5 * band (band) uint8... 1 2 3 - Attributes: ... + Attributes:... long_name: z """ From 143a375e5f63cffdce1b7a53aa7a66413932717a Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:08:00 +1200 Subject: [PATCH 40/41] Apply suggestions from code review Co-authored-by: Dongdong Tian --- pygmt/xarray/backend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/xarray/backend.py b/pygmt/xarray/backend.py index cda61565661..933ddfe875e 100644 --- a/pygmt/xarray/backend.py +++ b/pygmt/xarray/backend.py @@ -22,10 +22,10 @@ class GMTBackendEntrypoint(BackendEntrypoint): :gmt-docs:`reference/features.html#grid-file-format`. When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with - ``engine="gmt"``, pass the ``raster_kind`` parameter that can be either: + ``engine="gmt"``, the ``raster_kind`` parameter is required and can be either: - - ``"grid"`` - for reading single-band raster grids - - ``"image"`` - for reading multi-band raster images + - ``"grid"``: for reading single-band raster grids + - ``"image"``: for reading multi-band raster images Examples -------- From acab8b2cfbb2260960fccef6201dcc215980cad9 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:28:58 +1200 Subject: [PATCH 41/41] Describe more on why one should choose the gmt engine --- pygmt/xarray/backend.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pygmt/xarray/backend.py b/pygmt/xarray/backend.py index 933ddfe875e..a95e98983db 100644 --- a/pygmt/xarray/backend.py +++ b/pygmt/xarray/backend.py @@ -18,8 +18,11 @@ class GMTBackendEntrypoint(BackendEntrypoint): Xarray backend to read raster grid/image files using 'gmt' engine. Internally, GMT uses the netCDF C library to read netCDF files, and GDAL for GeoTIFF - and other raster formats. See also - :gmt-docs:`reference/features.html#grid-file-format`. + and other raster formats. See :gmt-docs:`reference/features.html#grid-file-format` + for more details about supported formats. This GMT engine can also read + :gmt-docs:`GMT remote datasets ` (file names starting + with an `@`) directly, and pre-loads :class:`pygmt.GMTDataArrayAccessor` properties + (in the '.gmt' accessor) for easy access to GMT-specific metadata and features. When using :py:func:`xarray.open_dataarray` or :py:func:`xarray.load_dataarray` with ``engine="gmt"``, the ``raster_kind`` parameter is required and can be either: