Repository: fatiando/verde
Pull request #157 (2018-12-11 15:21:30)

Name the different Travis builds
Makes it a lot easier to see which build does what in the Travis
website.

**Reminders**

- [ ] Run `make format` and `make check` to make sure the code follows the style guide.
- [ ] Add tests for new features or tests that would have caught the bug that you're fixing.
- [ ] Add new public functions/methods/classes to `doc/api/index.rst` and `verde/__init__.py`.
- [ ] Write detailed docstrings for all functions/classes/methods.
- [ ] If adding new functionality, add an example to the docstring, gallery, and/or tutorials.



Patch:
diff --git a/.travis.yml b/.travis.yml
index 1f878ad..7769a4c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -37,7 +37,8 @@ env:
 matrix:
     include:
         # Code style and quality checks
-        - os: linux
+        - name: "Style checks"
+          os: linux
           env:
               - PYTHON=3.6
               - CHECK=true
@@ -46,40 +47,47 @@ matrix:
               - CONDA_REQUIREMENTS=""
               - CONDA_REQUIREMENTS_DEV=""
         # Main build to deploy to PyPI and Github pages
-        - os: linux
+        - name: "Linux - Python 3.6 (deploy)"
+          os: linux
           env:
               - PYTHON=3.6
               - TEST=true
               - BUILD_DOCS=true
               - DEPLOY_DOCS=true
               - DEPLOY_PYPI=true
-        - os: linux
+        - name: "Linux - Python 3.5"
+          os: linux
           env:
               - PYTHON=3.5
               - TEST=true
               - BUILD_DOCS=true
-        - os: linux
+        - name: "Linux - Python 3.7"
+          os: linux
           env:
               - PYTHON=3.7
               - TEST=true
               - BUILD_DOCS=true
-        - os: osx
+        - name: "Mac - Python 3.5"
+          os: osx
           env:
               - PYTHON=3.5
               - TEST=true
               - BUILD_DOCS=true
-        - os: osx
+        - name: "Mac - Python 3.6"
+          os: osx
           env:
               - PYTHON=3.6
               - TEST=true
               - BUILD_DOCS=true
-        - os: osx
+        - name: "Mac - Python 3.7"
+          os: osx
           env:
               - PYTHON=3.7
               - TEST=true
               - BUILD_DOCS=true
         # Check tests pass when using optional dependencies.
-        - os: linux
+        - name: "Linux - Python 3.6 (optional deps)"
+          os: linux
           env:
               - PYTHON=3.6
               - TEST=true
diff --git a/doc/api/index.rst b/doc/api/index.rst
index 1451e5f..691becc 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -71,7 +71,7 @@ Utilities
     variance_to_weights
     test
     grid_to_table
-    
+
 .. automodule:: verde.datasets
 
 .. currentmodule:: verde
@@ -92,10 +92,13 @@ Datasets
     datasets.fetch_rio_magnetic
     datasets.setup_rio_magnetic_map
 
-Base Classes
-------------
+Base Classes and Functions
+--------------------------
 
 .. autosummary::
    :toctree: generated/
 
     base.BaseGridder
+    base.n_1d_arrays
+    base.check_fit_input
+    base.least_squares
diff --git a/verde/base/__init__.py b/verde/base/__init__.py
new file mode 100644
index 0000000..9e246d9
--- /dev/null
+++ b/verde/base/__init__.py
@@ -0,0 +1,4 @@
+# pylint: disable=missing-docstring
+from .base_classes import BaseGridder
+from .least_squares import least_squares
+from .utils import n_1d_arrays, check_fit_input
diff --git a/verde/base.py b/verde/base/base_classes.py
similarity index 86%
rename from verde/base.py
rename to verde/base/base_classes.py
index 3fdfefd..a063e0f 100644
--- a/verde/base.py
+++ b/verde/base/base_classes.py
@@ -1,18 +1,14 @@
 """
 Base classes for all gridders.
 """
-from warnings import warn
-
 import xarray as xr
 import pandas as pd
 import numpy as np
 from sklearn.base import BaseEstimator
 from sklearn.metrics import r2_score
-from sklearn.preprocessing import StandardScaler
-from sklearn.linear_model import LinearRegression, Ridge
 
-from .coordinates import grid_coordinates, profile_coordinates, scatter_points
-from .utils import check_data
+from ..coordinates import grid_coordinates, profile_coordinates, scatter_points
+from .utils import check_data, check_fit_input
 
 
 class BaseGridder(BaseEstimator):
@@ -524,82 +520,3 @@ def get_instance_region(instance, region):
             raise ValueError("No default region found. Argument must be supplied.")
         region = getattr(instance, "region_")
     return region
-
-
-def check_fit_input(coordinates, data, weights, unpack=True):
-    """
-    Validate the inputs to the fit method of gridders.
-
-    Checks that the coordinates, data, and weights (if given) all have the same
-    shape. Weights arrays are raveled.
-
-    Parameters
-    ----------
-    coordinates : tuple of arrays
-        Arrays with the coordinates of each data point. Should be in the
-        following order: (easting, northing, vertical, ...).
-    data : array or tuple of arrays
-        The data values of each data point. Data can have more than one
-        component. In such cases, data should be a tuple of arrays.
-    weights : None or array
-        If not None, then the weights assigned to each data point.
-        Typically, this should be 1 over the data uncertainty squared.
-        If the data has multiple components, the weights have the same number
-        of components.
-    unoack : bool
-        If False, data and weights will be tuples always. If they are single
-        arrays, then they will be returned as a 1-element tuple. If True, will
-        unpack the tuples if there is only 1 array in each.
-
-    Returns
-    -------
-    validated_inputs
-        The validated inputs in the same order. If weights are given, will
-        ravel the array before returning.
-
-    """
-    data = check_data(data)
-    weights = check_data(weights)
-    if any(i.shape != j.shape for i in coordinates for j in data):
-        raise ValueError("Coordinate and data arrays must have the same shape.")
-    if any(w is not None for w in weights):
-        if len(weights) != len(data):
-            raise ValueError(
-                "Number of data '{}' and weights '{}' must be equal.".format(
-                    len(data), len(weights)
-                )
-            )
-        if any(i.size != j.size for i in weights for j in data):
-            raise ValueError("Weights must have the same size as the data array.")
-        weights = tuple(i.ravel() for i in weights)
-    else:
-        weights = tuple([None] * len(data))
-    if unpack:
-        if len(weights) == 1:
-            weights = weights[0]
-        if len(data) == 1:
-            data = data[0]
-    return coordinates, data, weights
-
-
-def least_squares(jacobian, data, weights, damping=None):
-    """
-    Estimate forces that fit the data using least-squares. Scales the
-    Jacobian matrix to have unit standard deviation. This helps balance the
-    regularization and the difference between forces.
-    """
-    if jacobian.shape[0] < jacobian.shape[1]:
-        warn(
-            "Under-determined problem detected (ndata, nparams)={}.".format(
-                jacobian.shape
-            )
-        )
-    scaler = StandardScaler(copy=False, with_mean=False, with_std=True)
-    jacobian = scaler.fit_transform(jacobian)
-    if damping is None:
-        regr = LinearRegression(fit_intercept=False, normalize=False)
-    else:
-        regr = Ridge(alpha=damping, fit_intercept=False, normalize=False)
-    regr.fit(jacobian, data.ravel(), sample_weight=weights)
-    params = regr.coef_ / scaler.scale_
-    return params
diff --git a/verde/base/least_squares.py b/verde/base/least_squares.py
new file mode 100644
index 0000000..b54826c
--- /dev/null
+++ b/verde/base/least_squares.py
@@ -0,0 +1,53 @@
+"""
+Functions for least-squares fitting with optional regularization.
+"""
+from warnings import warn
+
+from sklearn.preprocessing import StandardScaler
+from sklearn.linear_model import LinearRegression, Ridge
+
+
+def least_squares(jacobian, data, weights, damping=None):
+    """
+    Solve a weighted least-squares problem with optional damping regularization.
+
+    Scales the Jacobian matrix so that each column has unit variance. This helps keep
+    the regularization parameter in a sensible range. The scaling is undone before
+    returning the estimated parameters so that scaling isn't required for predictions.
+    Doesn't normalize the column means because that operation can't be undone.
+
+    Parameters
+    ----------
+    jacobian : 2d-array
+        The Jacobian/sensitivity/feature matrix.
+    data : 1d-array
+        The data array. Must be a single 1D array. If fitting multiple data components,
+        stack the arrays and the Jacobian matrices.
+    weights : None or 1d-array
+        The data weights. Like the data, this must also be a 1D array. Stack the weights
+        in the same order as the data. Use ``weights=None`` to fit without weights.
+    damping : None or float
+        The positive damping (Tikhonov 0th order) regularization parameter. If
+        ``damping=None``, will use a regular least-squares fit.
+
+    Returns
+    -------
+    parameters : 1d-array
+        The estimated 1D array of parameters that fit the data.
+
+    """
+    if jacobian.shape[0] < jacobian.shape[1]:
+        warn(
+            "Under-determined problem detected (ndata, nparams)={}.".format(
+                jacobian.shape
+            )
+        )
+    scaler = StandardScaler(copy=False, with_mean=False, with_std=True)
+    jacobian = scaler.fit_transform(jacobian)
+    if damping is None:
+        regr = LinearRegression(fit_intercept=False, normalize=False)
+    else:
+        regr = Ridge(alpha=damping, fit_intercept=False, normalize=False)
+    regr.fit(jacobian, data.ravel(), sample_weight=weights)
+    params = regr.coef_ / scaler.scale_
+    return params
diff --git a/verde/base/utils.py b/verde/base/utils.py
new file mode 100644
index 0000000..f26d330
--- /dev/null
+++ b/verde/base/utils.py
@@ -0,0 +1,113 @@
+"""
+Utility functions for building gridders and checking arguments.
+"""
+import numpy as np
+
+
+def check_data(data):
+    """
+    Check the *data* argument and make sure it's a tuple.
+    If the data is a single array, return it as a tuple with a single element.
+
+    This is the default format accepted and used by all gridders and processing
+    functions.
+
+    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
+
+
+def check_fit_input(coordinates, data, weights, unpack=True):
+    """
+    Validate the inputs to the fit method of gridders.
+
+    Checks that the coordinates, data, and weights (if given) all have the same
+    shape. Weights arrays are raveled.
+
+    Parameters
+    ----------
+    coordinates : tuple of arrays
+        Arrays with the coordinates of each data point. Should be in the
+        following order: (easting, northing, vertical, ...).
+    data : array or tuple of arrays
+        The data values of each data point. Data can have more than one
+        component. In such cases, data should be a tuple of arrays.
+    weights : None or array
+        If not None, then the weights assigned to each data point.
+        Typically, this should be 1 over the data uncertainty squared.
+        If the data has multiple components, the weights have the same number
+        of components.
+    unpack : bool
+        If False, data and weights will be tuples always. If they are single
+        arrays, then they will be returned as a 1-element tuple. If True, will
+        unpack the tuples if there is only 1 array in each.
+
+    Returns
+    -------
+    validated_inputs
+        The validated inputs in the same order. If weights are given, will
+        ravel the array before returning.
+
+    """
+    data = check_data(data)
+    weights = check_data(weights)
+    if any(i.shape != j.shape for i in coordinates for j in data):
+        raise ValueError("Coordinate and data arrays must have the same shape.")
+    if any(w is not None for w in weights):
+        if len(weights) != len(data):
+            raise ValueError(
+                "Number of data '{}' and weights '{}' must be equal.".format(
+                    len(data), len(weights)
+                )
+            )
+        if any(i.size != j.size for i in weights for j in data):
+            raise ValueError("Weights must have the same size as the data array.")
+        weights = tuple(i.ravel() for i in weights)
+    else:
+        weights = tuple([None] * len(data))
+    if unpack:
+        if len(weights) == 1:
+            weights = weights[0]
+        if len(data) == 1:
+            data = data[0]
+    return coordinates, data, weights
+
+
+def n_1d_arrays(arrays, n):
+    """
+    Get the first n elements from a tuple/list, make sure they are arrays, and ravel.
+
+    Use this function to make sure that coordinate and data arrays are ready for
+    building Jacobian matrices and least-squares fitting.
+
+    Parameters
+    ----------
+    arrays : tuple of arrays
+        The arrays. Can be lists or anything that can be converted to a numpy array
+        (including numpy arrays).
+    n : int
+        How many arrays to return.
+
+    Returns
+    -------
+    1darrays : tuple of arrays
+        The converted 1D numpy arrays.
+
+    Examples
+    --------
+
+    >>> import numpy as np
+    >>> arrays = [np.arange(4).reshape(2, 2)]*3
+    >>> n_1d_arrays(arrays, n=2)
+    (array([0, 1, 2, 3]), array([0, 1, 2, 3]))
+
+    """
+    return tuple(np.atleast_1d(i).ravel() for i in arrays[:n])
diff --git a/verde/chain.py b/verde/chain.py
index 2814b7d..97cad7d 100644
--- a/verde/chain.py
+++ b/verde/chain.py
@@ -4,7 +4,7 @@ Class for chaining gridders.
 from sklearn.utils.validation import check_is_fitted
 
 from .base import BaseGridder
-from .utils import check_data
+from .base.utils import check_data
 from .coordinates import get_region
 
 
diff --git a/verde/coordinates.py b/verde/coordinates.py
index ac51daf..ecb8d92 100644
--- a/verde/coordinates.py
+++ b/verde/coordinates.py
@@ -9,7 +9,7 @@ try:
 except ImportError:
     from scipy.spatial import cKDTree as KDTree  # pylint: disable=no-name-in-module
 
-from .utils import n_1d_arrays
+from .base.utils import n_1d_arrays
 
 
 def check_region(region):
diff --git a/verde/mask.py b/verde/mask.py
index 41da2fc..8a00a22 100644
--- a/verde/mask.py
+++ b/verde/mask.py
@@ -3,7 +3,7 @@ Mask grid points based on different criteria.
 """
 import numpy as np
 
-from .utils import n_1d_arrays
+from .base import n_1d_arrays
 
 try:
     from pykdtree.kdtree import KDTree
diff --git a/verde/spline.py b/verde/spline.py
index 62aab7f..2f71a25 100644
--- a/verde/spline.py
+++ b/verde/spline.py
@@ -6,9 +6,9 @@ from warnings import warn
 import numpy as np
 from sklearn.utils.validation import check_is_fitted
 
-from .base import BaseGridder, check_fit_input, least_squares
+from .base import n_1d_arrays, BaseGridder, check_fit_input, least_squares
 from .coordinates import get_region
-from .utils import n_1d_arrays, parse_engine
+from .utils import parse_engine
 
 try:
     import numba
diff --git a/verde/trend.py b/verde/trend.py
index 5c20fa2..f51d8c2 100644
--- a/verde/trend.py
+++ b/verde/trend.py
@@ -4,9 +4,8 @@
 import numpy as np
 from sklearn.utils.validation import check_is_fitted
 
-from .base import BaseGridder, check_fit_input, least_squares
+from .base import BaseGridder, check_fit_input, least_squares, n_1d_arrays
 from .coordinates import get_region
-from .utils import n_1d_arrays
 
 
 class Trend(BaseGridder):
diff --git a/verde/utils.py b/verde/utils.py
index e1cfca8..3b256ce 100644
--- a/verde/utils.py
+++ b/verde/utils.py
@@ -6,6 +6,8 @@ import functools
 import numpy as np
 import pandas as pd
 
+from .base.utils import check_data
+
 
 def parse_engine(engine):
     """
@@ -66,56 +68,6 @@ def dummy_jit(**kwargs):  # pylint: disable=unused-argument
     return dummy_decorator
 
 
-def n_1d_arrays(arrays, n):
-    """
-    Get the first n elements from a tuple/list, make sure they are arrays, and ravel.
-
-    Parameters
-    ----------
-    arrays : tuple of arrays
-        The arrays. Can be lists or anything that can be converted to a numpy array
-        (including numpy arrays).
-    n : int
-        How many arrays to return.
-
-    Returns
-    -------
-    1darrays : tuple of arrays
-        The converted 1D numpy arrays.
-
-    Examples
-    --------
-
-    >>> import numpy as np
-    >>> arrays = [np.arange(4).reshape(2, 2)]*3
-    >>> n_1d_arrays(arrays, n=2)
-    (array([0, 1, 2, 3]), array([0, 1, 2, 3]))
-
-    """
-    return tuple(np.atleast_1d(i).ravel() for i in arrays[:n])
-
-
-def check_data(data):
-    """
-    Check the *data* argument and make sure it's a tuple.
-    If the data is a single array, return it as a tuple with a single element.
-
-    This is the default format accepted and used by all gridders and processing
-    functions.
-
-    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
-
-
 def variance_to_weights(variance, tol=1e-15, dtype="float64"):
     """
     Converts data variances to weights for gridding.
diff --git a/verde/vector.py b/verde/vector.py
index 17b3805..2827fd9 100644
--- a/verde/vector.py
+++ b/verde/vector.py
@@ -4,9 +4,9 @@ Classes for dealing with vector data.
 import numpy as np
 from sklearn.utils.validation import check_is_fitted
 
-from .base import check_fit_input, least_squares, BaseGridder
+from .base import n_1d_arrays, check_fit_input, least_squares, BaseGridder
 from .spline import warn_weighted_exact_solution
-from .utils import n_1d_arrays, parse_engine
+from .utils import parse_engine
 from .coordinates import get_region
 
 try:


Test patch:
diff --git a/verde/tests/test_base.py b/verde/tests/test_base.py
index d1db6c2..5ea0645 100644
--- a/verde/tests/test_base.py
+++ b/verde/tests/test_base.py
@@ -6,12 +6,12 @@ import numpy as np
 import numpy.testing as npt
 import pytest
 
-from ..base import (
+from ..base.utils import check_fit_input
+from ..base.base_classes import (
+    BaseGridder,
     get_dims,
     get_data_names,
     get_instance_region,
-    BaseGridder,
-    check_fit_input,
 )
 from ..coordinates import grid_coordinates, scatter_points
 
diff --git a/verde/tests/test_vector.py b/verde/tests/test_vector.py
index 8db056f..e8ba142 100644
--- a/verde/tests/test_vector.py
+++ b/verde/tests/test_vector.py
@@ -9,7 +9,7 @@ import pytest
 from ..datasets.synthetic import CheckerBoard
 from ..coordinates import grid_coordinates
 from ..trend import Trend
-from ..utils import n_1d_arrays
+from ..base import n_1d_arrays
 from .utils import requires_numba
 from ..vector import VectorSpline2D, Vector