From 8a539b57bcfd26bbf1d6dfd3b95132e8a5df34bb Mon Sep 17 00:00:00 2001 From: Wei Ji Date: Mon, 29 Jun 2020 23:25:19 +1200 Subject: [PATCH 1/6] Initialize a GMTDataArrayAccessor An xarray accessor for GMT specific information! Currently holds the gridline/pixel registration and cartesian/geographic type properties. Created a new 'Metadata' section in the API docs for this, and moved info and grdinfo here. --- doc/api/index.rst | 15 +++++++++++-- pygmt/__init__.py | 2 +- pygmt/modules.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 28683d3b75c..467e88de8cc 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -62,7 +62,6 @@ Operations on tabular data: :toctree: generated blockmedian - info surface Operations on grids: @@ -71,7 +70,6 @@ Operations on grids: :toctree: generated grdcut - grdinfo grdtrack GMT Defaults @@ -84,6 +82,19 @@ Operations on GMT defaults: config +Metadata +-------- + +Getting metadata from tabular or grid data: + +.. autosummary:: + :toctree: generated + + GMTDataArrayAccessor + info + grdinfo + + Miscellaneous ------------- diff --git a/pygmt/__init__.py b/pygmt/__init__.py index 963cbf39d48..56e770366e6 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -18,7 +18,7 @@ from .gridding import surface from .sampling import grdtrack from .mathops import makecpt -from .modules import config, info, grdinfo, which +from .modules import GMTDataArrayAccessor, config, info, grdinfo, which from .gridops import grdcut from . import datasets diff --git a/pygmt/modules.py b/pygmt/modules.py index 89c6a46db88..1f9f8e90fc1 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -1,6 +1,8 @@ """ Non-plot GMT modules. """ +import xarray as xr + from .clib import Session from .helpers import ( build_arg_string, @@ -214,3 +216,55 @@ def __exit__(self, exc_type, exc_value, traceback): ) with Session() as lib: lib.call_module("set", arg_str) + + +@xr.register_dataarray_accessor("gmt") +class GMTDataArrayAccessor: + """ + This is the GMT extension for :class:`xarray.DataArray`. + + You can access various GMT specific metadata as follow: + + >>> from pygmt.datasets import load_earth_relief + >>> # Use the global Earth relief grid with 1 degree spacing + >>> grid = load_earth_relief(resolution="01d") + + >>> # See if grid uses Gridline (0) or Pixel (1) registration + >>> grid.gmt.registration + 0 + >>> # See if grid uses Cartesian (0) or Geographic (1) coordinate system + >>> grid.gmt.gtype + 1 + """ + + def __init__(self, xarray_obj): + self._obj = xarray_obj + self._source = self._obj.encoding["source"] + self._info = grdinfo(self._source) + + @property + def registration(self): + """ + Registration type of the grid, either Gridline (0) or Pixel (1). + """ + if "Gridline node registration used" in self._info: + _registration = 0 + elif "Pixel node registration used" in self._info: + _registration = 1 + else: + _registration = None + return _registration + + @property + def gtype(self): + """ + Coordinate system type of the grid, either Cartesian (0) or Geographic + (1). + """ + if "[Cartesian grid]" in self._info: + _gtype = 0 + elif "[Geographic grid]" in self._info: + _gtype = 1 + else: + _gtype = None + return _gtype From 0ab88cdcff2f019197ed11c3828661ba0b560224 Mon Sep 17 00:00:00 2001 From: Wei Ji Date: Tue, 30 Jun 2020 15:30:31 +1200 Subject: [PATCH 2/6] Add more tests and allow for overriding registration and coordinate type Using a "setter" decorator to set/change class attributes. Wrote some unit tests to ensure code coverage if up to scratch, except that we can't test Pixel Registration properly yet. --- pygmt/modules.py | 59 ++++++++++++++++++++++-------- pygmt/tests/test_accessor.py | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 pygmt/tests/test_accessor.py diff --git a/pygmt/modules.py b/pygmt/modules.py index 1f9f8e90fc1..c38f0c6d8e2 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -1,6 +1,7 @@ """ Non-plot GMT modules. """ +# import logging import xarray as xr from .clib import Session @@ -239,21 +240,40 @@ class GMTDataArrayAccessor: def __init__(self, xarray_obj): self._obj = xarray_obj - self._source = self._obj.encoding["source"] - self._info = grdinfo(self._source) + try: + self._source = self._obj.encoding["source"] + self._info = grdinfo(self._source) + except KeyError: + default_reg_and_gtype = "Gridline node registration used [Cartesian grid]" + # logging.warning( + # msg="Cannot find a NetCDF source of xarray grid. " + # f"Will fallback to using GMT's default setting: {default_reg_and_gtype}" + # ) + self._info = default_reg_and_gtype @property def registration(self): """ Registration type of the grid, either Gridline (0) or Pixel (1). """ - if "Gridline node registration used" in self._info: - _registration = 0 - elif "Pixel node registration used" in self._info: - _registration = 1 + try: + return self._registration + except AttributeError: + if "Gridline node registration used" in self._info: + self._registration = 0 + elif "Pixel node registration used" in self._info: + self._registration = 1 + return self._registration + + @registration.setter + def registration(self, value): + if value in (0, 1): + self._registration = value else: - _registration = None - return _registration + raise GMTInvalidInput( + f"Invalid grid registration value: {value}, should be a boolean of " + "either 0 for Gridline registration or 1 for Pixel registration" + ) @property def gtype(self): @@ -261,10 +281,21 @@ def gtype(self): Coordinate system type of the grid, either Cartesian (0) or Geographic (1). """ - if "[Cartesian grid]" in self._info: - _gtype = 0 - elif "[Geographic grid]" in self._info: - _gtype = 1 + try: + return self._gtype + except AttributeError: + if "[Cartesian grid]" in self._info: + self._gtype = 0 + elif "[Geographic grid]" in self._info: + self._gtype = 1 + return self._gtype + + @gtype.setter + def gtype(self, value): + if value in (0, 1): + self._gtype = value else: - _gtype = None - return _gtype + raise GMTInvalidInput( + f"Invalid coordinate system type: {value}, should be a boolean of " + "either 0 for Cartesian or 1 for Geographic" + ) diff --git a/pygmt/tests/test_accessor.py b/pygmt/tests/test_accessor.py new file mode 100644 index 00000000000..f1c4b4ed401 --- /dev/null +++ b/pygmt/tests/test_accessor.py @@ -0,0 +1,69 @@ +""" +Test the behaviour of the GMTDataArrayAccessor class +""" +import pytest +import xarray as xr + +from .. import which +from ..exceptions import GMTInvalidInput + + +def test_accessor_gridline_geographic(): + """ + Check that a grid returns a registration value of 0 when Gridline + registered, and a gtype value of 1 when using Geographic coordinates. + """ + fname = which(fname="@tut_bathy.nc", download="c") + grid = xr.open_dataarray(fname) + assert grid.gmt.registration == 0 # gridline registration + assert grid.gmt.gtype == 1 # geographic coordinate type + + +def test_registration_pixel_cartesian(): + """ + Check that a grid returns a registration value of 1 when Pixel registered, + and a gtype value of 0 when using Cartesian coordinates. + """ + # Wait for GMT 6.1.0 earth_relief grids that are pixel registered + # fname = which(fname="@earth_relief_01d_p", download="c") + # grid = xr.open_dataarray(fname) + # assert grid.gmt.registration == 1 # pixel registration + # assert grid.gmt.gtype == 1 # geographic coordinate type + + +def test_accessor_set_pixel_registration(): + """ + Check that we can set a grid to be Pixel registered with a registration + value of 1. + """ + grid = xr.DataArray(data=[[0.1, 0.2], [0.3, 0.4]]) + assert grid.gmt.registration == 0 # default to gridline registration + grid.gmt.registration = 1 # set to pixel registration + assert grid.gmt.registration == 1 # ensure changed to pixel registration + + +def test_accessor_set_geographic_cartesian_roundtrip(): + """ + Check that we can set a grid to switch between the default Cartesian + coordinate type using a gtype of 1, set it to Geographic 0, and then back + to Cartesian again 1. + """ + grid = xr.DataArray(data=[[0.1, 0.2], [0.3, 0.4]]) + assert grid.gmt.gtype == 0 # default to cartesian coordinate type + grid.gmt.gtype = 1 # set to geographic type + assert grid.gmt.gtype == 1 # ensure changed to geographic coordinate type + grid.gmt.gtype = 0 # set back to cartesian type + assert grid.gmt.gtype == 0 # ensure changed to cartesian coordinate type + + +def test_accessor_set_non_boolean(): + """ + Check that setting non boolean values on registration and gtype do not work + """ + grid = xr.DataArray(data=[[0.1, 0.2], [0.3, 0.4]]) + + with pytest.raises(GMTInvalidInput): + grid.gmt.registration = "2" + + with pytest.raises(GMTInvalidInput): + grid.gmt.gtype = 2 From e33c5ebb8e3b0120a98d7fa727e284d3036b1c7d Mon Sep 17 00:00:00 2001 From: Wei Ji Date: Sat, 11 Jul 2020 11:31:29 +1200 Subject: [PATCH 3/6] Fix some typos in the GMTDataArrayAccessor docstring --- pygmt/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/modules.py b/pygmt/modules.py index c38f0c6d8e2..9a3e90099cf 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -224,7 +224,7 @@ class GMTDataArrayAccessor: """ This is the GMT extension for :class:`xarray.DataArray`. - You can access various GMT specific metadata as follow: + You can access various GMT specific metadata about your grid as follows: >>> from pygmt.datasets import load_earth_relief >>> # Use the global Earth relief grid with 1 degree spacing @@ -246,7 +246,7 @@ def __init__(self, xarray_obj): except KeyError: default_reg_and_gtype = "Gridline node registration used [Cartesian grid]" # logging.warning( - # msg="Cannot find a NetCDF source of xarray grid. " + # msg="Cannot find a NetCDF source for the xarray grid. " # f"Will fallback to using GMT's default setting: {default_reg_and_gtype}" # ) self._info = default_reg_and_gtype From 896ed590ae5ce5198c1af810e729f4159436f1a8 Mon Sep 17 00:00:00 2001 From: Wei Ji Date: Sat, 11 Jul 2020 11:49:49 +1200 Subject: [PATCH 4/6] Get registration and gtype using `grdinfo -C` Breaks backward compatibility with GMT 6.0, but simplifies the code (at some expense of readability). Co-Authored-By: Dongdong Tian --- pygmt/modules.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/pygmt/modules.py b/pygmt/modules.py index 9a3e90099cf..70d7333cdd1 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -242,27 +242,23 @@ def __init__(self, xarray_obj): self._obj = xarray_obj try: self._source = self._obj.encoding["source"] - self._info = grdinfo(self._source) + self._registration, self._gtype = map( + int, grdinfo(self._source, C="n", o="10,11").split() + ) except KeyError: - default_reg_and_gtype = "Gridline node registration used [Cartesian grid]" + self._registration = 0 # Default to Gridline registration + self._gtype = 0 # Default to Cartesian grid type # logging.warning( - # msg="Cannot find a NetCDF source for the xarray grid. " - # f"Will fallback to using GMT's default setting: {default_reg_and_gtype}" + # msg="Cannot find a NetCDF source for the xarray grid. " + # "Will fallback to using GMT's default setting to assume " + # "'Gridline node registration used [Cartesian grid]'" # ) - self._info = default_reg_and_gtype @property def registration(self): """ Registration type of the grid, either Gridline (0) or Pixel (1). """ - try: - return self._registration - except AttributeError: - if "Gridline node registration used" in self._info: - self._registration = 0 - elif "Pixel node registration used" in self._info: - self._registration = 1 return self._registration @registration.setter @@ -281,13 +277,6 @@ def gtype(self): Coordinate system type of the grid, either Cartesian (0) or Geographic (1). """ - try: - return self._gtype - except AttributeError: - if "[Cartesian grid]" in self._info: - self._gtype = 0 - elif "[Geographic grid]" in self._info: - self._gtype = 1 return self._gtype @gtype.setter From f0ac4440c715d85756439cc5f051bd9c4d3c4071 Mon Sep 17 00:00:00 2001 From: Wei Ji Date: Sun, 12 Jul 2020 10:04:16 +1200 Subject: [PATCH 5/6] Update tests now that GMT 6.1 is out, check pixel/gridline/cartesia/geo --- .github/workflows/ci_tests.yaml | 6 +++--- pygmt/modules.py | 2 +- pygmt/tests/test_accessor.py | 17 ++++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 06e4431175f..b019487edf2 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -67,7 +67,7 @@ jobs: path: | ~/.gmt/cache ~/.gmt/server - key: cache-gmt-${{ runner.os }}-${{ github.ref }}-20200710-2 + key: cache-gmt-${{ runner.os }}-${{ github.ref }}-20200711 restore-keys: cache-gmt-${{ runner.os }}-refs/heads/master- # Workaround for the timeouts of 'gmt which' on Linux and Windows @@ -80,7 +80,7 @@ jobs: for data in earth_relief_01d_p.grd earth_relief_01d_g.grd earth_relief_30m_p.grd earth_relief_30m_g.grd earth_relief_10m_p.grd earth_relief_10m_g.grd; do wget --no-check-certificate https://oceania.generic-mapping-tools.org/server/earth/earth_relief/${data} -P ~/.gmt/server/earth/earth_relief/ done - for data in ridge.txt Table_5_11.txt tut_bathy.nc tut_quakes.ngdc tut_ship.xyz usgs_quakes_22.txt; do + for data in ridge.txt Table_5_11.txt test.dat.nc tut_bathy.nc tut_quakes.ngdc tut_ship.xyz usgs_quakes_22.txt; do wget --no-check-certificate https://oceania.generic-mapping-tools.org/cache/${data} -P ~/.gmt/cache/ done if: steps.cache.outputs.cache-hit != 'true' && runner.os != 'macOS' @@ -90,7 +90,7 @@ jobs: shell: bash -l {0} run: | gmt which -Ga @earth_relief_10m_p @earth_relief_10m_g @earth_relief_30m_p @earth_relief_30m_g @earth_relief_01d_p @earth_relief_01d_g - gmt which -Ga @ridge.txt @Table_5_11.txt @tut_bathy.nc @tut_quakes.ngdc @tut_ship.xyz @usgs_quakes_22.txt + gmt which -Ga @ridge.txt @Table_5_11.txt @test.dat.nc @tut_bathy.nc @tut_quakes.ngdc @tut_ship.xyz @usgs_quakes_22.txt if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'macOS' # Install the package that we want to test diff --git a/pygmt/modules.py b/pygmt/modules.py index 70d7333cdd1..c4b4c42938b 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -232,7 +232,7 @@ class GMTDataArrayAccessor: >>> # See if grid uses Gridline (0) or Pixel (1) registration >>> grid.gmt.registration - 0 + 1 >>> # See if grid uses Cartesian (0) or Geographic (1) coordinate system >>> grid.gmt.gtype 1 diff --git a/pygmt/tests/test_accessor.py b/pygmt/tests/test_accessor.py index f1c4b4ed401..0eb88b7fecd 100644 --- a/pygmt/tests/test_accessor.py +++ b/pygmt/tests/test_accessor.py @@ -8,27 +8,26 @@ from ..exceptions import GMTInvalidInput -def test_accessor_gridline_geographic(): +def test_accessor_gridline_cartesian(): """ Check that a grid returns a registration value of 0 when Gridline registered, and a gtype value of 1 when using Geographic coordinates. """ - fname = which(fname="@tut_bathy.nc", download="c") + fname = which(fname="@test.dat.nc", download="a") grid = xr.open_dataarray(fname) assert grid.gmt.registration == 0 # gridline registration - assert grid.gmt.gtype == 1 # geographic coordinate type + assert grid.gmt.gtype == 0 # cartesian coordinate type -def test_registration_pixel_cartesian(): +def test_accessor_pixel_geographic(): """ Check that a grid returns a registration value of 1 when Pixel registered, and a gtype value of 0 when using Cartesian coordinates. """ - # Wait for GMT 6.1.0 earth_relief grids that are pixel registered - # fname = which(fname="@earth_relief_01d_p", download="c") - # grid = xr.open_dataarray(fname) - # assert grid.gmt.registration == 1 # pixel registration - # assert grid.gmt.gtype == 1 # geographic coordinate type + fname = which(fname="@earth_relief_01d_p", download="a") + grid = xr.open_dataarray(fname) + assert grid.gmt.registration == 1 # pixel registration + assert grid.gmt.gtype == 1 # geographic coordinate type def test_accessor_set_pixel_registration(): From b567eac44bc017c07ee71db917ff3ae609d1c7b1 Mon Sep 17 00:00:00 2001 From: Wei Ji Date: Sun, 12 Jul 2020 10:58:40 +1200 Subject: [PATCH 6/6] Add inline comments to describe code and remove commented out warning --- pygmt/modules.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pygmt/modules.py b/pygmt/modules.py index c4b4c42938b..7339241002a 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -1,7 +1,6 @@ """ Non-plot GMT modules. """ -# import logging import xarray as xr from .clib import Session @@ -241,18 +240,15 @@ class GMTDataArrayAccessor: def __init__(self, xarray_obj): self._obj = xarray_obj try: - self._source = self._obj.encoding["source"] + self._source = self._obj.encoding["source"] # filepath to NetCDF source + # From the shortened summary information of `grdinfo`, + # get grid registration in column 10, and grid type in column 11 self._registration, self._gtype = map( int, grdinfo(self._source, C="n", o="10,11").split() ) except KeyError: self._registration = 0 # Default to Gridline registration self._gtype = 0 # Default to Cartesian grid type - # logging.warning( - # msg="Cannot find a NetCDF source for the xarray grid. " - # "Will fallback to using GMT's default setting to assume " - # "'Gridline node registration used [Cartesian grid]'" - # ) @property def registration(self):