Repository: fatiando/verde
Pull request #43 (2018-05-19 20:26:02)

Use 'coordinates' instead of individual arrays
All fit and predict methods have been changed to use a `coordinates`
tuple instead of `easting, northing`. This will allow the classes to be
used in more generic methods that take in more than 2 coordinates, like
equivalent layers.

Move the `base` subpackage back to a module and import `BaseGridder` in
the main API.

Needed for #42



Patch:
diff --git a/doc/api/_generate_api.rst b/doc/api/_generate_api.rst
index f1cfce1..99547c3 100644
--- a/doc/api/_generate_api.rst
+++ b/doc/api/_generate_api.rst
@@ -12,4 +12,3 @@ To include a new package/module, list it below and include it in api.index.rst
 
    verde
    verde.datasets
-   verde.base
diff --git a/doc/api/index.rst b/doc/api/index.rst
index 02e3bbf..eb75b95 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -5,4 +5,3 @@ API Reference
 
 .. include:: verde.rst
 .. include:: verde.datasets.rst
-.. include:: verde.base.rst
diff --git a/examples/data/synthetic_checkerboard.py b/examples/data/synthetic_checkerboard.py
index c8786bd..9ecde50 100644
--- a/examples/data/synthetic_checkerboard.py
+++ b/examples/data/synthetic_checkerboard.py
@@ -17,8 +17,8 @@ print("region:", synth.region_)
 print("wavelengths (east, north):", synth.w_east, synth.w_north)
 
 # The CheckerBoard class behaves like any gridder class
-print("Checkerboard value at (2000, -2500):",
-      synth.predict(easting=2000, northing=-2500))
+print("Checkerboard value at (easting=2000, northing=-2500):",
+      synth.predict((2000, -2500)))
 
 # Generating a grid results in a xarray.Dataset
 grid = synth.grid()
diff --git a/examples/scipygridder.py b/examples/scipygridder.py
index 4531d15..d04e8b2 100644
--- a/examples/scipygridder.py
+++ b/examples/scipygridder.py
@@ -38,10 +38,10 @@ lon, lat, bathymetry = vd.block_reduce(data.longitude, data.latitude,
 # Project the data using pyproj so that we can use it as input for the gridder.
 # We'll set the latitude of true scale to the mean latitude of the data.
 projection = pyproj.Proj(proj='merc', lat_ts=data.latitude.mean())
-easting, northing = projection(lon, lat)
+coordinates = projection(lon, lat)
 
 # Now we can set up a gridder for the decimated data
-grd = vd.ScipyGridder(method='cubic').fit(easting, northing, bathymetry)
+grd = vd.ScipyGridder(method='cubic').fit(coordinates, bathymetry)
 print("Gridder used:", grd)
 
 # Get the grid region in geographic coordinates
diff --git a/examples/trend.py b/examples/trend.py
index 15347e3..70d104a 100644
--- a/examples/trend.py
+++ b/examples/trend.py
@@ -22,12 +22,12 @@ print("Original data:")
 print(data.head())
 
 # Fit a 2nd degree 2D polynomial to the anomaly data
-trend = vd.Trend(degree=2).fit(data.longitude, data.latitude,
-                               data.total_field_anomaly_nt)
+coordinates = (data.longitude, data.latitude)
+trend = vd.Trend(degree=2).fit(coordinates, data.total_field_anomaly_nt)
 print("\nTrend estimator:", trend)
 
 # Add the estimated trend and the residual data to the DataFrame
-data['trend'] = trend.predict(data.longitude, data.latitude)
+data['trend'] = trend.predict(coordinates)
 data['residual'] = trend.residual_
 print("\nUpdated DataFrame:")
 print(data.head())
diff --git a/verde/__init__.py b/verde/__init__.py
index 45b22c7..40b9910 100644
--- a/verde/__init__.py
+++ b/verde/__init__.py
@@ -5,6 +5,7 @@ from ._version import get_versions as _get_versions
 
 # Import functions/classes to make the API
 from . import datasets
+from .base import BaseGridder
 from .coordinates import scatter_points, grid_coordinates, inside, \
     profile_coordinates, block_region, get_region
 from .grid_math import block_reduce
diff --git a/verde/base/gridder.py b/verde/base.py
similarity index 90%
rename from verde/base/gridder.py
rename to verde/base.py
index 2a9f98b..2d4c70b 100644
--- a/verde/base/gridder.py
+++ b/verde/base.py
@@ -5,112 +5,16 @@ import xarray as xr
 import pandas as pd
 from sklearn.base import BaseEstimator
 
-from ..coordinates import grid_coordinates, profile_coordinates, scatter_points
-
-
-def get_dims(instance, dims):
-    """
-    Get default dimension names for an instance if not given as arguments.
-
-    Examples
-    --------
-
-    >>> grd = BaseGridder()
-    >>> get_dims(grd, dims=None)
-    ('northing', 'easting')
-    >>> grd.coordinate_system = 'geographic'
-    >>> get_dims(grd, dims=None)
-    ('latitude', 'longitude')
-    >>> grd.coordinate_system = 'cartesian'
-    >>> get_dims(grd, dims=('john', 'paul'))
-    ('john', 'paul')
-
-    """
-    if dims is not None:
-        return dims
-    valid_coords = ['cartesian', 'geographic']
-    coords = getattr(instance, 'coordinate_system', 'cartesian')
-    if coords not in valid_coords:
-        raise ValueError(
-            "Invalid coordinate system for {}: '{}'. Must be one of {}."
-            .format(instance.__class__.__name__, coords, str(valid_coords)))
-    if coords == 'geographic':
-        return ('latitude', 'longitude')
-    return ('northing', 'easting')
-
-
-def get_data_names(instance, data_names):
-    """
-    Get default names for data fields for an instance if not given as arguments
-
-    Examples
-    --------
-
-    >>> grd = BaseGridder()
-    >>> get_data_names(grd, data_names=None)
-    ('scalars',)
-    >>> grd.data_type = 'vector2d'
-    >>> get_data_names(grd, data_names=None)
-    ('east_component', 'north_component')
-    >>> grd.data_type = 'vector3d'
-    >>> get_data_names(grd, data_names=None)
-    ('east_component', 'north_component', 'vertical_component')
-    >>> get_data_names(grd, data_names=('ringo', 'george'))
-    ('ringo', 'george')
-
-    """
-    if data_names is not None:
-        return data_names
-    valid_types = ['scalar', 'vector2d', 'vector3d']
-    data_type = getattr(instance, 'data_type', 'scalar')
-    if data_type not in valid_types:
-        raise ValueError(
-            "Invalid data type for {}: '{}'. Must be one of {}."
-            .format(instance.__class__.__name__, data_type, str(valid_types)))
-    if data_type == 'vector2d':
-        return ('east_component', 'north_component')
-    elif data_type == 'vector3d':
-        return ('east_component', 'north_component', 'vertical_component')
-    return ('scalars',)
-
-
-def get_region(instance, region):
-    """
-    Get the region attribute stored in instance if one is not provided.
-    """
-    if region is None:
-        if not hasattr(instance, 'region_'):
-            raise ValueError(
-                "No default region found. Argument must be supplied.")
-        region = getattr(instance, 'region_')
-    return region
-
-
-def check_data(data):
-    """
-    Check the data returned by predict.
-    If the data is a single array, return it as a tuple with a single element.
-
-    Examples
-    --------
-
-    >>> check_data([1, 2, 3])
-    ([1, 2, 3],)
-    >>> check_data(([1, 2], [3, 4]))
-    ([1, 2], [3, 4])
-    """
-    if not isinstance(data, tuple):
-        data = (data,)
-    return data
+from .coordinates import grid_coordinates, profile_coordinates, scatter_points
 
 
 class BaseGridder(BaseEstimator):
     """
     Base class for gridders.
 
-    Requires the implementation of the ``predict(easting, northing)`` method.
-    The data returned by it should be a 1d or 2d numpy array for scalar data or
-    a tuple with 1d or 2d numpy arrays for each component of vector data.
+    Requires the implementation of the ``predict(coordiantes)`` method. The
+    data returned by it should be a 1d or 2d numpy array for scalar data or a
+    tuple with 1d or 2d numpy arrays for each component of vector data.
 
     Doesn't define any new attributes.
 
@@ -147,7 +51,7 @@ class BaseGridder(BaseEstimator):
     ...     def __init__(self, multiplier=1):
     ...         # Init should only assign the parameters to attributes
     ...         self.multiplier = multiplier
-    ...     def fit(self, easting, northing, data):
+    ...     def fit(self, coordiantes, data):
     ...         # Argument checking should be done in fit
     ...         if self.multiplier <= 0:
     ...             raise ValueError('Invalid multiplier {}'
@@ -155,17 +59,17 @@ class BaseGridder(BaseEstimator):
     ...         self.mean_ = data.mean()*self.multiplier
     ...         # fit should return self so that we can chain operations
     ...         return self
-    ...     def predict(self, easting, northing):
+    ...     def predict(self, coordinates):
     ...         # We know the gridder has been fitted if it has the mean
     ...         check_is_fitted(self, ['mean_'])
-    ...         return np.ones_like(easting)*self.mean_
+    ...         return np.ones_like(coordinates[0])*self.mean_
     >>> # Try it on some synthetic data
     >>> synthetic = vd.datasets.CheckerBoard().fit(region=(0, 5, -10, 8))
     >>> data = synthetic.scatter()
     >>> print('{:.4f}'.format(data.scalars.mean()))
     -32.2182
     >>> # Fit the gridder to our synthetic data
-    >>> grd = MeanGridder().fit(data.easting, data.northing, data.scalars)
+    >>> grd = MeanGridder().fit((data.easting, data.northing), data.scalars)
     >>> grd
     MeanGridder(multiplier=1)
     >>> # Interpolate on a regular grid
@@ -183,10 +87,9 @@ class BaseGridder(BaseEstimator):
     >>> print(', '.join(['{:.1f}'.format(i) for i in profile.scalars]))
     -32.2, -32.2, -32.2, -32.2, -32.2, -32.2, -32.2, -32.2, -32.2, -32.2
 
-
     """
 
-    def predict(self, easting, northing):
+    def predict(self, coordinates):
         """
         Interpolate data on the given set of points.
 
@@ -194,10 +97,9 @@ class BaseGridder(BaseEstimator):
 
         Parameters
         ----------
-        easting : array
-            The values of the West-East coordinates of each data point.
-        northing : array
-            The values of the South-North coordinates of each data point.
+        coordinates : tuple of arrays
+            Arrays with the coordinates of each computation point. Should be in
+            the following order: (easting, northing, vertical, ...)
 
         Returns
         -------
@@ -276,13 +178,13 @@ class BaseGridder(BaseEstimator):
         dims = get_dims(self, dims)
         data_names = get_data_names(self, data_names)
         region = get_region(self, region)
-        easting, northing = grid_coordinates(region, shape=shape,
-                                             spacing=spacing, adjust=adjust)
+        coordinates = grid_coordinates(region, shape=shape, spacing=spacing,
+                                       adjust=adjust)
         if projection is None:
-            data = check_data(self.predict(easting, northing))
+            data = check_data(self.predict(coordinates))
         else:
-            data = check_data(self.predict(*projection(easting, northing)))
-        coords = {dims[1]: easting[0, :], dims[0]: northing[:, 0]}
+            data = check_data(self.predict(projection(*coordinates)))
+        coords = {dims[1]: coordinates[0][0, :], dims[0]: coordinates[1][:, 0]}
         attrs = {'metadata': 'Generated by {}'.format(repr(self))}
         data_vars = {name: (dims, value, attrs)
                      for name, value in zip(data_names, data)}
@@ -333,12 +235,12 @@ class BaseGridder(BaseEstimator):
         dims = get_dims(self, dims)
         data_names = get_data_names(self, data_names)
         region = get_region(self, region)
-        east, north = scatter_points(region, size, random_state)
+        coordinates = scatter_points(region, size, random_state)
         if projection is None:
-            data = check_data(self.predict(east, north))
+            data = check_data(self.predict(coordinates))
         else:
-            data = check_data(self.predict(*projection(east, north)))
-        columns = [(dims[0], north), (dims[1], east)]
+            data = check_data(self.predict(projection(*coordinates)))
+        columns = [(dims[0], coordinates[1]), (dims[1], coordinates[0])]
         columns.extend(zip(data_names, data))
         return pd.DataFrame(dict(columns), columns=[c[0] for c in columns])
 
@@ -391,9 +293,105 @@ class BaseGridder(BaseEstimator):
         east, north, distances = profile_coordinates(
             point1, point2, size, coordinate_system=coordsys)
         if projection is None:
-            data = check_data(self.predict(east, north))
+            data = check_data(self.predict((east, north)))
         else:
-            data = check_data(self.predict(*projection(east, north)))
+            data = check_data(self.predict(projection(east, north)))
         columns = [(dims[0], north), (dims[1], east), ('distance', distances)]
         columns.extend(zip(data_names, data))
         return pd.DataFrame(dict(columns), columns=[c[0] for c in columns])
+
+
+def get_dims(instance, dims):
+    """
+    Get default dimension names for an instance if not given as arguments.
+
+    Examples
+    --------
+
+    >>> grd = BaseGridder()
+    >>> get_dims(grd, dims=None)
+    ('northing', 'easting')
+    >>> grd.coordinate_system = 'geographic'
+    >>> get_dims(grd, dims=None)
+    ('latitude', 'longitude')
+    >>> grd.coordinate_system = 'cartesian'
+    >>> get_dims(grd, dims=('john', 'paul'))
+    ('john', 'paul')
+
+    """
+    if dims is not None:
+        return dims
+    valid_coords = ['cartesian', 'geographic']
+    coords = getattr(instance, 'coordinate_system', 'cartesian')
+    if coords not in valid_coords:
+        raise ValueError(
+            "Invalid coordinate system for {}: '{}'. Must be one of {}."
+            .format(instance.__class__.__name__, coords, str(valid_coords)))
+    if coords == 'geographic':
+        return ('latitude', 'longitude')
+    return ('northing', 'easting')
+
+
+def get_data_names(instance, data_names):
+    """
+    Get default names for data fields for an instance if not given as arguments
+
+    Examples
+    --------
+
+    >>> grd = BaseGridder()
+    >>> get_data_names(grd, data_names=None)
+    ('scalars',)
+    >>> grd.data_type = 'vector2d'
+    >>> get_data_names(grd, data_names=None)
+    ('east_component', 'north_component')
+    >>> grd.data_type = 'vector3d'
+    >>> get_data_names(grd, data_names=None)
+    ('east_component', 'north_component', 'vertical_component')
+    >>> get_data_names(grd, data_names=('ringo', 'george'))
+    ('ringo', 'george')
+
+    """
+    if data_names is not None:
+        return data_names
+    valid_types = ['scalar', 'vector2d', 'vector3d']
+    data_type = getattr(instance, 'data_type', 'scalar')
+    if data_type not in valid_types:
+        raise ValueError(
+            "Invalid data type for {}: '{}'. Must be one of {}."
+            .format(instance.__class__.__name__, data_type, str(valid_types)))
+    if data_type == 'vector2d':
+        return ('east_component', 'north_component')
+    elif data_type == 'vector3d':
+        return ('east_component', 'north_component', 'vertical_component')
+    return ('scalars',)
+
+
+def get_region(instance, region):
+    """
+    Get the region attribute stored in instance if one is not provided.
+    """
+    if region is None:
+        if not hasattr(instance, 'region_'):
+            raise ValueError(
+                "No default region found. Argument must be supplied.")
+        region = getattr(instance, 'region_')
+    return region
+
+
+def check_data(data):
+    """
+    Check the data returned by predict.
+    If the data is a single array, return it as a tuple with a single element.
+
+    Examples
+    --------
+
+    >>> check_data([1, 2, 3])
+    ([1, 2, 3],)
+    >>> check_data(([1, 2], [3, 4]))
+    ([1, 2], [3, 4])
+    """
+    if not isinstance(data, tuple):
+        data = (data,)
+    return data
diff --git a/verde/base/__init__.py b/verde/base/__init__.py
deleted file mode 100644
index 0c2d96e..0000000
--- a/verde/base/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""
-Base classes for all gridders.
-"""
-from .gridder import BaseGridder
diff --git a/verde/datasets/synthetic.py b/verde/datasets/synthetic.py
index 1ef78d9..cbcc187 100644
--- a/verde/datasets/synthetic.py
+++ b/verde/datasets/synthetic.py
@@ -4,7 +4,7 @@ Generators of synthetic datasets.
 import numpy as np
 from sklearn.utils.validation import check_is_fitted
 
-from ..base.gridder import BaseGridder
+from ..base import BaseGridder
 from ..coordinates import check_region
 
 
@@ -143,16 +143,17 @@ class CheckerBoard(BaseGridder):
         self.region_ = region
         return self
 
-    def predict(self, easting, northing):
+    def predict(self, coordinates):
         """
         Evaluate the checkerboard function on a given set of points.
 
         Parameters
         ----------
-        easting : array
-            The values of the West-East coordinates of each data point.
-        northing : array
-            The values of the South-North coordinates of each data point.
+        coordinates : tuple of arrays
+            Arrays with the coordinates of each data point. Should be in the
+            following order: (easting, northing, vertical, ...). Only easting
+            and northing will be used, all subsequent coordinates will be
+            ignored.
 
         Returns
         -------
@@ -161,6 +162,7 @@ class CheckerBoard(BaseGridder):
 
         """
         check_is_fitted(self, ['region_'])
+        easting, northing = coordinates[:2]
         data = (self.amplitude *
                 np.sin((2*np.pi/self.w_east)*easting) *
                 np.cos((2*np.pi/self.w_north)*northing))
diff --git a/verde/scipy_bridge.py b/verde/scipy_bridge.py
index 9a43a38..c6ee745 100644
--- a/verde/scipy_bridge.py
+++ b/verde/scipy_bridge.py
@@ -45,7 +45,7 @@ class ScipyGridder(BaseGridder):
         self.method = method
         self.extra_args = extra_args
 
-    def fit(self, easting, northing, data):
+    def fit(self, coordinates, data):
         """
         Fit the interpolator to the given data.
 
@@ -58,10 +58,11 @@ class ScipyGridder(BaseGridder):
 
         Parameters
         ----------
-        easting : array
-            The values of the West-East coordinates of each data point.
-        northing : array
-            The values of the South-North coordinates of each data point.
+        coordinates : tuple of arrays
+            Arrays with the coordinates of each data point. Should be in the
+            following order: (easting, northing, vertical, ...). Only easting
+            and northing will be used, all subsequent coordinates will be
+            ignored.
         data : array
             The data values that will be interpolated.
 
@@ -82,13 +83,14 @@ class ScipyGridder(BaseGridder):
             kwargs = {}
         else:
             kwargs = self.extra_args
+        easting, northing = coordinates[:2]
         self.region_ = get_region(easting, northing)
         points = np.column_stack((np.ravel(easting), np.ravel(northing)))
         self.interpolator_ = classes[self.method](points, np.ravel(data),
                                                   **kwargs)
         return self
 
-    def predict(self, easting, northing):
+    def predict(self, coordinates):
         """
         Interpolate data on the given set of points.
 
@@ -96,10 +98,11 @@ class ScipyGridder(BaseGridder):
 
         Parameters
         ----------
-        easting : array
-            The values of the West-East coordinates of each data point.
-        northing : array
-            The values of the South-North coordinates of each data point.
+        coordinates : tuple of arrays
+            Arrays with the coordinates of each data point. Should be in the
+            following order: (easting, northing, vertical, ...). Only easting
+            and northing will be used, all subsequent coordinates will be
+            ignored.
 
         Returns
         -------
@@ -108,4 +111,4 @@ class ScipyGridder(BaseGridder):
 
         """
         check_is_fitted(self, ['interpolator_'])
-        return self.interpolator_((easting, northing))
+        return self.interpolator_(coordinates[:2])
diff --git a/verde/trend.py b/verde/trend.py
index c60744a..e589dc6 100644
--- a/verde/trend.py
+++ b/verde/trend.py
@@ -6,7 +6,7 @@ from sklearn.utils.validation import check_is_fitted
 from sklearn.preprocessing import StandardScaler
 from sklearn.linear_model import LinearRegression
 
-from .base.gridder import BaseGridder
+from .base import BaseGridder
 from .coordinates import get_region
 
 
@@ -30,13 +30,13 @@ class Trend(BaseGridder):
     --------
 
     >>> from verde import grid_coordinates
-    >>> east, north = grid_coordinates((1, 5, -5, -1), shape=(5, 5))
-    >>> data = 10 + 2*east - 0.4*north
-    >>> trend = Trend(degree=1).fit(east, north, data)
+    >>> coordinates = grid_coordinates((1, 5, -5, -1), shape=(5, 5))
+    >>> data = 10 + 2*coordinates[0] - 0.4*coordinates[1]
+    >>> trend = Trend(degree=1).fit(coordinates, data)
     >>> print(', '.join(['{:.1f}'.format(i) for i in trend.coef_]))
     10.0, 2.0, -0.4
     >>> import numpy as np
-    >>> np.allclose(trend.predict(east, north), data)
+    >>> np.allclose(trend.predict(coordinates), data)
     True
     >>> np.allclose(trend.residual_, 0, atol=1e-5)
     True
@@ -45,7 +45,7 @@ class Trend(BaseGridder):
     >>> data_out[2, 2] += 500
     >>> weights = np.ones_like(data)
     >>> weights[2, 2] = 1e-10
-    >>> trend_out = Trend(degree=1).fit(east, north, data_out, weights)
+    >>> trend_out = Trend(degree=1).fit(coordinates, data_out, weights)
     >>> print(', '.join(['{:.1f}'.format(i) for i in trend_out.coef_]))
     10.0, 2.0, -0.4
     >>> print('{:.2f}'.format(trend_out.residual_[2, 2]))
@@ -56,7 +56,7 @@ class Trend(BaseGridder):
     def __init__(self, degree):
         self.degree = degree
 
-    def fit(self, easting, northing, data, weights=None):
+    def fit(self, coordinates, data, weights=None):
         """
         Fit the trend to the given data.
 
@@ -67,10 +67,11 @@ class Trend(BaseGridder):
 
         Parameters
         ----------
-        easting : array
-            The values of the West-East coordinates of each data point.
-        northing : array
-            The values of the South-North coordinates of each data point.
+        coordinates : tuple of arrays
+            Arrays with the coordinates of each data point. Should be in the
+            following order: (easting, northing, vertical, ...). Only easting
+            and northing will be used, all subsequent coordinates will be
+            ignored.
         data : array
             The data values of each data point.
         weights : None or array
@@ -83,6 +84,7 @@ class Trend(BaseGridder):
             Returns this estimator instance for chaining operations.
 
         """
+        easting, northing = coordinates[:2]
         if easting.shape != northing.shape or easting.shape != data.shape:
             raise ValueError(
                 "Coordinate and data arrays must have the same shape.")
@@ -102,7 +104,7 @@ class Trend(BaseGridder):
         self.coef_ = regr.coef_/scaler.scale_
         return self
 
-    def predict(self, easting, northing):
+    def predict(self, coordinates):
         """
         Evaluate the polynomial trend on the given set of points.
 
@@ -110,10 +112,11 @@ class Trend(BaseGridder):
 
         Parameters
         ----------
-        easting : array
-            The values of the West-East coordinates of each data point.
-        northing : array
-            The values of the South-North coordinates of each data point.
+        coordinates : tuple of arrays
+            Arrays with the coordinates of each data point. Should be in the
+            following order: (easting, northing, vertical, ...). Only easting
+            and northing will be used, all subsequent coordinates will be
+            ignored.
 
         Returns
         -------
@@ -122,6 +125,7 @@ class Trend(BaseGridder):
 
         """
         check_is_fitted(self, ['coef_'])
+        easting, northing = coordinates[:2]
         jac = trend_jacobian(easting, northing, degree=self.degree)
         shape = np.broadcast(easting, northing).shape
         return jac.dot(self.coef_).reshape(shape)


Test patch:
diff --git a/verde/tests/test_base.py b/verde/tests/test_base.py
index 04af292..02f1eb2 100644
--- a/verde/tests/test_base.py
+++ b/verde/tests/test_base.py
@@ -6,7 +6,7 @@ import pytest
 import numpy as np
 import numpy.testing as npt
 
-from ..base.gridder import get_dims, get_data_names, get_region, BaseGridder
+from ..base import get_dims, get_data_names, get_region, BaseGridder
 from ..coordinates import grid_coordinates, scatter_points
 
 
@@ -100,26 +100,27 @@ class PolyGridder(BaseGridder):
     def __init__(self, degree=1):
         self.degree = degree
 
-    def fit(self, easting, northing, data):
+    def fit(self, coordinates, data):
         "Fit an easting polynomial"
         ndata = data.size
         nparams = self.degree + 1
         jac = np.zeros((ndata, nparams))
         for j in range(nparams):
-            jac[:, j] = easting.ravel()**j
+            jac[:, j] = coordinates[0].ravel()**j
         self.coefs_ = np.linalg.solve(jac.T.dot(jac), jac.T.dot(data.ravel()))
         return self
 
-    def predict(self, easting, northing):
+    def predict(self, coordinates):
         "Predict the data"
-        return np.sum(cof*easting**deg for deg, cof in enumerate(self.coefs_))
+        return np.sum(cof*coordinates[0]**deg
+                      for deg, cof in enumerate(self.coefs_))
 
 
 def test_basegridder():
     "Test basic functionality of BaseGridder"
 
     with pytest.raises(NotImplementedError):
-        BaseGridder().predict(None, None)
+        BaseGridder().predict((None, None))
 
     grd = PolyGridder()
     assert repr(grd) == 'PolyGridder(degree=1)'
@@ -129,26 +130,26 @@ def test_basegridder():
     region = (0, 10, -10, -5)
     shape = (50, 30)
     angular, linear = 2, 100
-    east, north = scatter_points(region, 1000, random_state=0)
-    data = angular*east + linear
-    grd = PolyGridder().fit(east, north, data)
+    coordinates = scatter_points(region, 1000, random_state=0)
+    data = angular*coordinates[0] + linear
+    grd = PolyGridder().fit(coordinates, data)
 
     with pytest.raises(ValueError):
         # A region should be given because it hasn't been assigned
         grd.grid()
 
-    east_true, north_true = grid_coordinates(region, shape)
-    data_true = angular*east_true + linear
+    coordinates_true = grid_coordinates(region, shape)
+    data_true = angular*coordinates_true[0] + linear
     grid = grd.grid(region, shape)
 
     npt.assert_allclose(grd.coefs_, [linear, angular])
     npt.assert_allclose(grid.scalars.values, data_true)
-    npt.assert_allclose(grid.easting.values, east_true[0, :])
-    npt.assert_allclose(grid.northing.values, north_true[:, 0])
+    npt.assert_allclose(grid.easting.values, coordinates_true[0][0, :])
+    npt.assert_allclose(grid.northing.values, coordinates_true[1][:, 0])
     npt.assert_allclose(grd.scatter(region, 1000, random_state=0).scalars,
                         data)
     npt.assert_allclose(grd.profile((0, 0), (10, 0), 30).scalars,
-                        angular*east_true[0, :] + linear)
+                        angular*coordinates_true[0][0, :] + linear)
 
 
 def test_basegridder_projection():
@@ -157,11 +158,11 @@ def test_basegridder_projection():
     region = (0, 10, -10, -5)
     shape = (50, 30)
     angular, linear = 2, 100
-    east, north = scatter_points(region, 1000, random_state=0)
-    data = angular*east + linear
-    east_true, north_true = grid_coordinates(region, shape)
-    data_true = angular*east_true + linear
-    grd = PolyGridder().fit(east, north, data)
+    coordinates = scatter_points(region, 1000, random_state=0)
+    data = angular*coordinates[0] + linear
+    coordinates_true = grid_coordinates(region, shape)
+    data_true = angular*coordinates_true[0] + linear
+    grd = PolyGridder().fit(coordinates, data)
 
     # Lets say we want to specify the region for a grid using a coordinate
     # system that is lon/2, lat/2.
@@ -176,7 +177,8 @@ def test_basegridder_projection():
 
     npt.assert_allclose(grd.coefs_, [linear, angular])
     npt.assert_allclose(grid.scalars.values, data_true)
-    npt.assert_allclose(grid.easting.values, east_true[0, :]/2)
-    npt.assert_allclose(grid.northing.values, north_true[:, 0]/2)
+    npt.assert_allclose(grid.easting.values, coordinates_true[0][0, :]/2)
+    npt.assert_allclose(grid.northing.values, coordinates_true[1][:, 0]/2)
     npt.assert_allclose(scat.scalars, data)
-    npt.assert_allclose(prof.scalars, angular*east_true[0, :] + linear)
+    npt.assert_allclose(prof.scalars,
+                        angular*coordinates_true[0][0, :] + linear)
diff --git a/verde/tests/test_scipy.py b/verde/tests/test_scipy.py
index 1b7b907..df8fbe3 100644
--- a/verde/tests/test_scipy.py
+++ b/verde/tests/test_scipy.py
@@ -18,8 +18,8 @@ def test_scipy_gridder_same_points():
     # The interpolation should be perfect on top of the data points
     for method in ['nearest', 'linear', 'cubic']:
         grd = ScipyGridder(method=method)
-        grd.fit(data.easting, data.northing, data.scalars)
-        predicted = grd.predict(data.easting, data.northing)
+        grd.fit((data.easting, data.northing), data.scalars)
+        predicted = grd.predict((data.easting, data.northing))
         npt.assert_allclose(predicted, data.scalars)
 
 
@@ -27,13 +27,14 @@ def test_scipy_gridder():
     "See if the gridder recovers known points."
     synth = CheckerBoard().fit(region=(1000, 5000, -8000, -6000))
     data = synth.scatter(size=20000, random_state=0)
-    east, north = 3000, -7000
-    true_data = synth.predict(east, north)
+    coords = (data.easting, data.northing)
+    pt_coords = (3000, -7000)
+    true_data = synth.predict(pt_coords)
     # nearest will never be too close to the truth
-    grd = ScipyGridder('cubic').fit(data.easting, data.northing, data.scalars)
-    npt.assert_almost_equal(grd.predict(east, north), true_data, decimal=2)
-    grd = ScipyGridder('linear').fit(data.easting, data.northing, data.scalars)
-    npt.assert_almost_equal(grd.predict(east, north), true_data, decimal=1)
+    grd = ScipyGridder('cubic').fit(coords, data.scalars)
+    npt.assert_almost_equal(grd.predict(pt_coords), true_data, decimal=2)
+    grd = ScipyGridder('linear').fit(coords, data.scalars)
+    npt.assert_almost_equal(grd.predict(pt_coords), true_data, decimal=1)
 
 
 def test_scipy_gridder_region():
@@ -42,22 +43,24 @@ def test_scipy_gridder_region():
     synth = CheckerBoard().fit(region=region)
     # Test using xarray objects
     grid = synth.grid()
-    east, north = grid_coordinates(region, grid.scalars.shape)
-    grd = ScipyGridder().fit(east, north, grid.scalars)
+    coords = grid_coordinates(region, grid.scalars.shape)
+    grd = ScipyGridder().fit(coords, grid.scalars)
     npt.assert_allclose(grd.region_, region)
     # Test using pandas objects
-    data = pd.DataFrame({'easting': east.ravel(), 'northing': north.ravel(),
+    data = pd.DataFrame({'easting': coords[0].ravel(),
+                         'northing': coords[1].ravel(),
                          'scalars': grid.scalars.values.ravel()})
-    grd = ScipyGridder().fit(data.easting, data.northing, data.scalars)
+    grd = ScipyGridder().fit((data.easting, data.northing), data.scalars)
     npt.assert_allclose(grd.region_, region)
 
 
 def test_scipy_gridder_extra_args():
     "Passing in extra arguments to scipy"
     data = CheckerBoard().fit().scatter(random_state=100)
+    coords = (data.easting, data.northing)
     grd = ScipyGridder(method='linear', extra_args=dict(rescale=True))
-    grd.fit(data.easting, data.northing, data.scalars)
-    predicted = grd.predict(data.easting, data.northing)
+    grd.fit(coords, data.scalars)
+    predicted = grd.predict(coords)
     npt.assert_allclose(predicted, data.scalars)
 
 
@@ -66,4 +69,4 @@ def test_scipy_gridder_fails():
     data = CheckerBoard().fit().scatter(random_state=0)
     grd = ScipyGridder(method='some invalid method name')
     with pytest.raises(ValueError):
-        grd.fit(data.easting, data.northing, data.scalars)
+        grd.fit((data.easting, data.northing), data.scalars)
diff --git a/verde/tests/test_trend.py b/verde/tests/test_trend.py
index e4956f5..70ac122 100644
--- a/verde/tests/test_trend.py
+++ b/verde/tests/test_trend.py
@@ -16,46 +16,47 @@ def simple_model():
     east, north = grid_coordinates((1000, 5000, -5000, -1000), shape=(50, 50))
     coefs = [10, 2, -0.4]
     data = coefs[0] + coefs[1]*east + coefs[2]*north
-    return east, north, coefs, data
+    return (east, north), coefs, data
 
 
 def test_trend(simple_model):
     "Test the trend estimation on a simple problem"
-    east, north, coefs, data = simple_model
-    trend = Trend(degree=1).fit(east, north, data)
+    coords, coefs, data = simple_model
+    trend = Trend(degree=1).fit(coords, data)
     npt.assert_allclose(trend.coef_, coefs)
-    npt.assert_allclose(trend.predict(east, north), data)
+    npt.assert_allclose(trend.predict(coords), data)
     npt.assert_allclose(trend.residual_, 0, atol=1e-5)
 
 
 def test_trend_weights(simple_model):
     "Use weights to account for outliers"
-    east, north, coefs, data = simple_model
+    coords, coefs, data = simple_model
     data_out = data.copy()
     outlier = data_out[20, 20]*50
     data_out[20, 20] += outlier
     weights = np.ones_like(data)
     weights[20, 20] = 1e-10
-    trend = Trend(degree=1).fit(east, north, data_out, weights)
+    trend = Trend(degree=1).fit(coords, data_out, weights)
     npt.assert_allclose(trend.coef_, coefs)
     npt.assert_allclose(trend.residual_[20, 20], outlier)
-    npt.assert_allclose(trend.predict(east, north), data)
+    npt.assert_allclose(trend.predict(coords), data)
 
 
 def test_trend_fails_coordinates():
     "Test the failing conditions for the trend estimator"
-    east = north = np.arange(20)
+    coords = (np.arange(20), np.arange(20))
     data = np.arange(30)
     with pytest.raises(ValueError):
-        Trend(degree=10).fit(east, north, data)
+        Trend(degree=10).fit(coords, data)
 
 
 def test_trend_fails_weights():
     "Test the failing conditions for the trend estimator"
-    east = north = data = np.arange(20)
+    data = np.arange(20)
+    coords = (data, data)
     weights = np.arange(30)
     with pytest.raises(ValueError):
-        Trend(degree=10).fit(east, north, data, weights)
+        Trend(degree=10).fit(coords, data, weights)
 
 
 def test_polynomial_combinations_fails():
@@ -68,7 +69,6 @@ def test_polynomial_combinations_fails():
 
 def test_trend_jacobian_fails():
     "Test failing conditions for the trend jacobian builder"
-    east = np.arange(50)
-    north = np.arange(30)
+    east, north = np.arange(50), np.arange(30)
     with pytest.raises(ValueError):
         trend_jacobian(east, north, degree=1)