Repository: GenericMappingTools/pygmt

Issue:
xyz2grd wrapper
Hi, 

I tried to write a wrapper of the [xyz2grd function ](http://gmt.soest.hawaii.edu/doc/5.3.2/xyz2grd.html), in a similar way to the grdfilter wrapper:
I added the function to the gridops.py file, and call it in the __init__.py file. Still, the function doesn't appear when I try to call it. 
![image](https://user-images.githubusercontent.com/47150926/94416255-e8500f00-017e-11eb-8f86-1be204f3fd24.png)
Maybe it wasn't as simple as change the 'grdcut' to 'grdfilter', but I would expect it to recognise the function anyway, and then give me errors... 

**xyz2grd**
The function is used to transform a table/text file into a binary grid file. 

Right now I use <os> library to call gmt from python:

> os.chdir(path_to_xyzfile)
> res='60m'
> infile=flist[0]
> outfile='../nc/test.nc'
> command=str('gmt xyz2grd '+str(infile)+' -G'+str(outfile)+' -I'+str(res)+' -Rg') 
> os.system(command)


Any ideas of how I can solve it?

![image](https://user-images.githubusercontent.com/47150926/94417121-f94d5000-017f-11eb-9ede-1dda66100aa1.png)
![image](https://user-images.githubusercontent.com/47150926/94417164-04a07b80-0180-11eb-8eeb-44cfa121941c.png)
![image](https://user-images.githubusercontent.com/47150926/94417199-11bd6a80-0180-11eb-98fd-8f52206e6327.png)


Discussion:
weiji14: Sorry for the force-pushes, had to resolve a tricky merge conflict. If you run `git switch newfeature_xyz2grd` locally, and then `git pull`, this should bring in the resolved changes. It's getting late for me now, but I'll try and take a closer look at this next week.
carocamargo: I'm sorry I disappeared, I kind that forgot about this :X .. Do I still need to push the changes, or not anymore? 
seisman: @carocamargo Welcome back! Please click the "Update branch" button to merge the master branch into your branch first. Then you can keep working on it or ask us to review it if you think it's ready.
seisman: Ping @carocamargo to remind you working on this PR when you have time.
weiji14: > Sorry for the force-pushes, had to resolve a tricky merge conflict. If you run `git switch newfeature_xyz2grd` locally, and then `git pull`, this should bring in the resolved changes. It's getting late for me now, but I'll try and take a closer look at this next week.

Alright, if anyone is going to continue working on this, try doing `git pull` again. I've moved the `xyz2grd` function from pygmt/gridding.py to a standalone 'pygmt/src/xyz2grd.py' file (a recent change in PyGMT v0.3.0), and also started a really basic test file. Still a lot of work to do in terms of the code implementation but we'll get there!
willschlitzer: @carocamargo Mind if I add to this pull request to get it in for the v0.5.0 release?
willschlitzer: @weiji14 I'm trying to work on this but just ended up creating my own branch. I followed this guide but when I pushed it to GitHub it made its own branch. Any idea?

```
git checkout -b carocamargo-newfeature_xyz2grd master
git pull https://github.com/carocamargo/pygmt.git newfeature_xyz2grd
```

seisman: > @weiji14 I'm trying to work on this but just ended up creating my own branch. I followed this guide but when I pushed it to GitHub it made its own branch. Any idea?
> 
> ```
> git checkout -b carocamargo-newfeature_xyz2grd master
> git pull https://github.com/carocamargo/pygmt.git newfeature_xyz2grd
> ```

These are the steps given by github:
```
git remote add carocamargo https://github.com/carocamargo/pygmt.git
git fetch carocamargo newfeature_xyz2grd
git switch --track carocamargo/newfeature_xyz2grd
```
willschlitzer: Thanks @seisman!


carocamargo: Hi, sorry for the total disappearance.. is there anything I need to do to allow the PR? 
Thanks @willschlitzer and @seisman for keeping this feature alive
willschlitzer: @carocamargo I'm not too savvy with git, so I'm not sure how I can commit directly to this branch to finish up the pull request. @GenericMappingTools/pygmt-maintainers any reason I wouldn't be able to commit to this branch?
meghanrjones: Is the 'allow edits from maintainers' box checked? Here's a link to the relevant docs: https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork
willschlitzer: Yes, I assume I should be able to do it as @weiji14 had previously made commits, but I haven't been able to push to the GitHub repository
meghanrjones: > Yes, I assume I should be able to do it as @weiji14 had previously made commits, but I haven't been able to push to the GitHub repository

What error message do you get? If you used these steps (`git checkout -b carocamargo-newfeature_xyz2grd master;
git pull https://github.com/carocamargo/pygmt.git newfeature_xyz2grd`), I think you will have a different git history than carocamargo's branch and will not be able to push commits. Did you try using the steps that Dongdong posted (reposted below)? If you did already and want me to see if the problem is reproducible, I could try pushing a simple change (like updating the spacing parameter to use the common version). But, depending on what work you've done this could create a merge conflict.

```
git remote add carocamargo https://github.com/carocamargo/pygmt.git
git fetch carocamargo newfeature_xyz2grd
git switch --track carocamargo/newfeature_xyz2grd
```
willschlitzer: I tried deleting my branch and starting from scratch; I'm still getting "fatal: Authentication failed for 'https://github.com/carocamargo/pygmt.git/'"
weiji14: > I tried deleting my branch and starting from scratch; I'm still getting "fatal: Authentication failed for 'https://github.com/carocamargo/pygmt.git/'" I had originally thought the issue was that I created a conflict in the git history and my best bet was to start over, but clearly still having issues. @meghanrjones feel free to try and commit to it, I haven't made any big changes on my local machine that I can't redo.

Could you try using the ssh URL `git@github.com:carocamargo/pygmt.git` instead? You might need to set up authentication somehow following https://stackoverflow.com/questions/17659206/git-push-results-in-authentication-failed.
willschlitzer: I don't know what I did correctly this time, but it looks like I'm now able to push to this PR!
willschlitzer: I removed some of the optional parameters to simplify this PR. I figure it's easier to get it merged and then add additional parameters in separate pull requests.
meghanrjones: > I removed some of the optional parameters to simplify this PR. I figure it's easier to get it merged and then add additional parameters in separate pull requests.

Thanks for working on this! Sorry for the slowness in reviewing your PRs, they have been lower on my to-do list since we are at a feature pause until v0.4.1. I will take a look after the release.
willschlitzer: > 
> Thanks for working on this! Sorry for the slowness in reviewing your PRs, they have been lower on my to-do list since we are at a feature pause until v0.4.1. I will take a look after the release.

No worries, I expected a v0.4.1, but looking forward to getting them wrapped up!


Patch:
diff --git a/doc/api/index.rst b/doc/api/index.rst
index 40ab49ac..536c9ced 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -95,6 +95,7 @@ Operations on grids:
     grdlandmask
     grdgradient
     grdtrack
+    xyz2grd
 
 Crossover analysis with x2sys:
 
diff --git a/pygmt/__init__.py b/pygmt/__init__.py
index 972f6474..f25e8a7e 100644
--- a/pygmt/__init__.py
+++ b/pygmt/__init__.py
@@ -47,6 +47,7 @@ from pygmt.src import (
     which,
     x2sys_cross,
     x2sys_init,
+    xyz2grd,
 )
 
 # Get semantic version through setuptools-scm
diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py
index f8bae667..a2ced5db 100644
--- a/pygmt/src/__init__.py
+++ b/pygmt/src/__init__.py
@@ -41,3 +41,4 @@ from pygmt.src.which import which
 from pygmt.src.wiggle import wiggle
 from pygmt.src.x2sys_cross import x2sys_cross
 from pygmt.src.x2sys_init import x2sys_init
+from pygmt.src.xyz2grd import xyz2grd
diff --git a/pygmt/src/xyz2grd.py b/pygmt/src/xyz2grd.py
new file mode 100644
index 00000000..ef40abc8
--- /dev/null
+++ b/pygmt/src/xyz2grd.py
@@ -0,0 +1,74 @@
+"""
+xyz2grd - Convert data table to a grid.
+"""
+import xarray as xr
+from pygmt.clib import Session
+from pygmt.helpers import (
+    GMTTempFile,
+    build_arg_string,
+    fmt_docstring,
+    kwargs_to_strings,
+    use_alias,
+)
+
+
+@fmt_docstring
+@use_alias(
+    G="outgrid",
+    I="spacing",
+    R="region",
+    V="verbose",
+)
+@kwargs_to_strings(R="sequence")
+def xyz2grd(table, **kwargs):
+    """
+    Create a grid file from table data.
+
+    xyz2grd reads one or more z or xyz tables and creates a binary grid file.
+    xyz2grd will report if some of the nodes are not filled in with data. Such
+    unconstrained nodes are set to a value specified by the user [Default is
+    NaN]. Nodes with more than one value will be set to the mean value.
+
+    Full option list at :gmt-docs:`xyz2grd.html`
+
+    Parameters
+    ----------
+    table : str or {table-like}
+        Pass in either a file name to an ASCII data table, a 1D/2D
+        {table-classes}.
+
+    outgrid : str or None
+        Optional. The name of the output netCDF file with extension .nc to
+        store the grid in.
+    {I}
+    {R}
+    {V}
+
+    Returns
+    -------
+    ret: xarray.DataArray or None
+        Return type depends on whether the ``outgrid`` parameter is set:
+
+        - :class:`xarray.DataArray`: if ``outgrid`` is not set
+        - None if ``outgrid`` is set (grid output will be stored in file set by
+          ``outgrid``)```
+    """
+    with GMTTempFile(suffix=".nc") as tmpfile:
+        with Session() as lib:
+            file_context = lib.virtualfile_from_data(check_kind="vector", data=table)
+            with file_context as infile:
+                if "G" not in kwargs.keys():  # if outgrid is unset, output to tempfile
+                    kwargs.update({"G": tmpfile.name})
+                outgrid = kwargs["G"]
+                arg_str = build_arg_string(kwargs)
+                arg_str = " ".join([infile, arg_str])
+                lib.call_module("xyz2grd", arg_str)
+
+        if outgrid == tmpfile.name:  # if user did not set outgrid, return DataArray
+            with xr.open_dataarray(outgrid) as dataarray:
+                result = dataarray.load()
+                _ = result.gmt  # load GMTDataArray accessor information
+        else:
+            result = None  # if user sets an outgrid, return None
+
+        return result


Test patch:
diff --git a/pygmt/tests/test_xyz2grd.py b/pygmt/tests/test_xyz2grd.py
new file mode 100644
index 00000000..530c6b09
--- /dev/null
+++ b/pygmt/tests/test_xyz2grd.py
@@ -0,0 +1,69 @@
+"""
+Tests for xyz2grd.
+"""
+import os
+
+import numpy as np
+import pytest
+import xarray as xr
+from pygmt import grdinfo, xyz2grd
+from pygmt.datasets import load_sample_bathymetry
+from pygmt.helpers import GMTTempFile
+
+
+@pytest.fixture(scope="module", name="ship_data")
+def fixture_ship_data():
+    """
+    Load the data from the sample bathymetry dataset.
+    """
+    return load_sample_bathymetry()
+
+
+def test_xyz2grd_input_file():
+    """
+    Run xyz2grd by passing in a filename.
+    """
+    output = xyz2grd(table="@tut_ship.xyz", spacing=5, region=[245, 255, 20, 30])
+    assert isinstance(output, xr.DataArray)
+    assert output.gmt.registration == 0  # Gridline registration
+    assert output.gmt.gtype == 0  # Cartesian type
+    return output
+
+
+def test_xyz2grd_input_array(ship_data):
+    """
+    Run xyz2grd by passing in a numpy array.
+    """
+    output = xyz2grd(table=np.array(ship_data), spacing=5, region=[245, 255, 20, 30])
+    assert isinstance(output, xr.DataArray)
+    assert output.gmt.registration == 0  # Gridline registration
+    assert output.gmt.gtype == 0  # Cartesian type
+    return output
+
+
+def test_xyz2grd_input_df(ship_data):
+    """
+    Run xyz2grd by passing in a data frame.
+    """
+    output = xyz2grd(table=ship_data, spacing=5, region=[245, 255, 20, 30])
+    assert isinstance(output, xr.DataArray)
+    assert output.gmt.registration == 0  # Gridline registration
+    assert output.gmt.gtype == 0  # Cartesian type
+    return output
+
+
+def test_xyz2grd_input_array_file_out(ship_data):
+    """
+    Run xyz2grd by passing in a numpy array and set an outgrid file.
+    """
+    with GMTTempFile(suffix=".nc") as tmpfile:
+        result = xyz2grd(
+            table=np.array(ship_data),
+            spacing=5,
+            region=[245, 255, 20, 30],
+            outgrid=tmpfile.name,
+        )
+        assert result is None  # return value is None
+        assert os.path.exists(path=tmpfile.name)
+        result = grdinfo(tmpfile.name, per_column=True).strip()
+        assert result == "245 255 20 30 -3651.06079102 -352.379486084 5 5 3 3 0 0"