diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index a8907950553..b02c8044903 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -41,6 +41,7 @@ jobs: --exclude "^file://" --exclude "^https://docs.generic-mapping-tools.org/latest/%s$" --exclude "^https://docs.generic-mapping-tools.org/latest/%3Cmodule-name%3E.html$" + --exclude "^https://www.generic-mapping-tools.org/remote-datasets/%s$" --exclude "https://hackmd.io/@pygmt" --verbose "repository/**/*.rst" diff --git a/doc/api/index.rst b/doc/api/index.rst index b488a2a4874..5287a36836e 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -124,6 +124,9 @@ Operations on tabular data sphdistance sphinterpolate surface + triangulate + triangulate.regular_grid + triangulate.delaunay_triples xyz2grd Operations on raster data diff --git a/examples/gallery/embellishments/legend.py b/examples/gallery/embellishments/legend.py index 65f8c615b00..6463b6ef0d5 100644 --- a/examples/gallery/embellishments/legend.py +++ b/examples/gallery/embellishments/legend.py @@ -19,7 +19,7 @@ pen="faint", label="Apples", ) -fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label='"My lines"') +fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines") fig.plot(data="@Table_5_11.txt", style="t0.15i", color="orange", label="Oranges") fig.legend(position="JTR+jTR+o0.2c", box=True) diff --git a/pygmt/__init__.py b/pygmt/__init__.py index db33925edf2..23e1eab5de7 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -56,6 +56,7 @@ sphdistance, sphinterpolate, surface, + triangulate, which, x2sys_cross, x2sys_init, diff --git a/pygmt/datasets/earth_age.py b/pygmt/datasets/earth_age.py index e7a895475d4..1e74a099f10 100644 --- a/pygmt/datasets/earth_age.py +++ b/pygmt/datasets/earth_age.py @@ -26,7 +26,7 @@ def load_earth_age(resolution="01d", region=None, registration=None): registration type (**p** for pixel registration or **g** for gridline registration). - Refer to :gmt-datasets:`earth_age.html` for more details. + Refer to :gmt-datasets:`earth-age.html` for more details. Parameters ---------- @@ -54,8 +54,8 @@ def load_earth_age(resolution="01d", region=None, registration=None): The Earth seafloor crustal age grid. Coordinates are latitude and longitude in degrees. Age is in millions of years (Myr). - Notes - ----- + Note + ---- The :class:`xarray.DataArray` grid doesn't support slice operation, for Earth seafloor crustal age with resolutions of 5 arc-minutes or higher, which are stored as smaller tiles. diff --git a/pygmt/datasets/earth_relief.py b/pygmt/datasets/earth_relief.py index b9bb2851db4..f00653eafbd 100644 --- a/pygmt/datasets/earth_relief.py +++ b/pygmt/datasets/earth_relief.py @@ -62,8 +62,8 @@ def load_earth_relief(resolution="01d", region=None, registration=None, use_srtm The Earth relief grid. Coordinates are latitude and longitude in degrees. Relief is in meters. - Notes - ----- + Note + ---- The :class:`xarray.DataArray` grid doesn't support slice operation, for Earth relief data with resolutions of 5 arc-minutes or higher, which are stored as smaller tiles. diff --git a/pygmt/figure.py b/pygmt/figure.py index f6111fff54f..b922705d752 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -235,10 +235,17 @@ def psconvert(self, icc_gray=False, **kwargs): kwargs["N"] = "+i" else: kwargs["N"] += "+i" - # allow for spaces in figure name - kwargs["F"] = f'"{kwargs.get("F")}"' if kwargs.get("F") else None + + # Manually handle prefix -F argument so spaces aren't converted to \040 + # by build_arg_string function. For more information, see + # https://github.com/GenericMappingTools/pygmt/pull/1487 + try: + prefix_arg = f'-F"{kwargs.pop("F")}"' + except KeyError as err: + raise GMTInvalidInput("The 'prefix' must be specified.") from err + with Session() as lib: - lib.call_module("psconvert", build_arg_string(kwargs)) + lib.call_module("psconvert", f"{prefix_arg} {build_arg_string(kwargs)}") def savefig( self, fname, transparent=False, crop=True, anti_alias=True, show=False, **kwargs diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index b3df51558b4..ea7a89873e7 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -120,7 +120,7 @@ def dummy_context(arg): def build_arg_string(kwargs): - """ + r""" Transform keyword arguments into a GMT argument string. Make sure all arguments have been previously converted to a string @@ -131,6 +131,11 @@ def build_arg_string(kwargs): same command line argument. For example, the kwargs entry ``'B': ['xa', 'yaf']`` will be converted to ``-Bxa -Byaf`` in the argument string. + Note that spaces `` `` in arguments are converted to the equivalent octal + code ``\040``, except in the case of -J (projection) arguments where PROJ4 + strings (e.g. "+proj=longlat +datum=WGS84") will have their spaces removed. + See https://github.com/GenericMappingTools/pygmt/pull/1487 for more info. + Parameters ---------- kwargs : dict @@ -151,7 +156,7 @@ def build_arg_string(kwargs): ... A=True, ... B=False, ... E=200, - ... J="X4c", + ... J="+proj=longlat +datum=WGS84", ... P="", ... R="1/2/3/4", ... X=None, @@ -160,7 +165,7 @@ def build_arg_string(kwargs): ... ) ... ) ... ) - -A -E200 -JX4c -P -R1/2/3/4 -Z0 + -A -E200 -J+proj=longlat+datum=WGS84 -P -R1/2/3/4 -Z0 >>> print( ... build_arg_string( ... dict( @@ -176,6 +181,16 @@ def build_arg_string(kwargs): Traceback (most recent call last): ... pygmt.exceptions.GMTInvalidInput: Unrecognized parameter 'watre'. + >>> print( + ... build_arg_string( + ... dict( + ... B=["af", "WSne+tBlank Space"], + ... F='+t"Empty Spaces"', + ... l="'Void Space'", + ... ), + ... ) + ... ) + -BWSne+tBlank\040Space -Baf -F+t"Empty\040\040Spaces" -l'Void\040Space' """ gmt_args = [] @@ -185,11 +200,19 @@ def build_arg_string(kwargs): if kwargs[key] is None or kwargs[key] is False: pass # Exclude arguments that are None and False elif is_nonstr_iter(kwargs[key]): - gmt_args.extend(f"-{key}{value}" for value in kwargs[key]) + for value in kwargs[key]: + _value = str(value).replace(" ", r"\040") + gmt_args.append(rf"-{key}{_value}") elif kwargs[key] is True: gmt_args.append(f"-{key}") else: - gmt_args.append(f"-{key}{kwargs[key]}") + if key != "J": # non-projection parameters + _value = str(kwargs[key]).replace(" ", r"\040") + else: + # special handling if key == "J" (projection) + # remove any spaces in PROJ4 string + _value = str(kwargs[key]).replace(" ", "") + gmt_args.append(rf"-{key}{_value}") return " ".join(sorted(gmt_args)) diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 6c1a29b91de..a31cf8f88df 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -47,6 +47,7 @@ from pygmt.src.subplot import set_panel, subplot from pygmt.src.surface import surface from pygmt.src.text import text_ as text # "text" is an argument within "text_" +from pygmt.src.triangulate import triangulate from pygmt.src.velo import velo from pygmt.src.which import which from pygmt.src.wiggle import wiggle diff --git a/pygmt/src/blockm.py b/pygmt/src/blockm.py index 83ebf989a2d..75693524d6c 100644 --- a/pygmt/src/blockm.py +++ b/pygmt/src/blockm.py @@ -85,7 +85,7 @@ def _blockm(block_method, data, x, y, z, outfile, **kwargs): r="registration", w="wrap", ) -@kwargs_to_strings(R="sequence", i="sequence_comma", o="sequence_comma") +@kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma", o="sequence_comma") def blockmean(data=None, x=None, y=None, z=None, outfile=None, **kwargs): r""" Block average (x,y,z) data tables by mean estimation. @@ -182,7 +182,7 @@ def blockmean(data=None, x=None, y=None, z=None, outfile=None, **kwargs): r="registration", w="wrap", ) -@kwargs_to_strings(R="sequence", i="sequence_comma", o="sequence_comma") +@kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma", o="sequence_comma") def blockmedian(data=None, x=None, y=None, z=None, outfile=None, **kwargs): r""" Block average (x,y,z) data tables by median estimation. @@ -270,7 +270,7 @@ def blockmedian(data=None, x=None, y=None, z=None, outfile=None, **kwargs): r="registration", w="wrap", ) -@kwargs_to_strings(R="sequence", i="sequence_comma", o="sequence_comma") +@kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma", o="sequence_comma") def blockmode(data=None, x=None, y=None, z=None, outfile=None, **kwargs): r""" Block average (x,y,z) data tables by mode estimation. diff --git a/pygmt/src/config.py b/pygmt/src/config.py index 7c347e78758..7e4df67d3be 100644 --- a/pygmt/src/config.py +++ b/pygmt/src/config.py @@ -55,7 +55,7 @@ def __init__(self, **kwargs): self.old_defaults[key] = lib.get_default(key) # call gmt set to change GMT defaults - arg_str = " ".join([f"{key}={value}" for key, value in kwargs.items()]) + arg_str = " ".join([f'{key}="{value}"' for key, value in kwargs.items()]) with Session() as lib: lib.call_module("set", arg_str) diff --git a/pygmt/src/contour.py b/pygmt/src/contour.py index 20f24f70133..f64cd5e0c15 100644 --- a/pygmt/src/contour.py +++ b/pygmt/src/contour.py @@ -6,7 +6,6 @@ from pygmt.helpers import ( build_arg_string, check_data_input_order, - deprecate_parameter, fmt_docstring, kwargs_to_strings, use_alias, @@ -14,7 +13,6 @@ @fmt_docstring -@deprecate_parameter("columns", "incols", "v0.4.0", remove_version="v0.6.0") @check_data_input_order("v0.5.0", remove_version="v0.7.0") @use_alias( A="annotation", diff --git a/pygmt/src/grdfilter.py b/pygmt/src/grdfilter.py index cffcb928d22..0c8116ac708 100644 --- a/pygmt/src/grdfilter.py +++ b/pygmt/src/grdfilter.py @@ -26,7 +26,7 @@ f="coltypes", r="registration", ) -@kwargs_to_strings(R="sequence") +@kwargs_to_strings(I="sequence", R="sequence") def grdfilter(grid, **kwargs): r""" Filter a grid in the space (or time) domain. diff --git a/pygmt/src/grdhisteq.py b/pygmt/src/grdhisteq.py index 61de7503d4e..09555d05e94 100644 --- a/pygmt/src/grdhisteq.py +++ b/pygmt/src/grdhisteq.py @@ -141,7 +141,7 @@ def _grdhisteq(grid, output_type, **kwargs): def equalize_grid( grid, *, - outgrid=True, + outgrid=None, divisions=None, region=None, gaussian=None, @@ -163,7 +163,7 @@ def equalize_grid( ---------- grid : str or xarray.DataArray The file name of the input grid or the grid loaded as a DataArray. - outgrid : str or bool or None + outgrid : str or None The name of the output netCDF file with extension .nc to store the grid in. divisions : int @@ -183,7 +183,7 @@ def equalize_grid( ret: xarray.DataArray or None Return type depends on the ``outgrid`` parameter: - - xarray.DataArray if ``outgrid`` is True or None + - xarray.DataArray if ``outgrid`` is None - None if ``outgrid`` is a str (grid output is stored in ``outgrid``) @@ -202,8 +202,8 @@ def equalize_grid( ------- :meth:`pygmt.grd2cpt` - Notes - ----- + Note + ---- This method does a weighted histogram equalization for geographic grids to account for node area varying with latitude. """ @@ -211,9 +211,11 @@ def equalize_grid( with GMTTempFile(suffix=".nc") as tmpfile: if isinstance(outgrid, str): output_type = "file" - else: + elif outgrid is None: output_type = "xarray" outgrid = tmpfile.name + else: + raise GMTInvalidInput("Must specify 'outgrid' as a string or None.") return grdhisteq._grdhisteq( grid=grid, output_type=output_type, @@ -281,12 +283,13 @@ def compute_bins( Returns ------- - ret: pandas.DataFrame or None - Return type depends on the ``outfile`` parameter: + ret : pandas.DataFrame or numpy.ndarray or None + Return type depends on ``outfile`` and ``output_type``: - - pandas.DataFrame if ``outfile`` is True or None - - None if ``outfile`` is a str (file output is stored in + - None if ``outfile`` is set (output will be stored in file set by ``outfile``) + - :class:`pandas.DataFrame` or :class:`numpy.ndarray` if + ``outfile`` is not set (depends on ``output_type``) Example ------- @@ -312,8 +315,8 @@ def compute_bins( ------- :meth:`pygmt.grd2cpt` - Notes - ----- + Note + ---- This method does a weighted histogram equalization for geographic grids to account for node area varying with latitude. """ diff --git a/pygmt/src/grdlandmask.py b/pygmt/src/grdlandmask.py index c050824dc5b..93a9c369582 100644 --- a/pygmt/src/grdlandmask.py +++ b/pygmt/src/grdlandmask.py @@ -27,7 +27,7 @@ V="verbose", r="registration", ) -@kwargs_to_strings(R="sequence", N="sequence", E="sequence") +@kwargs_to_strings(I="sequence", R="sequence", N="sequence", E="sequence") def grdlandmask(**kwargs): r""" Create a grid file with set values for land and water. diff --git a/pygmt/src/grdproject.py b/pygmt/src/grdproject.py index 782d0ece58c..42734f39b72 100644 --- a/pygmt/src/grdproject.py +++ b/pygmt/src/grdproject.py @@ -30,7 +30,7 @@ n="interpolation", r="registration", ) -@kwargs_to_strings(C="sequence", R="sequence") +@kwargs_to_strings(C="sequence", D="sequence", R="sequence") def grdproject(grid, **kwargs): r""" Change projection of gridded data between geographical and rectangular. diff --git a/pygmt/src/nearneighbor.py b/pygmt/src/nearneighbor.py index 13f0f549993..1fe7e24bab8 100644 --- a/pygmt/src/nearneighbor.py +++ b/pygmt/src/nearneighbor.py @@ -33,7 +33,7 @@ r="registration", w="wrap", ) -@kwargs_to_strings(R="sequence", i="sequence_comma") +@kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma") def nearneighbor(data=None, x=None, y=None, z=None, **kwargs): r""" Grid table data using a "Nearest neighbor" algorithm diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index 8345c065916..e5df65498ff 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -7,7 +7,6 @@ build_arg_string, check_data_input_order, data_kind, - deprecate_parameter, fmt_docstring, is_nonstr_iter, kwargs_to_strings, @@ -17,8 +16,6 @@ @fmt_docstring -@deprecate_parameter("sizes", "size", "v0.4.0", remove_version="v0.6.0") -@deprecate_parameter("columns", "incols", "v0.4.0", remove_version="v0.6.0") @check_data_input_order("v0.5.0", remove_version="v0.7.0") @use_alias( A="straight_line", diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index 42fc2faf143..e5f834b4c1c 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -7,7 +7,6 @@ build_arg_string, check_data_input_order, data_kind, - deprecate_parameter, fmt_docstring, is_nonstr_iter, kwargs_to_strings, @@ -17,8 +16,6 @@ @fmt_docstring -@deprecate_parameter("columns", "incols", "v0.4.0", remove_version="v0.6.0") -@deprecate_parameter("sizes", "size", "v0.4.0", remove_version="v0.6.0") @check_data_input_order("v0.5.0", remove_version="v0.7.0") @use_alias( A="straight_line", diff --git a/pygmt/src/rose.py b/pygmt/src/rose.py index 8089a8510a7..842421067e7 100644 --- a/pygmt/src/rose.py +++ b/pygmt/src/rose.py @@ -6,7 +6,6 @@ from pygmt.helpers import ( build_arg_string, check_data_input_order, - deprecate_parameter, fmt_docstring, kwargs_to_strings, use_alias, @@ -14,7 +13,6 @@ @fmt_docstring -@deprecate_parameter("columns", "incols", "v0.4.0", remove_version="v0.6.0") @check_data_input_order("v0.5.0", remove_version="v0.7.0") @use_alias( A="sector", diff --git a/pygmt/src/subplot.py b/pygmt/src/subplot.py index 1e713afb189..19a9e6ecfd6 100644 --- a/pygmt/src/subplot.py +++ b/pygmt/src/subplot.py @@ -148,10 +148,6 @@ def subplot(self, nrows=1, ncols=1, **kwargs): {XY} """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access - # allow for spaces in string without needing double quotes - if isinstance(kwargs.get("A"), str): - kwargs["A"] = f'"{kwargs.get("A")}"' - kwargs["T"] = f'"{kwargs.get("T")}"' if kwargs.get("T") else None if nrows < 1 or ncols < 1: raise GMTInvalidInput("Please ensure that both 'nrows'>=1 and 'ncols'>=1.") @@ -222,8 +218,6 @@ def set_panel(self, panel=None, **kwargs): {V} """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access - # allow for spaces in string with needing double quotes - kwargs["A"] = f'"{kwargs.get("A")}"' if kwargs.get("A") is not None else None # convert tuple or list to comma-separated str panel = ",".join(map(str, panel)) if is_nonstr_iter(panel) else panel diff --git a/pygmt/src/surface.py b/pygmt/src/surface.py index 9ab8e7f70bc..c86ebc41e04 100644 --- a/pygmt/src/surface.py +++ b/pygmt/src/surface.py @@ -33,7 +33,7 @@ r="registration", w="wrap", ) -@kwargs_to_strings(R="sequence") +@kwargs_to_strings(I="sequence", R="sequence") def surface(data=None, x=None, y=None, z=None, **kwargs): r""" Grids table data using adjustable tension continuous curvature splines. diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 3c7d0c2fb31..4e74d47f1b7 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -210,7 +210,7 @@ def text_( kwargs["F"] += f"+j{justify}" if isinstance(position, str): - kwargs["F"] += f'+c{position}+t"{text}"' + kwargs["F"] += f"+c{position}+t{text}" extra_arrays = [] # If an array of transparency is given, GMT will read it from diff --git a/pygmt/src/triangulate.py b/pygmt/src/triangulate.py new file mode 100644 index 00000000000..e50eb086269 --- /dev/null +++ b/pygmt/src/triangulate.py @@ -0,0 +1,397 @@ +""" +triangulate - Delaunay triangulation or Voronoi partitioning and gridding of +Cartesian data. +""" +import warnings + +import pandas as pd +from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import ( + GMTTempFile, + build_arg_string, + fmt_docstring, + kwargs_to_strings, + use_alias, +) +from pygmt.io import load_dataarray + + +class triangulate: # pylint: disable=invalid-name + """ + Delaunay triangulation or Voronoi partitioning and gridding of Cartesian + data. + + Triangulate reads in x,y[,z] data and performs Delaunay triangulation, + i.e., it finds how the points should be connected to give the most + equilateral triangulation possible. If a map projection (give ``region`` + and ``projection``) is chosen then it is applied before the triangulation + is calculated. By default, the output is triplets of point id numbers that + make up each triangle. The id numbers refer to the points position (line + number, starting at 0 for the first line) in the input file. If ``outgrid`` + and ``spacing`` are set a grid will be calculated based on the surface + defined by the planar triangles. The actual algorithm used in the + triangulations is either that of Watson [1982] or Shewchuk [1996] [Default + is Shewchuk if installed; type ``gmt get GMT_TRIANGULATE`` on the command + line to see which method is selected]. Furthermore, if the Shewchuk + algorithm is installed then you can also perform the calculation of Voronoi + polygons and optionally grid your data via the natural nearest neighbor + algorithm. + + Note + ---- + For geographic data with global or very large extent you should consider + :gmt-docs:`sphtriangulate ` instead since + ``triangulate`` is a Cartesian or small-geographic area operator and is + unaware of periodic or polar boundary conditions. + """ + + @staticmethod + @fmt_docstring + @use_alias( + G="outgrid", + I="spacing", + J="projection", + R="region", + V="verbose", + b="binary", + d="nodata", + e="find", + f="coltypes", + h="header", + i="incols", + r="registration", + s="skiprows", + w="wrap", + ) + @kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma") + def _triangulate( + data=None, x=None, y=None, z=None, *, output_type, outfile=None, **kwargs + ): + """ + Delaunay triangulation or Voronoi partitioning and gridding of + Cartesian data. + + Must provide ``outfile`` or ``outgrid``. + + Full option list at :gmt-docs:`triangulate.html` + + {aliases} + + Parameters + ---------- + x/y/z : np.ndarray + Arrays of x and y coordinates and values z of the data points. + data : str or {table-like} + Pass in (x, y, z) or (longitude, latitude, elevation) values by + providing a file name to an ASCII data table, a 2D + {table-classes}. + {J} + {R} + {I} + outgrid : bool or str + The name of the output netCDF file with extension .nc to store the + grid in. The interpolation is performed in the original + coordinates, so if your triangles are close to the poles you are + better off projecting all data to a local coordinate system before + using ``triangulate`` (this is true of all gridding routines) or + instead select :gmt-docs:`sphtriangulate `. + outfile : str or bool or None + The name of the output ASCII file to store the results of the + histogram equalization in. + output_type: str + Determines the output type. Use "file", "xarray", "pandas", or + "numpy". + {V} + {b} + {d} + {e} + {f} + {h} + {i} + {r} + Only valid with ``outgrid``. + {s} + {w} + + Returns + ------- + ret: numpy.ndarray or pandas.DataFrame or xarray.DataArray or None + Return type depends on the ``output_type`` parameter: + + - numpy.ndarray if ``output_type`` is "numpy" + - pandas.DataFrame if ``output_type`` is "pandas" + - xarray.DataArray if ``output_type`` is "xarray"" + - None if ``output_type`` is "file" (output is stored in + ``outgrid`` or ``outfile``) + """ + with Session() as lib: + # Choose how data will be passed into the module + table_context = lib.virtualfile_from_data( + check_kind="vector", data=data, x=x, y=y, z=z, required_z=False + ) + with table_context as infile: + # table output if outgrid is unset, else output to outgrid + if (outgrid := kwargs.get("G")) is None: + kwargs.update({">": outfile}) + arg_str = " ".join([infile, build_arg_string(kwargs)]) + lib.call_module(module="triangulate", args=arg_str) + + if output_type == "file": + return None + if output_type == "xarray": + return load_dataarray(outgrid) + + result = pd.read_csv(outfile, sep="\t", header=None) + if output_type == "numpy": + return result.to_numpy() + return result + + @staticmethod + @fmt_docstring + def regular_grid( # pylint: disable=too-many-arguments,too-many-locals + data=None, + x=None, + y=None, + z=None, + outgrid=None, + spacing=None, + projection=None, + region=None, + verbose=None, + binary=None, + nodata=None, + find=None, + coltypes=None, + header=None, + incols=None, + registration=None, + skiprows=None, + wrap=None, + **kwargs, + ): + """ + Delaunay triangle based gridding of Cartesian data. + + Reads in x,y[,z] data and performs Delaunay triangulation, i.e., it + finds how the points should be connected to give the most equilateral + triangulation possible. If a map projection (give ``region`` and + ``projection``) is chosen then it is applied before the triangulation + is calculated. By setting ``outgrid`` and ``spacing``, a grid will be + calculated based on the surface defined by the planar triangles. The + actual algorithm used in the triangulations is either that of Watson + [1982] or Shewchuk [1996] [Default is Shewchuk if installed; type + ``gmt get GMT_TRIANGULATE`` on the command line to see which method is + selected]. This choice is made during the GMT installation. + Furthermore, if the Shewchuk algorithm is installed then you can also + perform the calculation of Voronoi polygons and optionally grid your + data via the natural nearest neighbor algorithm. + + Must provide either ``data`` or ``x``, ``y``, and ``z``. + + Must provide ``region`` and ``spacing``. + + Full option list at :gmt-docs:`triangulate.html` + + Parameters + ---------- + x/y/z : np.ndarray + Arrays of x and y coordinates and values z of the data points. + data : str or {table-like} + Pass in (x, y[, z]) or (longitude, latitude[, elevation]) values by + providing a file name to an ASCII data table, a 2D + {table-classes}. + {J} + {R} + {I} + outgrid : str or None + The name of the output netCDF file with extension .nc to store the + grid in. The interpolation is performed in the original + coordinates, so if your triangles are close to the poles you are + better off projecting all data to a local coordinate system before + using ``triangulate`` (this is true of all gridding routines) or + instead select :gmt-docs:`sphtriangulate `. + {V} + {b} + {d} + {e} + {f} + {h} + {i} + {r} + {s} + {w} + + Returns + ------- + ret: xarray.DataArray or None + Return type depends on whether the ``outgrid`` parameter is set: + + - xarray.DataArray if ``outgrid`` is None (default) + - None if ``outgrid`` is a str (grid output is stored in + ``outgrid``) + + Note + ---- + For geographic data with global or very large extent you should + consider :gmt-docs:`sphtriangulate ` instead since + ``triangulate`` is a Cartesian or small-geographic area operator and is + unaware of periodic or polar boundary conditions. + """ + # Return an xarray.DataArray if ``outgrid`` is not set + with GMTTempFile(suffix=".nc") as tmpfile: + if isinstance(outgrid, str): + output_type = "file" + elif outgrid is None: + output_type = "xarray" + outgrid = tmpfile.name + else: + raise GMTInvalidInput( + "'outgrid' should be a proper file name or `None`" + ) + + return triangulate._triangulate( + data=data, + x=x, + y=y, + z=z, + output_type=output_type, + outgrid=outgrid, + spacing=spacing, + projection=projection, + region=region, + verbose=verbose, + binary=binary, + nodata=nodata, + find=find, + coltypes=coltypes, + header=header, + incols=incols, + registration=registration, + skiprows=skiprows, + wrap=wrap, + **kwargs, + ) + + @staticmethod + @fmt_docstring + def delaunay_triples( # pylint: disable=too-many-arguments,too-many-locals + data=None, + x=None, + y=None, + z=None, + output_type="pandas", + outfile=None, + projection=None, + verbose=None, + binary=None, + nodata=None, + find=None, + coltypes=None, + header=None, + incols=None, + skiprows=None, + wrap=None, + **kwargs, + ): + """ + Delaunay triangle based gridding of Cartesian data. + + Reads in x,y[,z] data and performs Delaunay triangulation, i.e., it + finds how the points should be connected to give the most equilateral + triangulation possible. If a map projection (give ``region`` and + ``projection``) is chosen then it is applied before the triangulation + is calculated. The actual algorithm used in the triangulations is + either that of Watson [1982] or Shewchuk [1996] [Default if installed; + type ``gmt get GMT_TRIANGULATE`` on the command line to see which + method is selected). + + Must provide either ``data`` or ``x``, ``y``, and ``z``. + + Full option list at :gmt-docs:`triangulate.html` + + Parameters + ---------- + x/y/z : np.ndarray + Arrays of x and y coordinates and values z of the data points. + data : str or {table-like} + Pass in (x, y, z) or (longitude, latitude, elevation) values by + providing a file name to an ASCII data table, a 2D + {table-classes}. + {J} + {R} + outfile : str or bool or None + The name of the output ASCII file to store the results of the + histogram equalization in. + output_type : str + Determine the format the xyz data will be returned in [Default is + ``pandas``]: + + - ``numpy`` - :class:`numpy.ndarray` + - ``pandas``- :class:`pandas.DataFrame` + - ``file`` - ASCII file (requires ``outfile``) + {V} + {b} + {d} + {e} + {f} + {h} + {i} + {s} + {w} + + Returns + ------- + ret : pandas.DataFrame or numpy.ndarray or None + Return type depends on ``outfile`` and ``output_type``: + + - None if ``outfile`` is set (output will be stored in file set by + ``outfile``) + - :class:`pandas.DataFrame` or :class:`numpy.ndarray` if + ``outfile`` is not set (depends on ``output_type``) + + Note + ---- + For geographic data with global or very large extent you should + consider :gmt-docs:`sphtriangulate ` instead since + ``triangulate`` is a Cartesian or small-geographic area operator and is + unaware of periodic or polar boundary conditions. + """ + # Return a pandas.DataFrame if ``outfile`` is not set + if output_type not in ["numpy", "pandas", "file"]: + raise GMTInvalidInput( + "Must specify 'output_type' either as 'numpy', 'pandas' or 'file'." + ) + + if isinstance(outfile, str) and output_type != "file": + msg = ( + f"Changing 'output_type' from '{output_type}' to 'file' " + "since 'outfile' parameter is set. Please use output_type='file' " + "to silence this warning." + ) + warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) + output_type = "file" + + # Return a pandas.DataFrame if ``outfile`` is not set + with GMTTempFile(suffix=".txt") as tmpfile: + if output_type != "file": + outfile = tmpfile.name + return triangulate._triangulate( + data=data, + x=x, + y=y, + z=z, + output_type=output_type, + outfile=outfile, + projection=projection, + verbose=verbose, + binary=binary, + nodata=nodata, + find=find, + coltypes=coltypes, + header=header, + incols=incols, + skiprows=skiprows, + wrap=wrap, + **kwargs, + ) diff --git a/pygmt/src/xyz2grd.py b/pygmt/src/xyz2grd.py index 54324adf13a..7deb42778ab 100644 --- a/pygmt/src/xyz2grd.py +++ b/pygmt/src/xyz2grd.py @@ -12,6 +12,8 @@ ) from pygmt.io import load_dataarray +__doctest_skip__ = ["xyz2grd"] + @fmt_docstring @use_alias( @@ -109,7 +111,7 @@ def xyz2grd(data=None, x=None, y=None, z=None, **kwargs): - **f** 4-byte floating point single precision - **d** 8-byte floating point double precision - Default format is scanline orientation of ASCII numbers: **-ZTLa**. + [Default format is scanline orientation of ASCII numbers: **La**]. The difference between **A** and **a** is that the latter can decode both *date*\ **T**\ *clock* and *ddd:mm:ss[.xx]* formats but expects each input record to have a single value, while the former can handle @@ -132,6 +134,19 @@ def xyz2grd(data=None, x=None, y=None, z=None, **kwargs): - :class:`xarray.DataArray`: if ``outgrid`` is not set - None if ``outgrid`` is set (grid output will be stored in file set by ``outgrid``) + + Example + ------- + >>> import numpy as np + >>> import pygmt + >>> # generate a grid for z=x**2+y**2, with an x-range of 0 to 3, + >>> # and a y-range of 10.5 to 12.5. The x- and y-spacing are 1.0 and 0.5. + >>> x, y = np.meshgrid([0, 1, 2, 3], [10.5, 11.0, 11.5, 12.0, 12.5]) + >>> z = x**2 + y**2 + >>> xx, yy, zz = x.flatten(), y.flatten(), z.flatten() + >>> grid = pygmt.xyz2grd( + ... x=xx, y=yy, z=zz, spacing=(1.0, 0.5), region=[0, 3, 10, 13] + ... ) """ if "I" not in kwargs or "R" not in kwargs: raise GMTInvalidInput("Both 'region' and 'spacing' must be specified.") diff --git a/pygmt/tests/baseline/test_basemap_utm_projection.png.dvc b/pygmt/tests/baseline/test_basemap_utm_projection.png.dvc new file mode 100644 index 00000000000..c12f0d4026a --- /dev/null +++ b/pygmt/tests/baseline/test_basemap_utm_projection.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: e6984efed2a94673754cc7f1f1d74832 + size: 9069 + path: test_basemap_utm_projection.png diff --git a/pygmt/tests/baseline/test_config_format_date_map.png.dvc b/pygmt/tests/baseline/test_config_format_date_map.png.dvc new file mode 100644 index 00000000000..7bc946d104f --- /dev/null +++ b/pygmt/tests/baseline/test_config_format_date_map.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 3619720cdfcd857cbdbb49ed7fe6e930 + size: 1392 + path: test_config_format_date_map.png diff --git a/pygmt/tests/baseline/test_rose_no_sectors.png.dvc b/pygmt/tests/baseline/test_rose_no_sectors.png.dvc index 0eddeaf6fee..9e0184a9caf 100644 --- a/pygmt/tests/baseline/test_rose_no_sectors.png.dvc +++ b/pygmt/tests/baseline/test_rose_no_sectors.png.dvc @@ -1,4 +1,4 @@ outs: -- md5: 8e1c47b1cf6001dad3b3c0875af4562e - size: 150390 +- md5: ce2d5cd1415b7c7bbeea5bf6ff39c480 + size: 150288 path: test_rose_no_sectors.png diff --git a/pygmt/tests/test_basemap.py b/pygmt/tests/test_basemap.py index 726661ab165..7c05e84973a 100644 --- a/pygmt/tests/test_basemap.py +++ b/pygmt/tests/test_basemap.py @@ -73,6 +73,29 @@ def test_basemap_winkel_tripel(): return fig +@pytest.mark.mpl_image_compare(filename="test_basemap_utm_projection.png") +@pytest.mark.parametrize( + "projection", + [ + "EPSG_32723 +width=5", + "+proj=utm +zone=23 +south +datum=WGS84 +units=m +no_defs +width=5", + ], +) +def test_basemap_utm_projection(projection): + """ + Create a Universal Transverse Mercator (Zone 23S) basemap plot. + + Also check that providing the projection as an EPSG code or PROJ4 string + works. + """ + projection = projection.replace( + "EPSG_", "EPSG:" # workaround Windows not allowing colons in filenames + ) + fig = Figure() + fig.basemap(region=[-52, -50, -12, -11], projection=projection, frame="afg") + return fig + + @pytest.mark.mpl_image_compare def test_basemap_rose(): """ diff --git a/pygmt/tests/test_config.py b/pygmt/tests/test_config.py index ba39ab53b79..067ee1b7289 100644 --- a/pygmt/tests/test_config.py +++ b/pygmt/tests/test_config.py @@ -64,6 +64,25 @@ def test_config_font_annot(): return fig +@pytest.mark.mpl_image_compare +def test_config_format_date_map(): + """ + Test that setting FORMAT_DATE_MAP config changes how the output date string + is plotted. + + Note the space in 'o dd', this acts as a regression test for + https://github.com/GenericMappingTools/pygmt/issues/247. + """ + fig = Figure() + with config(FORMAT_DATE_MAP="o dd"): + fig.basemap( + region=["1969-7-21T", "1969-7-23T", 0, 1], + projection="X2.5c/0.1c", + frame=["sxa1D", "S"], + ) + return fig + + @pytest.mark.mpl_image_compare def test_config_format_time_map(): """ diff --git a/pygmt/tests/test_contour.py b/pygmt/tests/test_contour.py index ed0951836ce..770d90f5117 100644 --- a/pygmt/tests/test_contour.py +++ b/pygmt/tests/test_contour.py @@ -76,10 +76,13 @@ def test_contour_from_file(region): @pytest.mark.mpl_image_compare(filename="test_contour_vec.png") -def test_contour_deprecate_columns_to_incols(region): +def test_contour_incols_transposed_data(region): """ - Make sure that the old parameter "columns" is supported and it reports an - warning. + Make sure that transposing the data matrix still produces a correct result + with incols reordering the columns. + + This is a regression test for + https://github.com/GenericMappingTools/pygmt/issues/1313 Modified from the test_contour_vec() test. """ @@ -96,14 +99,12 @@ def test_contour_deprecate_columns_to_incols(region): # switch x and y from here onwards to simulate different column order data = np.array([y, x, z]).T - with pytest.warns(expected_warning=FutureWarning) as record: - fig.contour( - data, - projection="X10c", - region=region, - frame="a", - pen=True, - columns=[1, 0, 2], - ) - assert len(record) == 1 # check that only one warning was raised + fig.contour( + data, + projection="X10c", + region=region, + frame="a", + pen=True, + incols=[1, 0, 2], + ) return fig diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index 05dcb467c0e..2d5949eaddb 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -138,7 +138,8 @@ def test_figure_savefig_filename_with_spaces(): fig = Figure() fig.basemap(region=[0, 1, 0, 1], projection="X1c/1c", frame=True) with GMTTempFile(prefix="pygmt-filename with spaces", suffix=".png") as imgfile: - fig.savefig(imgfile.name) + fig.savefig(fname=imgfile.name) + assert r"\040" not in os.path.abspath(imgfile.name) assert os.path.exists(imgfile.name) diff --git a/pygmt/tests/test_grdhisteq.py b/pygmt/tests/test_grdhisteq.py index 9a4fd004b3d..a812e0d5565 100644 --- a/pygmt/tests/test_grdhisteq.py +++ b/pygmt/tests/test_grdhisteq.py @@ -66,13 +66,12 @@ def test_equalize_grid_outgrid_file(grid, expected_grid, region): xr.testing.assert_allclose(a=temp_grid, b=expected_grid) -@pytest.mark.parametrize("outgrid", [True, None]) -def test_equalize_grid_outgrid(grid, outgrid, expected_grid, region): +def test_equalize_grid_outgrid(grid, expected_grid, region): """ - Test grdhisteq.equalize_grid with ``outgrid=True`` and ``outgrid=None``. + Test grdhisteq.equalize_grid with ``outgrid=None``. """ temp_grid = grdhisteq.equalize_grid( - grid=grid, divisions=2, region=region, outgrid=outgrid + grid=grid, divisions=2, region=region, outgrid=None ) assert temp_grid.gmt.gtype == 1 # Geographic grid assert temp_grid.gmt.registration == 1 # Pixel registration @@ -135,3 +134,11 @@ def test_compute_bins_invalid_format(grid): grdhisteq.compute_bins(grid=grid, output_type=1) with pytest.raises(GMTInvalidInput): grdhisteq.compute_bins(grid=grid, output_type="pandas", header="o+c") + + +def test_equalize_grid_invalid_format(grid): + """ + Test that equalize_grid fails with incorrect format. + """ + with pytest.raises(GMTInvalidInput): + grdhisteq.equalize_grid(grid=grid, outgrid=True) diff --git a/pygmt/tests/test_grdproject.py b/pygmt/tests/test_grdproject.py index 26b2aba86ab..6e90f061c34 100644 --- a/pygmt/tests/test_grdproject.py +++ b/pygmt/tests/test_grdproject.py @@ -58,13 +58,20 @@ def test_grdproject_file_out(grid, expected_grid): xr.testing.assert_allclose(a=temp_grid, b=expected_grid) -def test_grdproject_no_outgrid(grid, expected_grid): +@pytest.mark.parametrize( + "projection", + ["M10c", "EPSG:3395 +width=10", "+proj=merc +ellps=WGS84 +units=m +width=10"], +) +def test_grdproject_no_outgrid(grid, projection, expected_grid): """ Test grdproject with no set outgrid. + + Also check that providing the projection as an EPSG code or PROJ4 string + works. """ assert grid.gmt.gtype == 1 # Geographic grid result = grdproject( - grid=grid, projection="M10c", spacing=3, region=[-53, -51, -20, -17] + grid=grid, projection=projection, spacing=3, region=[-53, -51, -20, -17] ) assert result.gmt.gtype == 0 # Rectangular grid assert result.gmt.registration == 1 # Pixel registration diff --git a/pygmt/tests/test_legend.py b/pygmt/tests/test_legend.py index b93bafc401b..66eadd3fc68 100644 --- a/pygmt/tests/test_legend.py +++ b/pygmt/tests/test_legend.py @@ -56,7 +56,7 @@ def test_legend_entries(): pen="faint", label="Apples", ) - fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label='"My lines"') + fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines") fig.plot(data="@Table_5_11.txt", style="t0.15i", color="orange", label="Oranges") fig.legend(position="JTR+jTR") diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index f05872dac6b..8627c03eeb7 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -452,53 +452,6 @@ def test_plot_datetime(): return fig -@pytest.mark.mpl_image_compare(filename="test_plot_sizes.png") -def test_plot_deprecate_sizes_to_size(data, region): - """ - Make sure that the old parameter "sizes" is supported and it reports a - warning. - - Modified from the test_plot_sizes() test. - """ - fig = Figure() - with pytest.warns(expected_warning=FutureWarning) as record: - fig.plot( - x=data[:, 0], - y=data[:, 1], - sizes=0.5 * data[:, 2], - region=region, - projection="X10c", - style="cc", - color="blue", - frame="af", - ) - assert len(record) == 1 # check that only one warning was raised - return fig - - -@pytest.mark.mpl_image_compare(filename="test_plot_from_file.png") -def test_plot_deprecate_columns_to_incols(region): - """ - Make sure that the old parameter "columns" is supported and it reports a - warning. - - Modified from the test_plot_from_file() test. - """ - fig = Figure() - with pytest.warns(expected_warning=FutureWarning) as record: - fig.plot( - data=POINTS_DATA, - region=region, - projection="X10c", - style="d1c", - color="yellow", - frame=True, - columns=[0, 1], - ) - assert len(record) == 1 # check that only one warning was raised - return fig - - @pytest.mark.mpl_image_compare def test_plot_ogrgmt_file_multipoint_default_style(): """ diff --git a/pygmt/tests/test_plot3d.py b/pygmt/tests/test_plot3d.py index 033267e0724..1eda151946a 100644 --- a/pygmt/tests/test_plot3d.py +++ b/pygmt/tests/test_plot3d.py @@ -423,58 +423,6 @@ def test_plot3d_scalar_xyz(): return fig -@pytest.mark.mpl_image_compare(filename="test_plot3d_sizes.png") -def test_plot3d_deprecate_sizes_to_size(data, region): - """ - Make sure that the old parameter "sizes" is supported and it reports an - warning. - - Modified from the test_plot3d_sizes() test. - """ - fig = Figure() - with pytest.warns(expected_warning=FutureWarning) as record: - fig.plot3d( - x=data[:, 0], - y=data[:, 1], - z=data[:, 2], - zscale=5, - perspective=[225, 30], - sizes=0.5 * data[:, 2], - region=region, - projection="X10c", - style="ui", - color="blue", - frame=["af", "zaf"], - ) - assert len(record) == 1 # check that only one warning was raised - return fig - - -@pytest.mark.mpl_image_compare(filename="test_plot3d_matrix.png") -def test_plot3d_deprecate_columns_to_incols(data, region): - """ - Make sure that the old parameter "columns" is supported and it reports an - warning. - - Modified from the test_plot3d_matrix() test. - """ - fig = Figure() - with pytest.warns(expected_warning=FutureWarning) as record: - fig.plot3d( - data, - zscale=5, - perspective=[225, 30], - region=region, - projection="M20c", - style="c1c", - color="#aaaaaa", - frame=["a", "za"], - columns="0,1,2", - ) - assert len(record) == 1 # check that only one warning was raised - return fig - - @pytest.mark.mpl_image_compare def test_plot3d_ogrgmt_file_multipoint_default_style(): """ diff --git a/pygmt/tests/test_psconvert.py b/pygmt/tests/test_psconvert.py index af610cf86bb..a18b14883f2 100644 --- a/pygmt/tests/test_psconvert.py +++ b/pygmt/tests/test_psconvert.py @@ -3,7 +3,9 @@ """ import os +import pytest from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput def test_psconvert(): @@ -36,3 +38,12 @@ def test_psconvert_twice(): fname = prefix + ".png" assert os.path.exists(fname) os.remove(fname) + + +def test_psconvert_without_prefix(): + """ + Call psconvert without the 'prefix' option. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.psconvert(fmt="g") diff --git a/pygmt/tests/test_rose.py b/pygmt/tests/test_rose.py index 53fabb5e6fd..b330fb43a54 100644 --- a/pygmt/tests/test_rose.py +++ b/pygmt/tests/test_rose.py @@ -152,7 +152,7 @@ def test_rose_no_sectors(data_fractures_compilation): region=[0, 500, 0, 360], diameter="10c", labels="180/0/90/270", - frame=["xg100", "yg45", "+t'Windrose diagram'"], + frame=["xg100", "yg45", "+tWindrose diagram"], pen="1.5p,red3", transparency=40, scale=0.5, @@ -184,37 +184,3 @@ def test_rose_bools(data_fractures_compilation): shift=False, ) return fig - - -@pytest.mark.mpl_image_compare(filename="test_rose_bools.png") -def test_rose_deprecate_columns_to_incols(data_fractures_compilation): - """ - Make sure that the old parameter "columns" is supported and it reports a - warning. - - Modified from the test_rose_bools() test. - """ - - # swap data column order of the sample fractures compilation dataset, - # as the use of the 'columns' parameter will reverse this action - data = data_fractures_compilation[["azimuth", "length"]] - - fig = Figure() - with pytest.warns(expected_warning=FutureWarning) as record: - fig.rose( - data, - region=[0, 1, 0, 360], - sector=10, - columns=[1, 0], - diameter="10c", - frame=["x0.2g0.2", "y30g30", "+glightgray"], - color="red3", - pen="1p", - orientation=False, - norm=True, - vectors=True, - no_scale=True, - shift=False, - ) - assert len(record) == 1 # check that only one warning was raised - return fig diff --git a/pygmt/tests/test_triangulate.py b/pygmt/tests/test_triangulate.py new file mode 100644 index 00000000000..ec835f9316e --- /dev/null +++ b/pygmt/tests/test_triangulate.py @@ -0,0 +1,169 @@ +""" +Tests for triangulate. +""" +import os + +import numpy as np +import pandas as pd +import pytest +import xarray as xr +from pygmt import triangulate, which +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import GMTTempFile, data_kind + + +@pytest.fixture(scope="module", name="dataframe") +def fixture_dataframe(): + """ + Load the table data from the sample bathymetry dataset. + """ + fname = which("@Table_5_11_mean.xyz", download="c") + return pd.read_csv( + fname, sep=r"\s+", header=None, names=["x", "y", "z"], skiprows=1 + )[:10] + + +@pytest.fixture(scope="module", name="expected_dataframe") +def fixture_dataframe_result(): + """ + Load the expected triangulate dataframe result. + """ + return pd.DataFrame( + data=[ + [7, 8, 2], + [8, 7, 9], + [7, 1, 0], + [1, 7, 2], + [1, 2, 4], + [8, 3, 2], + [9, 5, 3], + [5, 9, 6], + [5, 4, 3], + [4, 5, 6], + [4, 6, 1], + [3, 4, 2], + [9, 3, 8], + ] + ) + + +@pytest.fixture(scope="module", name="expected_grid") +def fixture_grid_result(): + """ + Load the expected triangulate grid result. + """ + return xr.DataArray( + data=[[779.6264, 752.1539, 749.38776], [771.2882, 726.9792, 722.1368]], + coords=dict(y=[5, 6], x=[2, 3, 4]), + dims=["y", "x"], + ) + + +@pytest.mark.parametrize("array_func", [np.array, xr.Dataset]) +def test_delaunay_triples_input_table_matrix(array_func, dataframe, expected_dataframe): + """ + Run triangulate.delaunay_triples by passing in a numpy.array or + xarray.Dataset. + """ + table = array_func(dataframe) + output = triangulate.delaunay_triples(data=table) + pd.testing.assert_frame_equal(left=output, right=expected_dataframe) + + +def test_delaunay_triples_input_xyz(dataframe, expected_dataframe): + """ + Run triangulate.delaunay_triples by passing in x, y, z numpy.ndarrays + individually. + """ + output = triangulate.delaunay_triples(x=dataframe.x, y=dataframe.y, z=dataframe.z) + pd.testing.assert_frame_equal(left=output, right=expected_dataframe) + + +def test_delaunay_triples_input_xy_no_z(dataframe, expected_dataframe): + """ + Run triangulate.delaunay_triples by passing in x and y, but no z. + """ + output = triangulate.delaunay_triples(x=dataframe.x, y=dataframe.y) + pd.testing.assert_frame_equal(left=output, right=expected_dataframe) + + +def test_delaunay_triples_wrong_kind_of_input(dataframe): + """ + Run triangulate.delaunay_triples using grid input that is not + file/matrix/vectors. + """ + data = dataframe.z.to_xarray() # convert pandas.Series to xarray.DataArray + assert data_kind(data) == "grid" + with pytest.raises(GMTInvalidInput): + triangulate.delaunay_triples(data=data) + + +def test_delaunay_triples_ndarray_output(dataframe, expected_dataframe): + """ + Test triangulate.delaunay_triples with "numpy" output type. + """ + output = triangulate.delaunay_triples(data=dataframe, output_type="numpy") + assert isinstance(output, np.ndarray) + np.testing.assert_allclose(actual=output, desired=expected_dataframe.to_numpy()) + + +def test_delaunay_triples_outfile(dataframe, expected_dataframe): + """ + Test triangulate.delaunay_triples with ``outfile``. + """ + with GMTTempFile(suffix=".txt") as tmpfile: + with pytest.warns(RuntimeWarning) as record: + result = triangulate.delaunay_triples(data=dataframe, outfile=tmpfile.name) + assert len(record) == 1 # check that only one warning was raised + assert result is None # return value is None + assert os.path.exists(path=tmpfile.name) + temp_df = pd.read_csv(filepath_or_buffer=tmpfile.name, sep="\t", header=None) + pd.testing.assert_frame_equal(left=temp_df, right=expected_dataframe) + + +def test_delaunay_triples_invalid_format(dataframe): + """ + Test that triangulate.delaunay_triples fails with incorrect format. + """ + with pytest.raises(GMTInvalidInput): + triangulate.delaunay_triples(data=dataframe, output_type=1) + + +def test_regular_grid_no_outgrid(dataframe, expected_grid): + """ + Run triangulate.regular_grid with no set outgrid and see it load into an + xarray.DataArray. + """ + data = dataframe.to_numpy() + output = triangulate.regular_grid(data=data, spacing=1, region=[2, 4, 5, 6]) + assert isinstance(output, xr.DataArray) + assert output.gmt.registration == 0 # Gridline registration + assert output.gmt.gtype == 0 # Cartesian type + xr.testing.assert_allclose(a=output, b=expected_grid) + + +def test_regular_grid_with_outgrid_param(dataframe, expected_grid): + """ + Run triangulate.regular_grid with the -Goutputfile.nc parameter. + """ + data = dataframe.to_numpy() + with GMTTempFile(suffix=".nc") as tmpfile: + output = triangulate.regular_grid( + data=data, spacing=1, region=[2, 4, 5, 6], outgrid=tmpfile.name + ) + assert output is None # check that output is None since outgrid is set + assert os.path.exists(path=tmpfile.name) # check that outgrid exists + with xr.open_dataarray(tmpfile.name) as grid: + assert isinstance(grid, xr.DataArray) + assert grid.gmt.registration == 0 # Gridline registration + assert grid.gmt.gtype == 0 # Cartesian type + xr.testing.assert_allclose(a=grid, b=expected_grid) + + +def test_regular_grid_invalid_format(dataframe): + """ + Test that triangulate.regular_grid fails with outgrid that is not None or a + proper file name. + """ + with pytest.raises(GMTInvalidInput): + triangulate.regular_grid(data=dataframe, outgrid=True)