From 4bb4e3705961680e67d5728c954b594a7aa4b9b5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 22 Mar 2024 19:38:59 +0800 Subject: [PATCH 01/17] GMT_GRID: Add private function _parse_nameunits for parsing long_name and units from x/y/z_units --- pygmt/datatypes/grid.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pygmt/datatypes/grid.py b/pygmt/datatypes/grid.py index dfb3c096d20..ee5e31a83f4 100644 --- a/pygmt/datatypes/grid.py +++ b/pygmt/datatypes/grid.py @@ -101,5 +101,44 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 ] +def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: + """ + Get long_name and units attributes from x_units/y_units/z_units in grid header. + + In GMT grid header, the x_units/y_units/z_units are strings in the form of + ``long_name [units]``, in which both ``long_name`` and ``units`` and standard + netCDF attributes defined by CF conventions. The ``[units]`` part is optional. + + This function parses the x_units/y_units/z_units string and get the ``long_name`` + and ``units`` attributes. + + Parameters + ---------- + nameunits + The x_units/y_units/z_units string in grid header. + + Returns + ------- + (long_name, units) + Tuple of netCDF attributes 'long_name' and 'units'. 'units' may be None. + + Examples + -------- + >>> _parse_nameunits("longitude [degrees_east]") + ('longitude', 'degrees_east') + >>> _parse_nameunits("latitude [degrees_north]") + ('latitude', 'degrees_north') + >>> _parse_nameunits("x") + ('x', None) + >>> _parse_nameunits("y") + ('y', None) + >>> + """ + parts = nameunits.split("[") + long_name = parts[0].strip() + units = parts[1].strip("]").strip() if len(parts) > 1 else None + return long_name, units + + class _GMT_GRID(ctp.Structure): # noqa: N801 pass From 8a514f9f782e622e991317d248b2ba91e3acbb8a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 22 Mar 2024 21:22:22 +0800 Subject: [PATCH 02/17] Typos Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com> --- pygmt/datatypes/grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/datatypes/grid.py b/pygmt/datatypes/grid.py index ee5e31a83f4..d414ae867c6 100644 --- a/pygmt/datatypes/grid.py +++ b/pygmt/datatypes/grid.py @@ -106,10 +106,10 @@ def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: Get long_name and units attributes from x_units/y_units/z_units in grid header. In GMT grid header, the x_units/y_units/z_units are strings in the form of - ``long_name [units]``, in which both ``long_name`` and ``units`` and standard + ``long_name [units]``, in which both ``long_name`` and ``units`` are standard netCDF attributes defined by CF conventions. The ``[units]`` part is optional. - This function parses the x_units/y_units/z_units string and get the ``long_name`` + This function parses the x_units/y_units/z_units string and gets the ``long_name`` and ``units`` attributes. Parameters From 14b3359f6afc1611811f98c0a4319a22119a5205 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 22 Mar 2024 22:24:17 +0800 Subject: [PATCH 03/17] Add the _parse_heade function --- pygmt/datatypes/grid.py | 86 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/pygmt/datatypes/grid.py b/pygmt/datatypes/grid.py index d414ae867c6..961d29484ad 100644 --- a/pygmt/datatypes/grid.py +++ b/pygmt/datatypes/grid.py @@ -5,6 +5,8 @@ import ctypes as ctp from typing import ClassVar +import numpy as np + # Constants for lengths of grid header variables. # # Note: Ideally we should be able to get these constants from the GMT shared library @@ -140,5 +142,89 @@ def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: return long_name, units +def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: + """ + Get dimension names, attributes, grid registration and type from the grid header. + + For a 2-D grid, the dimension names are set to "x", "y", and "z" by default. The + attributes for each dimension are parsed from the grid header following GMT source + codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and + "gmtnc_grd_info" for reference. + + The last dimension is special and is the data variable name, and the attributes + for this dimension are global attributes for the grid. + + The grid is assumed to be Cartesian by default. If the x and y units are + "degrees_east" and "degrees_north", respectively, then the grid is assumed to be + geographic. + + Parameters + ---------- + header + The grid header structure. + + Returns + ------- + dims : tuple + The dimension names with the last dimension for the data variable. + attrs : dict + The attributes for each dimension. + registration : int + The grid registration. 0 for gridline and 1 for pixel. + gtype : int + The grid type. 0 for Cartesian grid and 1 for geographic grid. + """ + # Default dimension names. The last dimension is for the data variable. + dims: tuple = ("x", "y", "z") + nameunits = (header.x_units, header.y_units, header.z_units) + + # Dictionary for dimension attributes with the dimension name as the key. + attrs: dict = {dim: {} for dim in dims} + # Dictionary for mapping the default dimension names to the actual names. + newdims = {dim: dim for dim in dims} + # Loop over dimensions and get the dimension name and attributes from header + for dim, nameunit in zip(dims, nameunits, strict=False): + # The long_name and units attributes. + long_name, units = _parse_nameunits(nameunit.decode()) + if long_name: + attrs[dim]["long_name"] = long_name + if units: + attrs[dim]["units"] = units + + # "degrees_east"/"degrees_north" are the units for geographic coordinates + # following CF-conventions + if units == "degrees_east": + attrs[dim]["standard_name"] = "longitude" + newdims[dim] = "lon" + elif units == "degrees_north": + attrs[dim]["standard_name"] = "latitude" + newdims[dim] = "lat" + + # Axis attributes are "X"/"Y"/"Z"/"T" for horizontal/vertical/time axis. + # The codes here may not work for 3-D grids. + if dim == dims[-1]: # The last dimension is the data. + attrs[dim]["actual_range"] = np.array([header.z_min, header.z_max]) + else: + attrs[dim]["axis"] = dim.upper() + idx = dims.index(dim) * 2 + attrs[dim]["actual_range"] = np.array(header.wesn[idx : idx + 2]) + + # Cartesian or Geographic grid + gtype = 0 + if ( + attrs[dims[0]].get("standard_name") == "longitude" + and attrs[dims[1]].get("standard_name") == "latitude" + ): + gtype = 1 + # Registration + registration = header.registration + + # Update the attributes dictionary with new dimension names as keys + attrs = {newdims[dim]: attrs[dim] for dim in dims} + # Update the dimension names + dims = tuple(newdims[dim] for dim in dims) + return dims, attrs, registration, gtype + + class _GMT_GRID(ctp.Structure): # noqa: N801 pass From 0acc9eb5cc624a141dcf279905548d68e1381fb8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 23 Mar 2024 06:25:22 +0800 Subject: [PATCH 04/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/datatypes/grid.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pygmt/datatypes/grid.py b/pygmt/datatypes/grid.py index 961d29484ad..4f9b16551ce 100644 --- a/pygmt/datatypes/grid.py +++ b/pygmt/datatypes/grid.py @@ -105,24 +105,24 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: """ - Get long_name and units attributes from x_units/y_units/z_units in grid header. + Get the long_name and units attributes from x_units/y_units/z_units in the grid header. - In GMT grid header, the x_units/y_units/z_units are strings in the form of + In the GMT grid header, the x_units/y_units/z_units are strings in the form of ``long_name [units]``, in which both ``long_name`` and ``units`` are standard netCDF attributes defined by CF conventions. The ``[units]`` part is optional. - This function parses the x_units/y_units/z_units string and gets the ``long_name`` + This function parses the x_units/y_units/z_units strings and gets the ``long_name`` and ``units`` attributes. Parameters ---------- nameunits - The x_units/y_units/z_units string in grid header. + The x_units/y_units/z_units strings in the grid header. Returns ------- (long_name, units) - Tuple of netCDF attributes 'long_name' and 'units'. 'units' may be None. + Tuple of netCDF attributes ``long_name`` and ``units``. ``units`` may be ``None``. Examples -------- From ab001441e490e8d1f035d89bb08a0df4158fdb2f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 25 Mar 2024 07:21:13 +0800 Subject: [PATCH 05/17] Move GMT_GRID_HEADER and parser functions to pygmt/datatypes/header.py --- pygmt/datatypes/grid.py | 223 +------------------------------------ pygmt/datatypes/header.py | 228 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 222 deletions(-) create mode 100644 pygmt/datatypes/header.py diff --git a/pygmt/datatypes/grid.py b/pygmt/datatypes/grid.py index 4f9b16551ce..e67f44ebef5 100644 --- a/pygmt/datatypes/grid.py +++ b/pygmt/datatypes/grid.py @@ -1,229 +1,8 @@ """ -Wrapper for the GMT_GRID data type and the GMT_GRID_HEADER data structure. +Wrapper for the GMT_GRID data type. """ import ctypes as ctp -from typing import ClassVar - -import numpy as np - -# Constants for lengths of grid header variables. -# -# Note: Ideally we should be able to get these constants from the GMT shared library -# using the ``lib["GMT_GRID_UNIT_LEN80"]`` syntax, but it causes cyclic import error. -# So we have to hardcode the values here. -GMT_GRID_UNIT_LEN80 = 80 -GMT_GRID_TITLE_LEN80 = 80 -GMT_GRID_COMMAND_LEN320 = 320 -GMT_GRID_REMARK_LEN160 = 160 - -# GMT uses single-precision for grids by default, but can be built to use -# double-precision. Currently, only single-precision is supported. -gmt_grdfloat = ctp.c_float - - -class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 - """ - GMT grid header structure for metadata about the grid. - - The class is used in the `GMT_GRID`/`GMT_IMAGE`/`GMT_CUBE` data structure. See the - GMT source code gmt_resources.h for the original C structure definitions. - """ - - _fields_: ClassVar = [ - # Number of columns - ("n_columns", ctp.c_uint32), - # Number of rows - ("n_rows", ctp.c_uint32), - # Grid registration, 0 for gridline and 1 for pixel - ("registration", ctp.c_uint32), - # Minimum/maximum x and y coordinates - ("wesn", ctp.c_double * 4), - # Minimum z value - ("z_min", ctp.c_double), - # Maximum z value - ("z_max", ctp.c_double), - # x and y increments - ("inc", ctp.c_double * 2), - # Grid values must be multiplied by this factor - ("z_scale_factor", ctp.c_double), - # After scaling, add this offset - ("z_add_offset", ctp.c_double), - # Units in x-directions, in the form "long_name [units]" - ("x_units", ctp.c_char * GMT_GRID_UNIT_LEN80), - # Units in y-direction, in the form "long_name [units]" - ("y_units", ctp.c_char * GMT_GRID_UNIT_LEN80), - # Grid value units, in the form "long_name [units]" - ("z_units", ctp.c_char * GMT_GRID_UNIT_LEN80), - # Name of data set - ("title", ctp.c_char * GMT_GRID_TITLE_LEN80), - # Name of generating command - ("command", ctp.c_char * GMT_GRID_COMMAND_LEN320), - # Comments for this data set - ("remark", ctp.c_char * GMT_GRID_REMARK_LEN160), - # Below are items used internally by GMT - # Number of data points (n_columns * n_rows) [paddings are excluded] - ("nm", ctp.c_size_t), - # Actual number of items (not bytes) required to hold this grid (mx * my), - # per band (for images) - ("size", ctp.c_size_t), - # Bits per data value (e.g., 32 for ints/floats; 8 for bytes). - # Only used for ERSI ArcInfo ASCII Exchange grids. - ("bits", ctp.c_uint), - # For complex grid. - # 0 for normal - # GMT_GRID_IS_COMPLEX_REAL = real part of complex grid - # GMT_GRID_IS_COMPLEX_IMAG = imag part of complex grid - ("complex_mode", ctp.c_uint), - # Grid format - ("type", ctp.c_uint), - # Number of bands [1]. Used with GMT_IMAGE containers - ("n_bands", ctp.c_uint), - # Actual x-dimension in memory. mx = n_columns + pad[0] + pad[1] - ("mx", ctp.c_uint), - # Actual y-dimension in memory. my = n_rows + pad[2] + pad[3] - ("my", ctp.c_uint), - # Paddings on west, east, south, north sides [2,2,2,2] - ("pad", ctp.c_uint * 4), - # Three or four char codes T|B R|C S|R|S (grd) or B|L|P + A|a (img) - # describing array layout in mem and interleaving - ("mem_layout", ctp.c_char * 4), - # Missing value as stored in grid file - ("nan_value", gmt_grdfloat), - # 0.0 for gridline grids and 0.5 for pixel grids - ("xy_off", ctp.c_double), - # Referencing system string in PROJ.4 format - ("ProjRefPROJ4", ctp.c_char_p), - # Referencing system string in WKT format - ("ProjRefWKT", ctp.c_char_p), - # Referencing system EPSG code - ("ProjRefEPSG", ctp.c_int), - # Lower-level information for GMT use only - ("hidden", ctp.c_void_p), - ] - - -def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: - """ - Get the long_name and units attributes from x_units/y_units/z_units in the grid header. - - In the GMT grid header, the x_units/y_units/z_units are strings in the form of - ``long_name [units]``, in which both ``long_name`` and ``units`` are standard - netCDF attributes defined by CF conventions. The ``[units]`` part is optional. - - This function parses the x_units/y_units/z_units strings and gets the ``long_name`` - and ``units`` attributes. - - Parameters - ---------- - nameunits - The x_units/y_units/z_units strings in the grid header. - - Returns - ------- - (long_name, units) - Tuple of netCDF attributes ``long_name`` and ``units``. ``units`` may be ``None``. - - Examples - -------- - >>> _parse_nameunits("longitude [degrees_east]") - ('longitude', 'degrees_east') - >>> _parse_nameunits("latitude [degrees_north]") - ('latitude', 'degrees_north') - >>> _parse_nameunits("x") - ('x', None) - >>> _parse_nameunits("y") - ('y', None) - >>> - """ - parts = nameunits.split("[") - long_name = parts[0].strip() - units = parts[1].strip("]").strip() if len(parts) > 1 else None - return long_name, units - - -def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: - """ - Get dimension names, attributes, grid registration and type from the grid header. - - For a 2-D grid, the dimension names are set to "x", "y", and "z" by default. The - attributes for each dimension are parsed from the grid header following GMT source - codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and - "gmtnc_grd_info" for reference. - - The last dimension is special and is the data variable name, and the attributes - for this dimension are global attributes for the grid. - - The grid is assumed to be Cartesian by default. If the x and y units are - "degrees_east" and "degrees_north", respectively, then the grid is assumed to be - geographic. - - Parameters - ---------- - header - The grid header structure. - - Returns - ------- - dims : tuple - The dimension names with the last dimension for the data variable. - attrs : dict - The attributes for each dimension. - registration : int - The grid registration. 0 for gridline and 1 for pixel. - gtype : int - The grid type. 0 for Cartesian grid and 1 for geographic grid. - """ - # Default dimension names. The last dimension is for the data variable. - dims: tuple = ("x", "y", "z") - nameunits = (header.x_units, header.y_units, header.z_units) - - # Dictionary for dimension attributes with the dimension name as the key. - attrs: dict = {dim: {} for dim in dims} - # Dictionary for mapping the default dimension names to the actual names. - newdims = {dim: dim for dim in dims} - # Loop over dimensions and get the dimension name and attributes from header - for dim, nameunit in zip(dims, nameunits, strict=False): - # The long_name and units attributes. - long_name, units = _parse_nameunits(nameunit.decode()) - if long_name: - attrs[dim]["long_name"] = long_name - if units: - attrs[dim]["units"] = units - - # "degrees_east"/"degrees_north" are the units for geographic coordinates - # following CF-conventions - if units == "degrees_east": - attrs[dim]["standard_name"] = "longitude" - newdims[dim] = "lon" - elif units == "degrees_north": - attrs[dim]["standard_name"] = "latitude" - newdims[dim] = "lat" - - # Axis attributes are "X"/"Y"/"Z"/"T" for horizontal/vertical/time axis. - # The codes here may not work for 3-D grids. - if dim == dims[-1]: # The last dimension is the data. - attrs[dim]["actual_range"] = np.array([header.z_min, header.z_max]) - else: - attrs[dim]["axis"] = dim.upper() - idx = dims.index(dim) * 2 - attrs[dim]["actual_range"] = np.array(header.wesn[idx : idx + 2]) - - # Cartesian or Geographic grid - gtype = 0 - if ( - attrs[dims[0]].get("standard_name") == "longitude" - and attrs[dims[1]].get("standard_name") == "latitude" - ): - gtype = 1 - # Registration - registration = header.registration - - # Update the attributes dictionary with new dimension names as keys - attrs = {newdims[dim]: attrs[dim] for dim in dims} - # Update the dimension names - dims = tuple(newdims[dim] for dim in dims) - return dims, attrs, registration, gtype class _GMT_GRID(ctp.Structure): # noqa: N801 diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py new file mode 100644 index 00000000000..c8af0477a0b --- /dev/null +++ b/pygmt/datatypes/header.py @@ -0,0 +1,228 @@ +""" +Wrapper for the GMT_GRID_HEADER data structure and related utility functions. +""" + +import ctypes as ctp +from typing import ClassVar + +import numpy as np + +# Constants for lengths of grid header variables. +# +# Note: Ideally we should be able to get these constants from the GMT shared library +# using the ``lib["GMT_GRID_UNIT_LEN80"]`` syntax, but it causes cyclic import error. +# So we have to hardcode the values here. +GMT_GRID_UNIT_LEN80 = 80 +GMT_GRID_TITLE_LEN80 = 80 +GMT_GRID_COMMAND_LEN320 = 320 +GMT_GRID_REMARK_LEN160 = 160 + +# GMT uses single-precision for grids by default, but can be built to use +# double-precision. Currently, only single-precision is supported. +gmt_grdfloat = ctp.c_float + + +class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 + """ + GMT grid header structure for metadata about the grid. + + The class is used in the `GMT_GRID`/`GMT_IMAGE`/`GMT_CUBE` data structure. See the + GMT source code gmt_resources.h for the original C structure definitions. + """ + + _fields_: ClassVar = [ + # Number of columns + ("n_columns", ctp.c_uint32), + # Number of rows + ("n_rows", ctp.c_uint32), + # Grid registration, 0 for gridline and 1 for pixel + ("registration", ctp.c_uint32), + # Minimum/maximum x and y coordinates + ("wesn", ctp.c_double * 4), + # Minimum z value + ("z_min", ctp.c_double), + # Maximum z value + ("z_max", ctp.c_double), + # x and y increments + ("inc", ctp.c_double * 2), + # Grid values must be multiplied by this factor + ("z_scale_factor", ctp.c_double), + # After scaling, add this offset + ("z_add_offset", ctp.c_double), + # Units in x-directions, in the form "long_name [units]" + ("x_units", ctp.c_char * GMT_GRID_UNIT_LEN80), + # Units in y-direction, in the form "long_name [units]" + ("y_units", ctp.c_char * GMT_GRID_UNIT_LEN80), + # Grid value units, in the form "long_name [units]" + ("z_units", ctp.c_char * GMT_GRID_UNIT_LEN80), + # Name of data set + ("title", ctp.c_char * GMT_GRID_TITLE_LEN80), + # Name of generating command + ("command", ctp.c_char * GMT_GRID_COMMAND_LEN320), + # Comments for this data set + ("remark", ctp.c_char * GMT_GRID_REMARK_LEN160), + # Below are items used internally by GMT + # Number of data points (n_columns * n_rows) [paddings are excluded] + ("nm", ctp.c_size_t), + # Actual number of items (not bytes) required to hold this grid (mx * my), + # per band (for images) + ("size", ctp.c_size_t), + # Bits per data value (e.g., 32 for ints/floats; 8 for bytes). + # Only used for ERSI ArcInfo ASCII Exchange grids. + ("bits", ctp.c_uint), + # For complex grid. + # 0 for normal + # GMT_GRID_IS_COMPLEX_REAL = real part of complex grid + # GMT_GRID_IS_COMPLEX_IMAG = imag part of complex grid + ("complex_mode", ctp.c_uint), + # Grid format + ("type", ctp.c_uint), + # Number of bands [1]. Used with GMT_IMAGE containers + ("n_bands", ctp.c_uint), + # Actual x-dimension in memory. mx = n_columns + pad[0] + pad[1] + ("mx", ctp.c_uint), + # Actual y-dimension in memory. my = n_rows + pad[2] + pad[3] + ("my", ctp.c_uint), + # Paddings on west, east, south, north sides [2,2,2,2] + ("pad", ctp.c_uint * 4), + # Three or four char codes T|B R|C S|R|S (grd) or B|L|P + A|a (img) + # describing array layout in mem and interleaving + ("mem_layout", ctp.c_char * 4), + # Missing value as stored in grid file + ("nan_value", gmt_grdfloat), + # 0.0 for gridline grids and 0.5 for pixel grids + ("xy_off", ctp.c_double), + # Referencing system string in PROJ.4 format + ("ProjRefPROJ4", ctp.c_char_p), + # Referencing system string in WKT format + ("ProjRefWKT", ctp.c_char_p), + # Referencing system EPSG code + ("ProjRefEPSG", ctp.c_int), + # Lower-level information for GMT use only + ("hidden", ctp.c_void_p), + ] + + +def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: + """ + Get the long_name and units attributes from x_units/y_units/z_units in the grid + header. + + In the GMT grid header, the x_units/y_units/z_units are strings in the form of + ``long_name [units]``, in which both ``long_name`` and ``units`` are standard + netCDF attributes defined by CF conventions. The ``[units]`` part is optional. + + This function parses the x_units/y_units/z_units strings and gets the ``long_name`` + and ``units`` attributes. + + Parameters + ---------- + nameunits + The x_units/y_units/z_units strings in the grid header. + + Returns + ------- + (long_name, units) + Tuple of netCDF attributes ``long_name`` and ``units``. ``units`` may be + ``None``. + + Examples + -------- + >>> _parse_nameunits("longitude [degrees_east]") + ('longitude', 'degrees_east') + >>> _parse_nameunits("latitude [degrees_north]") + ('latitude', 'degrees_north') + >>> _parse_nameunits("x") + ('x', None) + >>> _parse_nameunits("y") + ('y', None) + >>> + """ + parts = nameunits.split("[") + long_name = parts[0].strip() + units = parts[1].strip("]").strip() if len(parts) > 1 else None + return long_name, units + + +def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: + """ + Get dimension names, attributes, grid registration and type from the grid header. + + For a 2-D grid, the dimension names are set to "x", "y", and "z" by default. The + attributes for each dimension are parsed from the grid header following GMT source + codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and + "gmtnc_grd_info" for reference. + + The last dimension is special and is the data variable name, and the attributes + for this dimension are global attributes for the grid. + + The grid is assumed to be Cartesian by default. If the x and y units are + "degrees_east" and "degrees_north", respectively, then the grid is assumed to be + geographic. + + Parameters + ---------- + header + The grid header structure. + + Returns + ------- + dims : tuple + The dimension names with the last dimension for the data variable. + attrs : dict + The attributes for each dimension. + registration : int + The grid registration. 0 for gridline and 1 for pixel. + gtype : int + The grid type. 0 for Cartesian grid and 1 for geographic grid. + """ + # Default dimension names. The last dimension is for the data variable. + dims: tuple = ("x", "y", "z") + nameunits = (header.x_units, header.y_units, header.z_units) + + # Dictionary for dimension attributes with the dimension name as the key. + attrs: dict = {dim: {} for dim in dims} + # Dictionary for mapping the default dimension names to the actual names. + newdims = {dim: dim for dim in dims} + # Loop over dimensions and get the dimension name and attributes from header + for dim, nameunit in zip(dims, nameunits, strict=False): + # The long_name and units attributes. + long_name, units = _parse_nameunits(nameunit.decode()) + if long_name: + attrs[dim]["long_name"] = long_name + if units: + attrs[dim]["units"] = units + + # "degrees_east"/"degrees_north" are the units for geographic coordinates + # following CF-conventions + if units == "degrees_east": + attrs[dim]["standard_name"] = "longitude" + newdims[dim] = "lon" + elif units == "degrees_north": + attrs[dim]["standard_name"] = "latitude" + newdims[dim] = "lat" + + # Axis attributes are "X"/"Y"/"Z"/"T" for horizontal/vertical/time axis. + # The codes here may not work for 3-D grids. + if dim == dims[-1]: # The last dimension is the data. + attrs[dim]["actual_range"] = np.array([header.z_min, header.z_max]) + else: + attrs[dim]["axis"] = dim.upper() + idx = dims.index(dim) * 2 + attrs[dim]["actual_range"] = np.array(header.wesn[idx : idx + 2]) + + # Cartesian or Geographic grid + gtype = 0 + if ( + attrs[dims[0]].get("standard_name") == "longitude" + and attrs[dims[1]].get("standard_name") == "latitude" + ): + gtype = 1 + # Registration + registration = header.registration + + # Update the attributes dictionary with new dimension names as keys + attrs = {newdims[dim]: attrs[dim] for dim in dims} + # Update the dimension names + dims = tuple(newdims[dim] for dim in dims) + return dims, attrs, registration, gtype From b555527f52203fa8531d4f0b211c9390adac0d5c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 29 Mar 2024 12:11:09 +0800 Subject: [PATCH 06/17] Apply suggestions from code review Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- pygmt/datatypes/header.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index c8af0477a0b..4cb19b053ae 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -148,7 +148,7 @@ def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: """ Get dimension names, attributes, grid registration and type from the grid header. - For a 2-D grid, the dimension names are set to "x", "y", and "z" by default. The + For a 2-D grid, the dimension names are set to "y", "x", and "z" by default. The attributes for each dimension are parsed from the grid header following GMT source codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and "gmtnc_grd_info" for reference. @@ -168,7 +168,7 @@ def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: Returns ------- dims : tuple - The dimension names with the last dimension for the data variable. + The dimension names, with the last dimension being the data variable. attrs : dict The attributes for each dimension. registration : int @@ -177,8 +177,8 @@ def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: The grid type. 0 for Cartesian grid and 1 for geographic grid. """ # Default dimension names. The last dimension is for the data variable. - dims: tuple = ("x", "y", "z") - nameunits = (header.x_units, header.y_units, header.z_units) + dims: tuple = ("y", "x", "z") + nameunits = (header.y_units, header.x_units, header.z_units) # Dictionary for dimension attributes with the dimension name as the key. attrs: dict = {dim: {} for dim in dims} From 83337e758b7b8018af0faafd4209dc2fb83cebd5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 29 Mar 2024 13:43:14 +0800 Subject: [PATCH 07/17] Move _parse_header as class method --- pygmt/datatypes/header.py | 248 +++++++++++++++++++------------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 4cb19b053ae..a945eb0450a 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -22,6 +22,47 @@ gmt_grdfloat = ctp.c_float +def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: + """ + Get the long_name and units attributes from x_units/y_units/z_units in the grid + header. + + In the GMT grid header, the x_units/y_units/z_units are strings in the form of + ``long_name [units]``, in which both ``long_name`` and ``units`` are standard + netCDF attributes defined by CF conventions. The ``[units]`` part is optional. + + This function parses the x_units/y_units/z_units strings and gets the ``long_name`` + and ``units`` attributes. + + Parameters + ---------- + nameunits + The x_units/y_units/z_units strings in the grid header. + + Returns + ------- + (long_name, units) + Tuple of netCDF attributes ``long_name`` and ``units``. ``units`` may be + ``None``. + + Examples + -------- + >>> _parse_nameunits("longitude [degrees_east]") + ('longitude', 'degrees_east') + >>> _parse_nameunits("latitude [degrees_north]") + ('latitude', 'degrees_north') + >>> _parse_nameunits("x") + ('x', None) + >>> _parse_nameunits("y") + ('y', None) + >>> + """ + parts = nameunits.split("[") + long_name = parts[0].strip() + units = parts[1].strip("]").strip() if len(parts) > 1 else None + return long_name, units + + class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 """ GMT grid header structure for metadata about the grid. @@ -102,127 +143,86 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 ("hidden", ctp.c_void_p), ] - -def _parse_nameunits(nameunits: str) -> tuple[str, str | None]: - """ - Get the long_name and units attributes from x_units/y_units/z_units in the grid - header. - - In the GMT grid header, the x_units/y_units/z_units are strings in the form of - ``long_name [units]``, in which both ``long_name`` and ``units`` are standard - netCDF attributes defined by CF conventions. The ``[units]`` part is optional. - - This function parses the x_units/y_units/z_units strings and gets the ``long_name`` - and ``units`` attributes. - - Parameters - ---------- - nameunits - The x_units/y_units/z_units strings in the grid header. - - Returns - ------- - (long_name, units) - Tuple of netCDF attributes ``long_name`` and ``units``. ``units`` may be - ``None``. - - Examples - -------- - >>> _parse_nameunits("longitude [degrees_east]") - ('longitude', 'degrees_east') - >>> _parse_nameunits("latitude [degrees_north]") - ('latitude', 'degrees_north') - >>> _parse_nameunits("x") - ('x', None) - >>> _parse_nameunits("y") - ('y', None) - >>> - """ - parts = nameunits.split("[") - long_name = parts[0].strip() - units = parts[1].strip("]").strip() if len(parts) > 1 else None - return long_name, units - - -def _parse_header(header: _GMT_GRID_HEADER) -> tuple[tuple, dict, int, int]: - """ - Get dimension names, attributes, grid registration and type from the grid header. - - For a 2-D grid, the dimension names are set to "y", "x", and "z" by default. The - attributes for each dimension are parsed from the grid header following GMT source - codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and - "gmtnc_grd_info" for reference. - - The last dimension is special and is the data variable name, and the attributes - for this dimension are global attributes for the grid. - - The grid is assumed to be Cartesian by default. If the x and y units are - "degrees_east" and "degrees_north", respectively, then the grid is assumed to be - geographic. - - Parameters - ---------- - header - The grid header structure. - - Returns - ------- - dims : tuple - The dimension names, with the last dimension being the data variable. - attrs : dict - The attributes for each dimension. - registration : int - The grid registration. 0 for gridline and 1 for pixel. - gtype : int - The grid type. 0 for Cartesian grid and 1 for geographic grid. - """ - # Default dimension names. The last dimension is for the data variable. - dims: tuple = ("y", "x", "z") - nameunits = (header.y_units, header.x_units, header.z_units) - - # Dictionary for dimension attributes with the dimension name as the key. - attrs: dict = {dim: {} for dim in dims} - # Dictionary for mapping the default dimension names to the actual names. - newdims = {dim: dim for dim in dims} - # Loop over dimensions and get the dimension name and attributes from header - for dim, nameunit in zip(dims, nameunits, strict=False): - # The long_name and units attributes. - long_name, units = _parse_nameunits(nameunit.decode()) - if long_name: - attrs[dim]["long_name"] = long_name - if units: - attrs[dim]["units"] = units - - # "degrees_east"/"degrees_north" are the units for geographic coordinates - # following CF-conventions - if units == "degrees_east": - attrs[dim]["standard_name"] = "longitude" - newdims[dim] = "lon" - elif units == "degrees_north": - attrs[dim]["standard_name"] = "latitude" - newdims[dim] = "lat" - - # Axis attributes are "X"/"Y"/"Z"/"T" for horizontal/vertical/time axis. - # The codes here may not work for 3-D grids. - if dim == dims[-1]: # The last dimension is the data. - attrs[dim]["actual_range"] = np.array([header.z_min, header.z_max]) - else: - attrs[dim]["axis"] = dim.upper() - idx = dims.index(dim) * 2 - attrs[dim]["actual_range"] = np.array(header.wesn[idx : idx + 2]) - - # Cartesian or Geographic grid - gtype = 0 - if ( - attrs[dims[0]].get("standard_name") == "longitude" - and attrs[dims[1]].get("standard_name") == "latitude" - ): - gtype = 1 - # Registration - registration = header.registration - - # Update the attributes dictionary with new dimension names as keys - attrs = {newdims[dim]: attrs[dim] for dim in dims} - # Update the dimension names - dims = tuple(newdims[dim] for dim in dims) - return dims, attrs, registration, gtype + def _parse_header(self) -> tuple[tuple, dict, int, int]: + """ + Get dimension names, attributes, grid registration and type from the grid + header. + + For a 2-D grid, the dimension names are set to "y", "x", and "z" by default. The + attributes for each dimension are parsed from the grid header following GMT + source codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and + "gmtnc_grd_info" for reference. + + The last dimension is special and is the data variable name, and the attributes + for this dimension are global attributes for the grid. + + The grid is assumed to be Cartesian by default. If the x and y units are + "degrees_east" and "degrees_north", respectively, then the grid is assumed to be + geographic. + + Parameters + ---------- + header + The grid header structure. + + Returns + ------- + dims : tuple + The dimension names, with the last dimension being the data variable. + attrs : dict + The attributes for each dimension. + registration : int + The grid registration. 0 for gridline and 1 for pixel. + gtype : int + The grid type. 0 for Cartesian grid and 1 for geographic grid. + """ + # Default dimension names. The last dimension is for the data variable. + dims: tuple = ("y", "x", "z") + nameunits = (self.y_units, self.x_units, self.z_units) + + # Dictionary for dimension attributes with the dimension name as the key. + attrs: dict = {dim: {} for dim in dims} + # Dictionary for mapping the default dimension names to the actual names. + newdims = {dim: dim for dim in dims} + # Loop over dimensions and get the dimension name and attributes from header + for dim, nameunit in zip(dims, nameunits, strict=False): + # The long_name and units attributes. + long_name, units = _parse_nameunits(nameunit.decode()) + if long_name: + attrs[dim]["long_name"] = long_name + if units: + attrs[dim]["units"] = units + + # "degrees_east"/"degrees_north" are the units for geographic coordinates + # following CF-conventions + if units == "degrees_east": + attrs[dim]["standard_name"] = "longitude" + newdims[dim] = "lon" + elif units == "degrees_north": + attrs[dim]["standard_name"] = "latitude" + newdims[dim] = "lat" + + # Axis attributes are "X"/"Y"/"Z"/"T" for horizontal/vertical/time axis. + # The codes here may not work for 3-D grids. + if dim == dims[-1]: # The last dimension is the data. + attrs[dim]["actual_range"] = np.array([self.z_min, self.z_max]) + else: + attrs[dim]["axis"] = dim.upper() + idx = dims.index(dim) * 2 + attrs[dim]["actual_range"] = np.array(self.wesn[idx : idx + 2]) + + # Cartesian or Geographic grid + gtype = 0 + if ( + attrs[dims[0]].get("standard_name") == "longitude" + and attrs[dims[1]].get("standard_name") == "latitude" + ): + gtype = 1 + # Registration + registration = self.registration + + # Update the attributes dictionary with new dimension names as keys + attrs = {newdims[dim]: attrs[dim] for dim in dims} + # Update the dimension names + dims = tuple(newdims[dim] for dim in dims) + return dims, attrs, registration, gtype From c999b44ddb68ba5c92815a49dee829046fdb3c13 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 31 Mar 2024 17:54:26 +0800 Subject: [PATCH 08/17] Patches b555527f52203fa8531d4f0b211c9390adac0d5c --- pygmt/datatypes/header.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index a945eb0450a..0d09aeaca06 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -208,14 +208,14 @@ def _parse_header(self) -> tuple[tuple, dict, int, int]: attrs[dim]["actual_range"] = np.array([self.z_min, self.z_max]) else: attrs[dim]["axis"] = dim.upper() - idx = dims.index(dim) * 2 + idx = 2 if dim == "y" else 0 attrs[dim]["actual_range"] = np.array(self.wesn[idx : idx + 2]) # Cartesian or Geographic grid gtype = 0 if ( - attrs[dims[0]].get("standard_name") == "longitude" - and attrs[dims[1]].get("standard_name") == "latitude" + attrs[dims[1]].get("standard_name") == "longitude" + and attrs[dims[0]].get("standard_name") == "latitude" ): gtype = 1 # Registration From 2bd353abae7e2cda601a5cf386ae18e57694b063 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 31 Mar 2024 20:09:45 +0800 Subject: [PATCH 09/17] Add getter methods to hide techinical details for wrapping GMT_GRID/GMT_IMAGE --- pygmt/datatypes/header.py | 150 +++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 50 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 0d09aeaca06..5f2b6c0ff9f 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -143,42 +143,23 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 ("hidden", ctp.c_void_p), ] - def _parse_header(self) -> tuple[tuple, dict, int, int]: + def _parse_dimensions(self) -> dict[str, list]: """ - Get dimension names, attributes, grid registration and type from the grid - header. + Get dimension names and attributes from the grid header. - For a 2-D grid, the dimension names are set to "y", "x", and "z" by default. The + For a 2-D grid, the dimension names are set to "y" and "x" by default. The attributes for each dimension are parsed from the grid header following GMT source codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and "gmtnc_grd_info" for reference. - The last dimension is special and is the data variable name, and the attributes - for this dimension are global attributes for the grid. - - The grid is assumed to be Cartesian by default. If the x and y units are - "degrees_east" and "degrees_north", respectively, then the grid is assumed to be - geographic. - - Parameters - ---------- - header - The grid header structure. - Returns ------- - dims : tuple - The dimension names, with the last dimension being the data variable. - attrs : dict - The attributes for each dimension. - registration : int - The grid registration. 0 for gridline and 1 for pixel. - gtype : int - The grid type. 0 for Cartesian grid and 1 for geographic grid. + dict + Dictionary containing the list of dimension names and attributes. """ - # Default dimension names. The last dimension is for the data variable. - dims: tuple = ("y", "x", "z") - nameunits = (self.y_units, self.x_units, self.z_units) + # Default dimension names. + dims: tuple = ("y", "x") + nameunits = (self.y_units, self.x_units) # Dictionary for dimension attributes with the dimension name as the key. attrs: dict = {dim: {} for dim in dims} @@ -203,26 +184,95 @@ def _parse_header(self) -> tuple[tuple, dict, int, int]: newdims[dim] = "lat" # Axis attributes are "X"/"Y"/"Z"/"T" for horizontal/vertical/time axis. - # The codes here may not work for 3-D grids. - if dim == dims[-1]: # The last dimension is the data. - attrs[dim]["actual_range"] = np.array([self.z_min, self.z_max]) - else: - attrs[dim]["axis"] = dim.upper() - idx = 2 if dim == "y" else 0 - attrs[dim]["actual_range"] = np.array(self.wesn[idx : idx + 2]) - - # Cartesian or Geographic grid - gtype = 0 - if ( - attrs[dims[1]].get("standard_name") == "longitude" - and attrs[dims[0]].get("standard_name") == "latitude" - ): - gtype = 1 - # Registration - registration = self.registration - - # Update the attributes dictionary with new dimension names as keys - attrs = {newdims[dim]: attrs[dim] for dim in dims} - # Update the dimension names - dims = tuple(newdims[dim] for dim in dims) - return dims, attrs, registration, gtype + attrs[dim]["axis"] = dim.upper() + idx = 2 if dim == "y" else 0 + attrs[dim]["actual_range"] = np.array(self.wesn[idx : idx + 2]) + + # Save the lists of dimension names and attributes in the _nc attribute. + self._nc = { + "dims": [newdims[dim] for dim in dims], + "attrs": [attrs[dim] for dim in dims], + } + + def get_name(self) -> str: + """ + Get the name of the grid from the grid header. + + Returns + ------- + name + The name of the grid. + """ + return "z" + + def get_data_attrs(self) -> dict: + """ + Get the attributes for the data variable from the grid header. + + Returns + ------- + attrs + The attributes for the data variable. + """ + attrs = {} + long_name, units = _parse_nameunits(self.z_units.decode()) + if long_name: + attrs["long_name"] = long_name + if units: + attrs["units"] = units + attrs["actual_range"] = np.array([self.z_min, self.z_max]) + return attrs + + def get_dims(self): + """ + Get the dimension names from the grid header. + + Returns + ------- + dims : tuple + The dimension names. + """ + if not hasattr(self, "_nc"): + self._parse_dimensions() + return self._nc["dims"] + + def get_dim_attrs(self) -> list: + """ + Get the attributes for each dimension from the grid header. + + Returns + ------- + attrs + List of attributes for each dimension. + """ + if not hasattr(self, "_nc"): + self._parse_dimensions() + return self._nc["attrs"] + + def get_gtype(self) -> int: + """ + Get the grid type from the grid header. + + The grid is assumed to be Cartesian by default. If the x/y dimensions are named + "lon"/"lat" or have units "degrees_east"/"degrees_north", then the grid is + assumed to be geographic. + + Returns + ------- + gtype + The grid type. 0 for Cartesian grid and 1 for geographic grid. + """ + dims = self.get_dims() + gtype = 1 if dims[0] == "lat" and dims[1] == "lon" else 0 + return gtype + + def get_registration(self) -> int: + """ + Get the grid registration from the grid header. + + Returns + ------- + registration + The grid registration. 0 for gridline and 1 for pixel. + """ + return self.registration From dfbc2e5237ff53e6741be2905dcb43a3e3a5877f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 31 Mar 2024 20:24:50 +0800 Subject: [PATCH 10/17] Add more global attributes --- pygmt/datatypes/header.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 5f2b6c0ff9f..06bdfc402d6 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -214,13 +214,16 @@ def get_data_attrs(self) -> dict: attrs The attributes for the data variable. """ - attrs = {} + attrs = {"Conventions": "CF-1.7"} long_name, units = _parse_nameunits(self.z_units.decode()) if long_name: attrs["long_name"] = long_name if units: attrs["units"] = units attrs["actual_range"] = np.array([self.z_min, self.z_max]) + attrs["title"] = self.title + attrs["history"] = self.command + attrs["description"] = self.remark return attrs def get_dims(self): From 8a2a6e41007f08e0a220c23562b65849ef657da0 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 31 Mar 2024 20:31:40 +0800 Subject: [PATCH 11/17] Fix type hints issues --- pygmt/datatypes/header.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 06bdfc402d6..35044b26e51 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -3,7 +3,7 @@ """ import ctypes as ctp -from typing import ClassVar +from typing import ClassVar, Any import numpy as np @@ -143,7 +143,7 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 ("hidden", ctp.c_void_p), ] - def _parse_dimensions(self) -> dict[str, list]: + def _parse_dimensions(self): """ Get dimension names and attributes from the grid header. @@ -151,18 +151,13 @@ def _parse_dimensions(self) -> dict[str, list]: attributes for each dimension are parsed from the grid header following GMT source codes. See the GMT functions "gmtnc_put_units", "gmtnc_get_units" and "gmtnc_grd_info" for reference. - - Returns - ------- - dict - Dictionary containing the list of dimension names and attributes. """ # Default dimension names. - dims: tuple = ("y", "x") + dims = ("y", "x") nameunits = (self.y_units, self.x_units) # Dictionary for dimension attributes with the dimension name as the key. - attrs: dict = {dim: {} for dim in dims} + attrs = {dim: {} for dim in dims} # Dictionary for mapping the default dimension names to the actual names. newdims = {dim: dim for dim in dims} # Loop over dimensions and get the dimension name and attributes from header @@ -214,16 +209,17 @@ def get_data_attrs(self) -> dict: attrs The attributes for the data variable. """ - attrs = {"Conventions": "CF-1.7"} + attrs : dict[str, Any] = {} + attrs["Conventions"] = "CF-1.7" + attrs["title"] = self.title + attrs["history"] = self.command + attrs["description"] = self.remark long_name, units = _parse_nameunits(self.z_units.decode()) if long_name: attrs["long_name"] = long_name if units: attrs["units"] = units attrs["actual_range"] = np.array([self.z_min, self.z_max]) - attrs["title"] = self.title - attrs["history"] = self.command - attrs["description"] = self.remark return attrs def get_dims(self): From e3dec88cb96a6bacf57e53846a8b53c587742990 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 1 Apr 2024 11:05:05 +0800 Subject: [PATCH 12/17] Add GMT_GRID_VARNAME_LEN80 used in GMT_CUBE --- pygmt/datatypes/header.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 35044b26e51..9692c3968b4 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -3,7 +3,7 @@ """ import ctypes as ctp -from typing import ClassVar, Any +from typing import Any, ClassVar import numpy as np @@ -14,6 +14,7 @@ # So we have to hardcode the values here. GMT_GRID_UNIT_LEN80 = 80 GMT_GRID_TITLE_LEN80 = 80 +GMT_GRID_VARNAME_LEN80 = 80 GMT_GRID_COMMAND_LEN320 = 320 GMT_GRID_REMARK_LEN160 = 160 @@ -209,7 +210,7 @@ def get_data_attrs(self) -> dict: attrs The attributes for the data variable. """ - attrs : dict[str, Any] = {} + attrs: dict[str, Any] = {} attrs["Conventions"] = "CF-1.7" attrs["title"] = self.title attrs["history"] = self.command From 836c00428e3300a9a0603efb2fd3126bb38b1e5d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 31 Mar 2024 20:58:43 +0800 Subject: [PATCH 13/17] Add doctests --- pygmt/datatypes/header.py | 52 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 9692c3968b4..c56cd3cec77 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -70,6 +70,52 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 The class is used in the `GMT_GRID`/`GMT_IMAGE`/`GMT_CUBE` data structure. See the GMT source code gmt_resources.h for the original C structure definitions. + + Examples + -------- + >>> from pprint import pprint + >>> from pygmt.clib import Session + + >>> with Session() as lib: + ... with lib.virtualfile_out(kind="grid") as voutgrd: + ... lib.call_module("read", f"@static_earth_relief.nc {voutgrd} -Tg") + ... # Read the grid from the virtual file + ... grid = lib.read_virtualfile(voutgrd, kind="grid") + ... header = grid.contents.header.contents + ... name = header.get_name() + ... attrs = header.get_data_attrs() + ... dims = header.get_dims() + ... dim_attrs = header.get_dim_attrs() + ... gtype = header.get_gtype() + ... registration = header.get_registration() + >>> name + 'z' + >>> pprint(attrs) + {'Conventions': 'CF-1.7', + 'actual_range': array([190., 981.]), + 'description': 'Reduced by Gaussian Cartesian filtering (111.2 km fullwidth) ' + 'from SRTM15_V2.3.nc [Sandwell et al., 2022; ' + 'https://doi.org/10.1029/2021EA002069]', + 'history': 'grdcut @earth_relief_01d_p -R-55/-47/-24/-10 ' + '-Gstatic_earth_relief.nc', + 'long_name': 'elevation (m)', + 'title': 'Produced by grdcut'} + >>> dims + ['lat', 'lon'] + >>> pprint(dim_attrs[0]) + {'actual_range': array([-24., -10.]), + 'axis': 'Y', + 'long_name': 'latitude', + 'standard_name': 'latitude', + 'units': 'degrees_north'} + >>> pprint(dim_attrs[1]) + {'actual_range': array([-55., -47.]), + 'axis': 'X', + 'long_name': 'longitude', + 'standard_name': 'longitude', + 'units': 'degrees_east'} + >>> gtype, registration + (1, 1) """ _fields_: ClassVar = [ @@ -212,9 +258,9 @@ def get_data_attrs(self) -> dict: """ attrs: dict[str, Any] = {} attrs["Conventions"] = "CF-1.7" - attrs["title"] = self.title - attrs["history"] = self.command - attrs["description"] = self.remark + attrs["title"] = self.title.decode() + attrs["history"] = self.command.decode() + attrs["description"] = self.remark.decode() long_name, units = _parse_nameunits(self.z_units.decode()) if long_name: attrs["long_name"] = long_name From 4ca4533a835d0fccaeaca56da88d2d72f1023cb9 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 1 Apr 2024 12:56:57 +0800 Subject: [PATCH 14/17] Revert "Add doctests" This reverts commit 836c00428e3300a9a0603efb2fd3126bb38b1e5d. --- pygmt/datatypes/header.py | 52 +++------------------------------------ 1 file changed, 3 insertions(+), 49 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index c56cd3cec77..9692c3968b4 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -70,52 +70,6 @@ class _GMT_GRID_HEADER(ctp.Structure): # noqa: N801 The class is used in the `GMT_GRID`/`GMT_IMAGE`/`GMT_CUBE` data structure. See the GMT source code gmt_resources.h for the original C structure definitions. - - Examples - -------- - >>> from pprint import pprint - >>> from pygmt.clib import Session - - >>> with Session() as lib: - ... with lib.virtualfile_out(kind="grid") as voutgrd: - ... lib.call_module("read", f"@static_earth_relief.nc {voutgrd} -Tg") - ... # Read the grid from the virtual file - ... grid = lib.read_virtualfile(voutgrd, kind="grid") - ... header = grid.contents.header.contents - ... name = header.get_name() - ... attrs = header.get_data_attrs() - ... dims = header.get_dims() - ... dim_attrs = header.get_dim_attrs() - ... gtype = header.get_gtype() - ... registration = header.get_registration() - >>> name - 'z' - >>> pprint(attrs) - {'Conventions': 'CF-1.7', - 'actual_range': array([190., 981.]), - 'description': 'Reduced by Gaussian Cartesian filtering (111.2 km fullwidth) ' - 'from SRTM15_V2.3.nc [Sandwell et al., 2022; ' - 'https://doi.org/10.1029/2021EA002069]', - 'history': 'grdcut @earth_relief_01d_p -R-55/-47/-24/-10 ' - '-Gstatic_earth_relief.nc', - 'long_name': 'elevation (m)', - 'title': 'Produced by grdcut'} - >>> dims - ['lat', 'lon'] - >>> pprint(dim_attrs[0]) - {'actual_range': array([-24., -10.]), - 'axis': 'Y', - 'long_name': 'latitude', - 'standard_name': 'latitude', - 'units': 'degrees_north'} - >>> pprint(dim_attrs[1]) - {'actual_range': array([-55., -47.]), - 'axis': 'X', - 'long_name': 'longitude', - 'standard_name': 'longitude', - 'units': 'degrees_east'} - >>> gtype, registration - (1, 1) """ _fields_: ClassVar = [ @@ -258,9 +212,9 @@ def get_data_attrs(self) -> dict: """ attrs: dict[str, Any] = {} attrs["Conventions"] = "CF-1.7" - attrs["title"] = self.title.decode() - attrs["history"] = self.command.decode() - attrs["description"] = self.remark.decode() + attrs["title"] = self.title + attrs["history"] = self.command + attrs["description"] = self.remark long_name, units = _parse_nameunits(self.z_units.decode()) if long_name: attrs["long_name"] = long_name From c5897ba8b27e193cbfec868be0bd2a31914e5e0a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 1 Apr 2024 12:57:47 +0800 Subject: [PATCH 15/17] Decode title, history and description --- pygmt/datatypes/header.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 9692c3968b4..d4ca43c838d 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -212,9 +212,9 @@ def get_data_attrs(self) -> dict: """ attrs: dict[str, Any] = {} attrs["Conventions"] = "CF-1.7" - attrs["title"] = self.title - attrs["history"] = self.command - attrs["description"] = self.remark + attrs["title"] = self.title.decode() + attrs["history"] = self.command.decode() + attrs["description"] = self.remark.decode() long_name, units = _parse_nameunits(self.z_units.decode()) if long_name: attrs["long_name"] = long_name From 79f4c879e1cc0af2ce3f8cbd9d5eaac6a5644ad6 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 1 Apr 2024 13:22:52 +0800 Subject: [PATCH 16/17] Change getter methods to properties --- pygmt/datatypes/header.py | 63 ++++++++++----------------------------- 1 file changed, 16 insertions(+), 47 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index d4ca43c838d..109730f1163 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -190,25 +190,17 @@ def _parse_dimensions(self): "attrs": [attrs[dim] for dim in dims], } - def get_name(self) -> str: + @property + def name(self) -> str: """ - Get the name of the grid from the grid header. - - Returns - ------- - name - The name of the grid. + Name of the grid. """ return "z" - def get_data_attrs(self) -> dict: + @property + def data_attrs(self) -> dict[str, Any]: """ - Get the attributes for the data variable from the grid header. - - Returns - ------- - attrs - The attributes for the data variable. + Attributes for the data variable from the grid header. """ attrs: dict[str, Any] = {} attrs["Conventions"] = "CF-1.7" @@ -223,56 +215,33 @@ def get_data_attrs(self) -> dict: attrs["actual_range"] = np.array([self.z_min, self.z_max]) return attrs - def get_dims(self): + @property + def dims(self) -> list: """ - Get the dimension names from the grid header. - - Returns - ------- - dims : tuple - The dimension names. + List of dimension names. """ if not hasattr(self, "_nc"): self._parse_dimensions() return self._nc["dims"] - def get_dim_attrs(self) -> list: + @property + def dim_attrs(self) -> list[dict]: """ - Get the attributes for each dimension from the grid header. - - Returns - ------- - attrs - List of attributes for each dimension. + List of attributes for each dimension. """ if not hasattr(self, "_nc"): self._parse_dimensions() return self._nc["attrs"] - def get_gtype(self) -> int: + @property + def gtype(self) -> int: """ - Get the grid type from the grid header. + Grid type. 0 for Cartesian grid and 1 for geographic grid. The grid is assumed to be Cartesian by default. If the x/y dimensions are named "lon"/"lat" or have units "degrees_east"/"degrees_north", then the grid is assumed to be geographic. - - Returns - ------- - gtype - The grid type. 0 for Cartesian grid and 1 for geographic grid. """ - dims = self.get_dims() + dims = self.dims gtype = 1 if dims[0] == "lat" and dims[1] == "lon" else 0 return gtype - - def get_registration(self) -> int: - """ - Get the grid registration from the grid header. - - Returns - ------- - registration - The grid registration. 0 for gridline and 1 for pixel. - """ - return self.registration From a4b55ec1c2e0b21722fb0b00cfdd084af10b9b51 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 1 Apr 2024 13:50:35 +0800 Subject: [PATCH 17/17] Fix strict=False to strict=True --- pygmt/datatypes/header.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/datatypes/header.py b/pygmt/datatypes/header.py index 109730f1163..04e10ac0c72 100644 --- a/pygmt/datatypes/header.py +++ b/pygmt/datatypes/header.py @@ -161,8 +161,8 @@ def _parse_dimensions(self): attrs = {dim: {} for dim in dims} # Dictionary for mapping the default dimension names to the actual names. newdims = {dim: dim for dim in dims} - # Loop over dimensions and get the dimension name and attributes from header - for dim, nameunit in zip(dims, nameunits, strict=False): + # Loop over dimensions and get the dimension name and attributes from header. + for dim, nameunit in zip(dims, nameunits, strict=True): # The long_name and units attributes. long_name, units = _parse_nameunits(nameunit.decode()) if long_name: @@ -171,7 +171,7 @@ def _parse_dimensions(self): attrs[dim]["units"] = units # "degrees_east"/"degrees_north" are the units for geographic coordinates - # following CF-conventions + # following CF-conventions. if units == "degrees_east": attrs[dim]["standard_name"] = "longitude" newdims[dim] = "lon"