From 5e3edf471361d91477db100794451650658dc81b Mon Sep 17 00:00:00 2001
From: Bas Nijholt <basnijholt@gmail.com>
Date: Fri, 19 Oct 2018 14:19:42 +0200
Subject: [PATCH] documentation improvements

---
 AUTHORS.md                                    |   2 +-
 README.rst                                    |  30 ++++-----
 adaptive/learner/average_learner.py           |  17 +++++-
 adaptive/learner/balancing_learner.py         |  37 ++++++++----
 adaptive/learner/base_learner.py              |  17 ++++--
 adaptive/learner/data_saver.py                |   2 +
 adaptive/learner/integrator_learner.py        |   1 +
 adaptive/learner/learner1D.py                 |  57 +++++++++++-------
 adaptive/learner/learner2D.py                 |  41 +++++++++++++
 adaptive/learner/learnerND.py                 |  12 ++++
 adaptive/learner/skopt_learner.py             |   9 +--
 adaptive/runner.py                            |  13 ++--
 docs/source/_static/logo.png                  | Bin 0 -> 5093 bytes
 docs/source/conf.py                           |   1 +
 docs/source/{rest_of_readme.rst => docs.rst}  |  40 ++++++++----
 docs/source/index.rst                         |   2 +-
 .../adaptive.learner.base_learner.rst         |   2 +-
 .../adaptive.learner.triangulation.rst        |   7 +++
 docs/source/reference/adaptive.rst            |   2 +
 .../reference/adaptive.runner.extras.rst      |   6 +-
 docs/source/tutorial/tutorial.custom_loss.rst |   2 +-
 docs/source/tutorial/tutorial.rst             |   1 +
 22 files changed, 216 insertions(+), 85 deletions(-)
 create mode 100644 docs/source/_static/logo.png
 rename docs/source/{rest_of_readme.rst => docs.rst} (85%)
 create mode 100644 docs/source/reference/adaptive.learner.triangulation.rst

diff --git a/AUTHORS.md b/AUTHORS.md
index aa414900..3da642df 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,4 +1,4 @@
-# Adaptive Authors
+## Authors
 Below is a list of the contributors to Adaptive:
 
 + [Anton Akhmerov](<https://antonakhmerov.org>)
diff --git a/README.rst b/README.rst
index f92ed5d7..c32d133a 100644
--- a/README.rst
+++ b/README.rst
@@ -1,12 +1,10 @@
 .. summary-start
 
-.. _logo-adaptive:
+|logo| adaptive
+===============
 
-|image0| adaptive
-=================
-
-|PyPI| |Conda| |Downloads| |pipeline status| |DOI| |Binder| |Join the
-chat at https://gitter.im/python-adaptive/adaptive| |Documentation Status|
+|PyPI| |Conda| |Downloads| |Pipeline status| |DOI| |Binder| |Gitter|
+|Documentation| |GitHub|
 
 **Tools for adaptive parallel sampling of mathematical functions.**
 
@@ -126,7 +124,9 @@ We would like to give credits to the following people:
   Mathematical Software, 37 (3), art. no. 26, 2010.
 - Pauli Virtanen for his ``AdaptiveTriSampling`` script (no longer
   available online since SciPy Central went down) which served as
-  inspiration for the ``~adaptive.Learner2D``.
+  inspiration for the `~adaptive.Learner2D`.
+
+.. credits-end
 
 For general discussion, we have a `Gitter chat
 channel <https://gitter.im/python-adaptive/adaptive>`_. If you find any
@@ -136,21 +136,23 @@ or submit a `merge
 request <https://gitlab.kwant-project.org/qt/adaptive/merge_requests>`_.
 
 .. references-start
-.. |image0| image:: https://gitlab.kwant-project.org/qt/adaptive/uploads/d20444093920a4a0499e165b5061d952/logo.png
+.. |logo| image:: https://adaptive.readthedocs.io/en/latest/_static/logo.png
 .. |PyPI| image:: https://img.shields.io/pypi/v/adaptive.svg
    :target: https://pypi.python.org/pypi/adaptive
-.. |Conda| image:: https://anaconda.org/conda-forge/adaptive/badges/installer/conda.svg
+.. |Conda| image:: https://img.shields.io/badge/install%20with-conda-green.svg
    :target: https://anaconda.org/conda-forge/adaptive
-.. |Downloads| image:: https://anaconda.org/conda-forge/adaptive/badges/downloads.svg
+.. |Downloads| image:: https://img.shields.io/conda/dn/conda-forge/adaptive.svg
    :target: https://anaconda.org/conda-forge/adaptive
-.. |pipeline status| image:: https://gitlab.kwant-project.org/qt/adaptive/badges/master/pipeline.svg
+.. |Pipeline status| image:: https://gitlab.kwant-project.org/qt/adaptive/badges/master/pipeline.svg
    :target: https://gitlab.kwant-project.org/qt/adaptive/pipelines
 .. |DOI| image:: https://zenodo.org/badge/113714660.svg
-   :target: https://zenodo.org/badge/latestdoi/113714660
+   :target: https://doi.org/10.5281/zenodo.1446400
 .. |Binder| image:: https://mybinder.org/badge.svg
    :target: https://mybinder.org/v2/gh/python-adaptive/adaptive/master?filepath=learner.ipynb
-.. |Join the chat at https://gitter.im/python-adaptive/adaptive| image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg
+.. |Gitter| image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg
    :target: https://gitter.im/python-adaptive/adaptive
-.. |Documentation Status| image:: https://readthedocs.org/projects/adaptive/badge/?version=latest
+.. |Documentation| image:: https://readthedocs.org/projects/adaptive/badge/?version=latest
    :target: https://adaptive.readthedocs.io/en/latest/?badge=latest
+.. |GitHub| image:: https://img.shields.io/github/stars/python-adaptive/adaptive.svg?style=social
+   :target: https://github.com/python-adaptive/adaptive/stargazers
 .. references-end
diff --git a/adaptive/learner/average_learner.py b/adaptive/learner/average_learner.py
index 312101bf..704a9bb8 100644
--- a/adaptive/learner/average_learner.py
+++ b/adaptive/learner/average_learner.py
@@ -27,6 +27,8 @@ class AverageLearner(BaseLearner):
         Sampled points and values.
     pending_points : set
         Points that still have to be evaluated.
+    npoints : int
+        Number of evaluated points.
     """
 
     def __init__(self, function, atol=None, rtol=None):
@@ -48,7 +50,7 @@ class AverageLearner(BaseLearner):
 
     @property
     def n_requested(self):
-        return len(self.data) + len(self.pending_points)
+        return self.npoints + len(self.pending_points)
 
     def ask(self, n, tell_pending=True):
         points = list(range(self.n_requested, self.n_requested + n))
@@ -59,7 +61,7 @@ class AverageLearner(BaseLearner):
                           - set(self.data)
                           - set(self.pending_points))[:n]
 
-        loss_improvements = [self.loss_improvement(n) / n] * n
+        loss_improvements = [self._loss_improvement(n) / n] * n
         if tell_pending:
             for p in points:
                 self.tell_pending(p)
@@ -81,10 +83,13 @@ class AverageLearner(BaseLearner):
 
     @property
     def mean(self):
+        """The average of all values in `data`."""
         return self.sum_f / self.npoints
 
     @property
     def std(self):
+        """The corrected sample standard deviation of the values
+        in `data`."""
         n = self.npoints
         if n < 2:
             return np.inf
@@ -106,7 +111,7 @@ class AverageLearner(BaseLearner):
         return max(standard_error / self.atol,
                    standard_error / abs(self.mean) / self.rtol)
 
-    def loss_improvement(self, n):
+    def _loss_improvement(self, n):
         loss = self.loss()
         if np.isfinite(loss):
             return loss - self.loss(n=self.npoints + n)
@@ -118,6 +123,12 @@ class AverageLearner(BaseLearner):
         self.pending_points = set()
 
     def plot(self):
+        """Returns a histogram of the evaluated data.
+
+        Returns
+        -------
+        holoviews.element.Histogram
+            A histogram of the evaluated data."""
         hv = ensure_holoviews()
         vals = [v for v in self.data.values() if v is not None]
         if not vals:
diff --git a/adaptive/learner/balancing_learner.py b/adaptive/learner/balancing_learner.py
index 764758ba..f8ec0b08 100644
--- a/adaptive/learner/balancing_learner.py
+++ b/adaptive/learner/balancing_learner.py
@@ -22,7 +22,7 @@ class BalancingLearner(BaseLearner):
 
     Parameters
     ----------
-    learners : sequence of `BaseLearner`
+    learners : sequence of `~adaptive.BaseLearner`\s
         The learners from which to choose. These must all have the same type.
     cdims : sequence of dicts, or (keys, iterable of values), optional
         Constant dimensions; the parameters that label the learners. Used
@@ -42,6 +42,13 @@ class BalancingLearner(BaseLearner):
             >>> cdims = (['A', 'B'], [(True, 0), (True, 1),
             ...                       (False, 0), (False, 1)])
 
+    Attributes
+    ----------
+    learners : list
+        The sequence of `~adaptive.BaseLearner`\s.
+    function : callable
+        A function that calls the functions of the underlying learners.
+        Its signature is ``function(learner_index, point)``.
     strategy : 'loss_improvements' (default), 'loss', or 'npoints'
         The points that the `BalancingLearner` choses can be either based on:
         the best 'loss_improvements', the smallest total 'loss' of the
@@ -51,13 +58,13 @@ class BalancingLearner(BaseLearner):
 
     Notes
     -----
-    This learner compares the 'loss' calculated from the "child" learners.
+    This learner compares the `loss` calculated from the "child" learners.
     This requires that the 'loss' from different learners *can be meaningfully
     compared*. For the moment we enforce this restriction by requiring that
     all learners are the same type but (depending on the internals of the
     learner) it may be that the loss cannot be compared *even between learners
-    of the same type*. In this case the `BalancingLearner` will behave in an
-    undefined way.
+    of the same type*. In this case the `~adaptive.BalancingLearner` will
+    behave in an undefined way. Change the `strategy` in that case.
     """
 
     def __init__(self, learners, *, cdims=None, strategy='loss_improvements'):
@@ -81,6 +88,12 @@ class BalancingLearner(BaseLearner):
 
     @property
     def strategy(self):
+        """Can be either 'loss_improvements' (default), 'loss', or 'npoints'
+        The points that the `BalancingLearner` choses can be either based on:
+        the best 'loss_improvements', the smallest total 'loss' of the
+        child learners, or the number of points per learner, using 'npoints'.
+        One can dynamically change the strategy while the simulation is
+        running by changing the ``learner.strategy`` attribute."""
         return self._strategy
 
     @strategy.setter
@@ -122,7 +135,7 @@ class BalancingLearner(BaseLearner):
         points = []
         loss_improvements = []
         for _ in range(n):
-            losses = self.losses(real=False)
+            losses = self._losses(real=False)
             max_ind = np.argmax(losses)
             xs, ls = self.learners[max_ind].ask(1)
             points.append((max_ind, xs[0]))
@@ -165,7 +178,7 @@ class BalancingLearner(BaseLearner):
         self._loss.pop(index, None)
         self.learners[index].tell_pending(x)
 
-    def losses(self, real=True):
+    def _losses(self, real=True):
         losses = []
         loss_dict = self._loss if real else self._pending_loss
 
@@ -178,7 +191,7 @@ class BalancingLearner(BaseLearner):
 
     @cache_latest
     def loss(self, real=True):
-        losses = self.losses(real)
+        losses = self._losses(real)
         return max(losses)
 
     def plot(self, cdims=None, plotter=None, dynamic=True):
@@ -215,8 +228,8 @@ class BalancingLearner(BaseLearner):
         Returns
         -------
         dm : `holoviews.core.DynamicMap` (default) or `holoviews.core.HoloMap`
-            A `DynamicMap` (dynamic=True) or `HoloMap` (dynamic=False) with
-            sliders that are defined by `cdims`.
+             A `DynamicMap` ``(dynamic=True)`` or `HoloMap`
+             ``(dynamic=False)`` with sliders that are defined by `cdims`.
         """
         hv = ensure_holoviews()
         cdims = cdims or self._cdims_default
@@ -295,7 +308,7 @@ class BalancingLearner(BaseLearner):
         Notes
         -----
         The order of the child learners inside `learner.learners` is the same
-        as `adaptive.utils.named_product(**combos)`.
+        as ``adaptive.utils.named_product(**combos)``.
         """
         learners = []
         arguments = named_product(**combos)
@@ -313,7 +326,7 @@ class BalancingLearner(BaseLearner):
         folder : str
             Directory in which the learners's data will be saved.
         compress : bool, default True
-            Compress the data upon saving using 'gzip'. When saving
+            Compress the data upon saving using `gzip`. When saving
             using compression, one must load it with compression too.
 
         Notes
@@ -364,7 +377,7 @@ class BalancingLearner(BaseLearner):
 
         Example
         -------
-        See the example in the 'BalancingLearner.save' doc-string.
+        See the example in the `BalancingLearner.save` doc-string.
         """
         for l in self.learners:
             l.load(os.path.join(folder, l.fname), compress=compress)
diff --git a/adaptive/learner/base_learner.py b/adaptive/learner/base_learner.py
index b33cc018..eed2c5a8 100644
--- a/adaptive/learner/base_learner.py
+++ b/adaptive/learner/base_learner.py
@@ -20,6 +20,9 @@ class BaseLearner(metaclass=abc.ABCMeta):
     npoints : int, optional
         The number of evaluated points that have been added to the learner.
         Subclasses do not *have* to implement this attribute.
+    pending_points : set, optional
+        Points that have been requested but have not been evaluated yet.
+        Subclasses do not *have* to implement this attribute.
 
     Notes
     -----
@@ -118,10 +121,11 @@ class BaseLearner(metaclass=abc.ABCMeta):
 
         Notes
         -----
-        There are __two ways__ of naming the files:
-        1. Using the 'fname' argument in 'learner.save(fname='example.p')
-        2. Setting the 'fname' attribute, like
-           'learner.fname = "data/example.p"' and then 'learner.save()'.
+        There are **two ways** of naming the files:
+
+        1. Using the ``fname`` argument in ``learner.save(fname='example.p')``
+        2. Setting the ``fname`` attribute, like
+           ``learner.fname = "data/example.p"`` and then ``learner.save()``.
         """
         fname = fname or self.fname
         data = self._get_data()
@@ -142,7 +146,7 @@ class BaseLearner(metaclass=abc.ABCMeta):
 
         Notes
         -----
-        See the notes in the 'BaseLearner.save' doc-string.
+        See the notes in the `save` doc-string.
         """
         fname = fname or self.fname
         with suppress(FileNotFoundError, EOFError):
@@ -157,6 +161,9 @@ class BaseLearner(metaclass=abc.ABCMeta):
 
     @property
     def fname(self):
+        """Filename for the learner when it is saved (or loaded) using
+        `~adaptive.BaseLearner.save` (or `~adaptive.BaseLearner.load` ).
+        """
         # This is a property because then it will be availible in the DataSaver
         try:
             return self._fname
diff --git a/adaptive/learner/data_saver.py b/adaptive/learner/data_saver.py
index 832c1a9e..8e035994 100644
--- a/adaptive/learner/data_saver.py
+++ b/adaptive/learner/data_saver.py
@@ -35,11 +35,13 @@ class DataSaver:
     def __getattr__(self, attr):
         return getattr(self.learner, attr)
 
+    @copy_docstring_from(BaseLearner.tell)
     def tell(self, x, result):
         y = self.arg_picker(result)
         self.extra_data[x] = result
         self.learner.tell(x, y)
 
+    @copy_docstring_from(BaseLearner.tell_pending)
     def tell_pending(self, x):
         self.learner.tell_pending(x)
 
diff --git a/adaptive/learner/integrator_learner.py b/adaptive/learner/integrator_learner.py
index 931d4531..0290a387 100644
--- a/adaptive/learner/integrator_learner.py
+++ b/adaptive/learner/integrator_learner.py
@@ -487,6 +487,7 @@ class IntegratorLearner(BaseLearner):
 
     @property
     def npoints(self):
+        """Number of evaluated points."""
         return len(self.done_points)
 
     @property
diff --git a/adaptive/learner/learner1D.py b/adaptive/learner/learner1D.py
index 343a9def..8868800d 100644
--- a/adaptive/learner/learner1D.py
+++ b/adaptive/learner/learner1D.py
@@ -34,7 +34,7 @@ def uniform_loss(interval, scale, function_values):
 
 
 def default_loss(interval, scale, function_values):
-    """Calculate loss on a single interval
+    """Calculate loss on a single interval.
 
     Currently returns the rescaled length of the interval. If one of the
     y-values is missing, returns 0 (so the intervals with missing data are
@@ -148,6 +148,12 @@ class Learner1D(BaseLearner):
 
     @property
     def vdim(self):
+        """Length of the output of ``learner.function``.
+        If the output is unsized (when it's a scalar)
+        then `vdim = 1`.
+
+        As long as no data is known `vdim = 1`.
+        """
         if self._vdim is None:
             if self.data:
                 y = next(iter(self.data.values()))
@@ -162,6 +168,7 @@ class Learner1D(BaseLearner):
 
     @property
     def npoints(self):
+        """Number of evaluated points."""
         return len(self.data)
 
     @cache_latest
@@ -169,7 +176,7 @@ class Learner1D(BaseLearner):
         losses = self.losses if real else self.losses_combined
         return max(losses.values()) if len(losses) > 0 else float('inf')
 
-    def update_interpolated_loss_in_interval(self, x_left, x_right):
+    def _update_interpolated_loss_in_interval(self, x_left, x_right):
         if x_left is not None and x_right is not None:
             dx = x_right - x_left
             if dx < self._dx_eps:
@@ -187,13 +194,13 @@ class Learner1D(BaseLearner):
                 self.losses_combined[a, b] = (b - a) * loss / dx
                 a = b
 
-    def update_losses(self, x, real=True):
+    def _update_losses(self, x, real=True):
         # When we add a new point x, we should update the losses
         # (x_left, x_right) are the "real" neighbors of 'x'.
-        x_left, x_right = self.find_neighbors(x, self.neighbors)
+        x_left, x_right = self._find_neighbors(x, self.neighbors)
         # (a, b) are the neighbors of the combined interpolated
         # and "real" intervals.
-        a, b = self.find_neighbors(x, self.neighbors_combined)
+        a, b = self._find_neighbors(x, self.neighbors_combined)
 
         # (a, b) is splitted into (a, x) and (x, b) so if (a, b) exists
         self.losses_combined.pop((a, b), None)  # we get rid of (a, b).
@@ -202,8 +209,8 @@ class Learner1D(BaseLearner):
             # We need to update all interpolated losses in the interval
             # (x_left, x) and (x, x_right). Since the addition of the point
             # 'x' could change their loss.
-            self.update_interpolated_loss_in_interval(x_left, x)
-            self.update_interpolated_loss_in_interval(x, x_right)
+            self._update_interpolated_loss_in_interval(x_left, x)
+            self._update_interpolated_loss_in_interval(x, x_right)
 
             # Since 'x' is in between (x_left, x_right),
             # we get rid of the interval.
@@ -230,7 +237,7 @@ class Learner1D(BaseLearner):
             self.losses_combined[x, b] = float('inf')
 
     @staticmethod
-    def find_neighbors(x, neighbors):
+    def _find_neighbors(x, neighbors):
         if x in neighbors:
             return neighbors[x]
         pos = neighbors.bisect_left(x)
@@ -239,14 +246,14 @@ class Learner1D(BaseLearner):
         x_right = keys[pos] if pos != len(neighbors) else None
         return x_left, x_right
 
-    def update_neighbors(self, x, neighbors):
+    def _update_neighbors(self, x, neighbors):
         if x not in neighbors:  # The point is new
-            x_left, x_right = self.find_neighbors(x, neighbors)
+            x_left, x_right = self._find_neighbors(x, neighbors)
             neighbors[x] = [x_left, x_right]
             neighbors.get(x_left, [None, None])[1] = x
             neighbors.get(x_right, [None, None])[0] = x
 
-    def update_scale(self, x, y):
+    def _update_scale(self, x, y):
         """Update the scale with which the x and y-values are scaled.
 
         For a learner where the function returns a single scalar the scale
@@ -291,16 +298,16 @@ class Learner1D(BaseLearner):
         if not self.bounds[0] <= x <= self.bounds[1]:
             return
 
-        self.update_neighbors(x, self.neighbors_combined)
-        self.update_neighbors(x, self.neighbors)
-        self.update_scale(x, y)
-        self.update_losses(x, real=True)
+        self._update_neighbors(x, self.neighbors_combined)
+        self._update_neighbors(x, self.neighbors)
+        self._update_scale(x, y)
+        self._update_losses(x, real=True)
 
         # If the scale has increased enough, recompute all losses.
         if self._scale[1] > 2 * self._oldscale[1]:
 
             for interval in self.losses:
-                self.update_interpolated_loss_in_interval(*interval)
+                self._update_interpolated_loss_in_interval(*interval)
 
             self._oldscale = deepcopy(self._scale)
 
@@ -309,8 +316,8 @@ class Learner1D(BaseLearner):
             # The point is already evaluated before
             return
         self.pending_points.add(x)
-        self.update_neighbors(x, self.neighbors_combined)
-        self.update_losses(x, real=False)
+        self._update_neighbors(x, self.neighbors_combined)
+        self._update_losses(x, real=False)
 
     def tell_many(self, xs, ys, *, force=False):
         if not force and not (len(xs) > 0.5 * len(self.data) and len(xs) > 2):
@@ -379,10 +386,10 @@ class Learner1D(BaseLearner):
             if ival in self.losses:
                 # If this interval does not exist it should already
                 # have an inf loss.
-                self.update_interpolated_loss_in_interval(*ival)
+                self._update_interpolated_loss_in_interval(*ival)
 
     def ask(self, n, tell_pending=True):
-        """Return n points that are expected to maximally reduce the loss."""
+        """Return 'n' points that are expected to maximally reduce the loss."""
         points, loss_improvements = self._ask_points_without_adding(n)
 
         if tell_pending:
@@ -392,7 +399,7 @@ class Learner1D(BaseLearner):
         return points, loss_improvements
 
     def _ask_points_without_adding(self, n):
-        """Return n points that are expected to maximally reduce the loss.
+        """Return 'n' points that are expected to maximally reduce the loss.
         Without altering the state of the learner"""
         # Find out how to divide the n points over the intervals
         # by finding  positive integer n_i that minimize max(L_i / n_i) subject
@@ -466,6 +473,14 @@ class Learner1D(BaseLearner):
         return points, loss_improvements
 
     def plot(self):
+        """Returns a plot of the evaluated data.
+
+        Returns
+        -------
+        plot : `holoviews.element.Scatter` (if vdim=1)\
+               else `holoviews.element.Path`
+            Plot of the evaluated data.
+        """
         hv = ensure_holoviews()
         if not self.data:
             p = hv.Scatter([]) * hv.Path([])
diff --git a/adaptive/learner/learner2D.py b/adaptive/learner/learner2D.py
index bbc3f644..11ac92a4 100644
--- a/adaptive/learner/learner2D.py
+++ b/adaptive/learner/learner2D.py
@@ -15,6 +15,19 @@ from ..utils import cache_latest
 # Learner2D and helper functions.
 
 def deviations(ip):
+    """Returns the deviation of the linear estimate.
+
+    Is useful when defining custom loss functions.
+
+    Parameters
+    ----------
+    ip : `scipy.interpolate.LinearNDInterpolator` instance
+
+    Returns
+    -------
+    numpy array
+        The deviation per triangle.
+    """
     values = ip.values / (ip.values.ptp(axis=0).max() or 1)
     gradients = interpolate.interpnd.estimate_gradients_2d_global(
         ip.tri, values, tol=1e-6)
@@ -37,6 +50,20 @@ def deviations(ip):
 
 
 def areas(ip):
+    """Returns the area per triangle of the triangulation inside
+    a `LinearNDInterpolator` instance.
+
+    Is useful when defining custom loss functions.
+
+    Parameters
+    ----------
+    ip : `scipy.interpolate.LinearNDInterpolator` instance
+
+    Returns
+    -------
+    numpy array
+        The area per triangle in ``ip.tri``.
+    """
     p = ip.tri.points[ip.tri.vertices]
     q = p[:, :-1, :] - p[:, -1, None, :]
     areas = abs(q[:, 0, 0] * q[:, 1, 1] - q[:, 0, 1] * q[:, 1, 0]) / 2
@@ -289,10 +316,17 @@ class Learner2D(BaseLearner):
 
     @property
     def npoints(self):
+        """Number of evaluated points."""
         return len(self.data)
 
     @property
     def vdim(self):
+        """Length of the output of ``learner.function``.
+        If the output is unsized (when it's a scalar)
+        then `vdim = 1`.
+
+        As long as no data is known `vdim = 1`.
+        """
         if self._vdim is None and self.data:
             try:
                 value = next(iter(self.data.values()))
@@ -337,11 +371,15 @@ class Learner2D(BaseLearner):
         return points_combined, values_combined
 
     def data_combined(self):
+        """Like `data`, however this includes the points in
+        `pending_points` for which the values are interpolated."""
         # Interpolate the unfinished points
         points, values = self._data_combined()
         return {tuple(k): v for k, v in zip(points, values)}
 
     def ip(self):
+        """A `scipy.interpolate.LinearNDInterpolator` instance
+        containing the learner's data."""
         if self._ip is None:
             points, values = self._data_in_bounds()
             points = self._scale(points)
@@ -349,6 +387,9 @@ class Learner2D(BaseLearner):
         return self._ip
 
     def ip_combined(self):
+        """A `scipy.interpolate.LinearNDInterpolator` instance
+        containing the learner's data *and* interpolated data of
+        the `pending_points`."""
         if self._ip_combined is None:
             points, values = self._data_combined()
             points = self._scale(points)
diff --git a/adaptive/learner/learnerND.py b/adaptive/learner/learnerND.py
index 274ae16f..c8081878 100644
--- a/adaptive/learner/learnerND.py
+++ b/adaptive/learner/learnerND.py
@@ -188,10 +188,17 @@ class LearnerND(BaseLearner):
 
     @property
     def npoints(self):
+        """Number of evaluated points."""
         return len(self.data)
 
     @property
     def vdim(self):
+        """Length of the output of ``learner.function``.
+        If the output is unsized (when it's a scalar)
+        then `vdim = 1`.
+
+        As long as no data is known `vdim = 1`.
+        """
         if self._vdim is None and self.data:
             try:
                 value = next(iter(self.data.values()))
@@ -205,6 +212,8 @@ class LearnerND(BaseLearner):
         return all(p in self.data for p in self._bounds_points)
 
     def ip(self):
+        """A `scipy.interpolate.LinearNDInterpolator` instance
+        containing the learner's data."""
         # XXX: take our own triangulation into account when generating the ip
         return interpolate.LinearNDInterpolator(self.points, self.values)
 
@@ -227,10 +236,12 @@ class LearnerND(BaseLearner):
 
     @property
     def values(self):
+        """Get the values from `data` as a numpy array."""
         return np.array(list(self.data.values()), dtype=float)
 
     @property
     def points(self):
+        """Get the points from `data` as a numpy array."""
         return np.array(list(self.data.keys()), dtype=float)
 
     def tell(self, point, value):
@@ -262,6 +273,7 @@ class LearnerND(BaseLearner):
         return simplex in self.tri.simplices
 
     def inside_bounds(self, point):
+        """Check whether a point is inside the bounds."""
         return all(mn <= p <= mx for p, (mn, mx) in zip(point, self.bounds))
 
     def tell_pending(self, point, *, simplex=None):
diff --git a/adaptive/learner/skopt_learner.py b/adaptive/learner/skopt_learner.py
index 2b778ba5..9aac8d4c 100644
--- a/adaptive/learner/skopt_learner.py
+++ b/adaptive/learner/skopt_learner.py
@@ -8,18 +8,18 @@ from ..utils import cache_latest
 
 
 class SKOptLearner(Optimizer, BaseLearner):
-    """Learn a function minimum using 'skopt.Optimizer'.
+    """Learn a function minimum using ``skopt.Optimizer``.
 
-    This is an 'Optimizer' from 'scikit-optimize',
+    This is an ``Optimizer`` from ``scikit-optimize``,
     with the necessary methods added to make it conform
-    to the 'adaptive' learner interface.
+    to the ``adaptive`` learner interface.
 
     Parameters
     ----------
     function : callable
         The function to learn.
     **kwargs :
-        Arguments to pass to 'skopt.Optimizer'.
+        Arguments to pass to ``skopt.Optimizer``.
     """
 
     def __init__(self, function, **kwargs):
@@ -63,6 +63,7 @@ class SKOptLearner(Optimizer, BaseLearner):
 
     @property
     def npoints(self):
+        """Number of evaluated points."""
         return len(self.Xi)
 
     def plot(self, nsamples=200):
diff --git a/adaptive/runner.py b/adaptive/runner.py
index ce2b61ad..b6d0de49 100644
--- a/adaptive/runner.py
+++ b/adaptive/runner.py
@@ -54,7 +54,7 @@ else:
 
 
 class BaseRunner:
-    """Base class for runners that use concurrent.futures.Executors.
+    """Base class for runners that use `concurrent.futures.Executors`.
 
     Parameters
     ----------
@@ -346,7 +346,7 @@ class BlockingRunner(BaseRunner):
 
 
 class AsyncRunner(BaseRunner):
-    """Run a learner asynchronously in an executor using asyncio.
+    """Run a learner asynchronously in an executor using `asyncio`.
 
     Parameters
     ----------
@@ -548,7 +548,7 @@ class AsyncRunner(BaseRunner):
         Parameters
         ----------
         save_kwargs : dict
-            Key-word arguments for 'learner.save(**save_kwargs)'.
+            Key-word arguments for ``learner.save(**save_kwargs)``.
         interval : int
             Number of seconds between saving the learner.
 
@@ -586,7 +586,7 @@ def simple(learner, goal):
 
     Parameters
     ----------
-    learner : adaptive.BaseLearner
+    learner : ~`adaptive.BaseLearner` instance
     goal : callable
         The end condition for the calculation. This function must take the
         learner as its sole argument, and return True if we should stop.
@@ -605,9 +605,10 @@ def replay_log(learner, log):
 
     Parameters
     ----------
-    learner : learner.BaseLearner
+    learner : `~adaptive.BaseLearner` instance
+        New learner where the log will be applied.
     log : list
-        contains tuples: '(method_name, *args)'.
+        contains tuples: ``(method_name, *args)``.
     """
     for method, *args in log:
         getattr(learner, method)(*args)
diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff7a0f4e8abb071626512f5eb9e9d9df245fba6f
GIT binary patch
literal 5093
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
H&OGd2KJ>0A

literal 0
HcmV?d00001

diff --git a/docs/source/conf.py b/docs/source/conf.py
index 5a70aa0c..707b987b 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -43,6 +43,7 @@ release = adaptive.__version__
 extensions = [
     'sphinx.ext.autodoc',
     'sphinx.ext.autosummary',
+    'sphinx.ext.autosectionlabel',
     'sphinx.ext.intersphinx',
     'sphinx.ext.mathjax',
     'sphinx.ext.viewcode',
diff --git a/docs/source/rest_of_readme.rst b/docs/source/docs.rst
similarity index 85%
rename from docs/source/rest_of_readme.rst
rename to docs/source/docs.rst
index 2fcab204..299cfc46 100644
--- a/docs/source/rest_of_readme.rst
+++ b/docs/source/docs.rst
@@ -19,9 +19,13 @@ The following learners are implemented:
 - `~adaptive.AverageLearner`, For stochastic functions where you want to
   average the result over many evaluations,
 - `~adaptive.IntegratorLearner`, for
-  when you want to intergrate a 1D function ``f: ℝ → ℝ``,
+  when you want to intergrate a 1D function ``f: ℝ → ℝ``.
+
+Meta-learners (to be used with other learners):
+
 - `~adaptive.BalancingLearner`, for when you want to run several learners at once,
-  selecting the “best” one each time you get more points.
+  selecting the “best” one each time you get more points,
+- `~adaptive.DataSaver`, for when your function doesn't just return a scalar or a vector.
 
 In addition to the learners, ``adaptive`` also provides primitives for
 running the sampling across several cores and even several machines,
@@ -47,8 +51,6 @@ on the *Play* :fa:`play` button or move the sliders.
     adaptive.notebook_extension()
     %output holomap='scrubber'
 
-
-
 `adaptive.Learner1D`
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -82,8 +84,6 @@ on the *Play* :fa:`play` button or move the sliders.
     (get_hm(uniform_loss).relabel('homogeneous samping')
      + get_hm(default_loss).relabel('with adaptive'))
 
-
-
 `adaptive.Learner2D`
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -111,8 +111,6 @@ on the *Play* :fa:`play` button or move the sliders.
     plots = {n: plot(learner, n) for n in range(4, 1010, 20)}
     hv.HoloMap(plots, kdims=['npoints']).collate()
 
-
-
 `adaptive.AverageLearner`
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -134,15 +132,31 @@ on the *Play* :fa:`play` button or move the sliders.
     plots = {n: plot(learner, n) for n in range(10, 10000, 200)}
     hv.HoloMap(plots, kdims=['npoints'])
 
+`adaptive.LearnerND`
+~~~~~~~~~~~~~~~~~~~~
 
-see more in the :ref:`Tutorial Adaptive`.
+.. jupyter-execute::
+    :hide-code:
 
+    def sphere(xyz):
+        import numpy as np
+        x, y, z = xyz
+        a = 0.4
+        return np.exp(-(x**2 + y**2 + z**2 - 0.75**2)**2/a**4)
 
-.. include:: ../../README.rst
-    :start-after: not-in-documentation-end
+    learner = adaptive.LearnerND(sphere, bounds=[(-1, 1), (-1, 1), (-1, 1)])
+    adaptive.runner.simple(learner, lambda l: l.npoints == 3000)
+
+    learner.plot_3D()
 
+see more in the :ref:`Tutorial Adaptive`.
 
-Authors
--------
+.. include:: ../../README.rst
+    :start-after: not-in-documentation-end
+    :end-before: credits-end
 
 .. mdinclude:: ../../AUTHORS.md
+
+.. include:: ../../README.rst
+    :start-after: credits-end
+    :end-before: references-start
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 42f0d393..c95748e1 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -16,6 +16,6 @@
    :maxdepth: 2
    :hidden:
 
-   rest_of_readme
+   docs
    tutorial/tutorial
    reference/adaptive
diff --git a/docs/source/reference/adaptive.learner.base_learner.rst b/docs/source/reference/adaptive.learner.base_learner.rst
index 28ff6160..7a908ab5 100644
--- a/docs/source/reference/adaptive.learner.base_learner.rst
+++ b/docs/source/reference/adaptive.learner.base_learner.rst
@@ -1,4 +1,4 @@
-adaptive.learner.BaseLearner
+adaptive.BaseLearner
 ============================
 
 .. autoclass:: adaptive.learner.BaseLearner
diff --git a/docs/source/reference/adaptive.learner.triangulation.rst b/docs/source/reference/adaptive.learner.triangulation.rst
new file mode 100644
index 00000000..8e4e4dfc
--- /dev/null
+++ b/docs/source/reference/adaptive.learner.triangulation.rst
@@ -0,0 +1,7 @@
+adaptive.learner.triangulation module
+=====================================
+
+.. automodule:: adaptive.learner.triangulation
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/source/reference/adaptive.rst b/docs/source/reference/adaptive.rst
index ab5049c9..54eeb9aa 100644
--- a/docs/source/reference/adaptive.rst
+++ b/docs/source/reference/adaptive.rst
@@ -7,6 +7,7 @@ Learners
 .. toctree::
 
     adaptive.learner.average_learner
+    adaptive.learner.base_learner
     adaptive.learner.balancing_learner
     adaptive.learner.data_saver
     adaptive.learner.integrator_learner
@@ -22,6 +23,7 @@ Runners
     adaptive.runner.Runner
     adaptive.runner.AsyncRunner
     adaptive.runner.BlockingRunner
+    adaptive.runner.BaseRunner
     adaptive.runner.extras
 
 Other
diff --git a/docs/source/reference/adaptive.runner.extras.rst b/docs/source/reference/adaptive.runner.extras.rst
index 00b809c5..90786f4e 100644
--- a/docs/source/reference/adaptive.runner.extras.rst
+++ b/docs/source/reference/adaptive.runner.extras.rst
@@ -1,5 +1,5 @@
-adaptive.runner.simple
-======================
+Runner extras
+=============
 
 Simple executor
 ---------------
@@ -9,7 +9,7 @@ Simple executor
 Sequential excecutor
 --------------------
 
-.. autofunction:: adaptive.runner.SequentialExecutor
+.. autoclass:: adaptive.runner.SequentialExecutor
 
 
 Replay log
diff --git a/docs/source/tutorial/tutorial.custom_loss.rst b/docs/source/tutorial/tutorial.custom_loss.rst
index f19151f4..b1cca295 100644
--- a/docs/source/tutorial/tutorial.custom_loss.rst
+++ b/docs/source/tutorial/tutorial.custom_loss.rst
@@ -8,7 +8,7 @@ Custom adaptive logic for 1D and 2D
 
 .. seealso::
     The complete source code of this tutorial can be found in
-    :jupyter-download:notebook:`tutorial.custom-loss-function`
+    :jupyter-download:notebook:`tutorial.custom-loss`
 
 .. jupyter-execute::
     :hide-code:
diff --git a/docs/source/tutorial/tutorial.rst b/docs/source/tutorial/tutorial.rst
index 2afd0011..e1476082 100644
--- a/docs/source/tutorial/tutorial.rst
+++ b/docs/source/tutorial/tutorial.rst
@@ -22,6 +22,7 @@ on the following packages
 - ``bokeh``
 - ``ipywidgets``
 
+We recommend to start with the :ref:`Tutorial `~adaptive.Learner1D``.
 
 .. note::
     Because this documentation consists of static html, the ``live_plot``
-- 
GitLab