Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (51)
Showing
with 1106 additions and 421 deletions
......@@ -94,4 +94,4 @@ ENV/
# asv files
benchmarks/html/
benchmarks/results/
.asv
\ No newline at end of file
.asv
......@@ -14,3 +14,7 @@ authors check:
- MISSING_AUTHORS=$(git shortlog -s HEAD | sed -e "s/^[0-9\t ]*//"| xargs -i sh -c 'grep -q "{}" AUTHORS.md || echo "{} missing from authors"')
- if [ ! -z "$MISSING_AUTHORS" ]; then { echo $MISSING_AUTHORS; exit 1; }; fi
allow_failure: true
check whitespace style:
script: ./check_whitespace
allow_failure: true
# Adaptive Authors
## Authors
Below is a list of the contributors to Adaptive:
+ [Anton Akhmerov](<https://antonakhmerov.org>)
......@@ -6,9 +6,3 @@ Below is a list of the contributors to Adaptive:
+ [Christoph Groth](<http://inac.cea.fr/Pisp/christoph.groth/>)
+ Jorn Hoofwijk
+ [Joseph Weston](<https://joseph.weston.cloud>)
For a full list of contributors run
```
git log --pretty=format:"%an" | sort | uniq
```
# ![][logo] adaptive
[![PyPI](https://img.shields.io/pypi/v/adaptive.svg)](https://pypi.python.org/pypi/adaptive)
[![Conda](https://anaconda.org/conda-forge/adaptive/badges/installer/conda.svg)](https://anaconda.org/conda-forge/adaptive)
[![Downloads](https://anaconda.org/conda-forge/adaptive/badges/downloads.svg)](https://anaconda.org/conda-forge/adaptive)
[![pipeline status](https://gitlab.kwant-project.org/qt/adaptive/badges/master/pipeline.svg)](https://gitlab.kwant-project.org/qt/adaptive/pipelines)
[![DOI](https://zenodo.org/badge/113714660.svg)](https://zenodo.org/badge/latestdoi/113714660)
[![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/python-adaptive/adaptive/master?filepath=learner.ipynb)
[![Join the chat at https://gitter.im/python-adaptive/adaptive](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/python-adaptive/adaptive)
**Tools for adaptive parallel sampling of mathematical functions.**
`adaptive` is an [open-source](LICENSE) Python library designed to make adaptive parallel function evaluation simple.
With `adaptive` you just supply a function with its bounds, and it will be evaluated at the "best" points in parameter space.
With just a few lines of code you can evaluate functions on a computing cluster, live-plot the data as it returns, and fine-tune the adaptive sampling algorithm.
Check out the `adaptive` [example notebook `learner.ipynb`](learner.ipynb) (or run it [live on Binder](https://mybinder.org/v2/gh/python-adaptive/adaptive/master?filepath=learner.ipynb)) to see examples of how to use `adaptive`.
**WARNING: `adaptive` is still in a beta development stage**
## Implemented algorithms
The core concept in `adaptive` is that of a *learner*. A *learner* samples
a function at the best places in its parameter space to get maximum
"information" about the function. As it evaluates the function
at more and more points in the parameter space, it gets a better idea of where
the best places are to sample next.
Of course, what qualifies as the "best places" will depend on your application domain!
`adaptive` makes some reasonable default choices, but the details of the adaptive
sampling are completely customizable.
The following learners are implemented:
* `Learner1D`, for 1D functions `f: ℝ → ℝ^N`,
* `Learner2D`, for 2D functions `f: ℝ^2 → ℝ^N`,
* `LearnerND`, for ND functions `f: ℝ^N → ℝ^M`,
* `AverageLearner`, For stochastic functions where you want to average the result over many evaluations,
* `IntegratorLearner`, for when you want to intergrate a 1D function `f: ℝ → ℝ`,
* `BalancingLearner`, for when you want to run several learners at once, selecting the "best" one each time you get more points.
In addition to the learners, `adaptive` also provides primitives for running
the sampling across several cores and even several machines, with built-in support
for [`concurrent.futures`](https://docs.python.org/3/library/concurrent.futures.html),
[`ipyparallel`](https://ipyparallel.readthedocs.io/en/latest/)
and [`distributed`](https://distributed.readthedocs.io/en/latest/).
## Examples
<img src="https://user-images.githubusercontent.com/6897215/38739170-6ac7c014-3f34-11e8-9e8f-93b3a3a3d61b.gif" width='20%'> </img>
<img src="https://user-images.githubusercontent.com/6897215/35219611-ac8b2122-ff73-11e7-9332-adffab64a8ce.gif" width='40%'> </img>
## Installation
`adaptive` works with Python 3.6 and higher on Linux, Windows, or Mac, and provides optional extensions for working with the Jupyter/IPython Notebook.
The recommended way to install adaptive is using `conda`:
```bash
conda install -c conda-forge adaptive
```
`adaptive` is also available on PyPI:
```bash
pip install adaptive[notebook]
```
The `[notebook]` above will also install the optional dependencies for running `adaptive` inside
a Jupyter notebook.
## Development
Clone the repository and run `setup.py develop` to add a link to the cloned repo into your
Python path:
```
git clone git@github.com:python-adaptive/adaptive.git
cd adaptive
python3 setup.py develop
```
We highly recommend using a Conda environment or a virtualenv to manage the versions of your installed
packages while working on `adaptive`.
In order to not pollute the history with the output of the notebooks, please setup the git filter by executing
```bash
python ipynb_filter.py
```
in the repository.
## Credits
We would like to give credits to the following people:
- Pedro Gonnet for his implementation of [`CQUAD`](https://www.gnu.org/software/gsl/manual/html_node/CQUAD-doubly_002dadaptive-integration.html), "Algorithm 4" as described in "Increasing the Reliability of Adaptive Quadrature Using Explicit Interpolants", P. Gonnet, ACM Transactions on 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 [`Learner2D`](adaptive/learner/learner2D.py).
For general discussion, we have a [Gitter chat channel](https://gitter.im/python-adaptive/adaptive). If you find any bugs or have any feature suggestions please file a GitLab [issue](https://gitlab.kwant-project.org/qt/adaptive/issues/new?issue) or submit a [merge request](https://gitlab.kwant-project.org/qt/adaptive/merge_requests).
[logo]: https://gitlab.kwant-project.org/qt/adaptive/uploads/d20444093920a4a0499e165b5061d952/logo.png "adaptive logo"
.. summary-start
|logo| adaptive
===============
|PyPI| |Conda| |Downloads| |Pipeline status| |DOI| |Binder| |Gitter|
|Documentation| |GitHub|
**Tools for adaptive parallel sampling of mathematical functions.**
``adaptive`` is an open-source Python library designed to
make adaptive parallel function evaluation simple. With ``adaptive`` you
just supply a function with its bounds, and it will be evaluated at the
“best” points in parameter space. With just a few lines of code you can
evaluate functions on a computing cluster, live-plot the data as it
returns, and fine-tune the adaptive sampling algorithm.
Run the ``adaptive`` example notebook `live on
Binder <https://mybinder.org/v2/gh/python-adaptive/adaptive/master?filepath=learner.ipynb>`_
to see examples of how to use ``adaptive`` or visit the
`tutorial on Read the Docs <https://adaptive.readthedocs.io/en/latest/tutorial/tutorial.html>`__.
.. summary-end
**WARNING: adaptive is still in a beta development stage**
.. not-in-documentation-start
Implemented algorithms
----------------------
The core concept in ``adaptive`` is that of a *learner*. A *learner*
samples a function at the best places in its parameter space to get
maximum “information” about the function. As it evaluates the function
at more and more points in the parameter space, it gets a better idea of
where the best places are to sample next.
Of course, what qualifies as the “best places” will depend on your
application domain! ``adaptive`` makes some reasonable default choices,
but the details of the adaptive sampling are completely customizable.
The following learners are implemented:
- ``Learner1D``, for 1D functions ``f: ℝ → ℝ^N``,
- ``Learner2D``, for 2D functions ``f: ℝ^2 → ℝ^N``,
- ``LearnerND``, for ND functions ``f: ℝ^N → ℝ^M``,
- ``AverageLearner``, For stochastic functions where you want to
average the result over many evaluations,
- ``IntegratorLearner``, for
when you want to intergrate a 1D function ``f: ℝ → ℝ``,
- ``BalancingLearner``, for when you want to run several learners at once,
selecting the “best” one each time you get more points.
In addition to the learners, ``adaptive`` also provides primitives for
running the sampling across several cores and even several machines,
with built-in support for
`concurrent.futures <https://docs.python.org/3/library/concurrent.futures.html>`_,
`ipyparallel <https://ipyparallel.readthedocs.io/en/latest/>`_ and
`distributed <https://distributed.readthedocs.io/en/latest/>`_.
Examples
--------
.. raw:: html
<img src="https://user-images.githubusercontent.com/6897215/38739170-6ac7c014-3f34-11e8-9e8f-93b3a3a3d61b.gif" width='20%'> </img> <img src="https://user-images.githubusercontent.com/6897215/35219611-ac8b2122-ff73-11e7-9332-adffab64a8ce.gif" width='40%'> </img> <img src="https://user-images.githubusercontent.com/6897215/47256441-d6d53700-d480-11e8-8224-d1cc49dbdcf5.gif" width='20%'> </img>
.. not-in-documentation-end
Installation
------------
``adaptive`` works with Python 3.6 and higher on Linux, Windows, or Mac,
and provides optional extensions for working with the Jupyter/IPython
Notebook.
The recommended way to install adaptive is using ``conda``:
.. code:: bash
conda install -c conda-forge adaptive
``adaptive`` is also available on PyPI:
.. code:: bash
pip install adaptive[notebook]
The ``[notebook]`` above will also install the optional dependencies for
running ``adaptive`` inside a Jupyter notebook.
Development
-----------
Clone the repository and run ``setup.py develop`` to add a link to the
cloned repo into your Python path:
.. code:: bash
git clone git@github.com:python-adaptive/adaptive.git
cd adaptive
python3 setup.py develop
We highly recommend using a Conda environment or a virtualenv to manage
the versions of your installed packages while working on ``adaptive``.
In order to not pollute the history with the output of the notebooks,
please setup the git filter by executing
.. code:: bash
python ipynb_filter.py
in the repository.
Credits
-------
We would like to give credits to the following people:
- Pedro Gonnet for his implementation of `CQUAD <https://www.gnu.org/software/gsl/manual/html_node/CQUAD-doubly_002dadaptive-integration.html>`_,
“Algorithm 4” as described in “Increasing the Reliability of Adaptive
Quadrature Using Explicit Interpolants”, P. Gonnet, ACM Transactions on
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`.
.. credits-end
For general discussion, we have a `Gitter chat
channel <https://gitter.im/python-adaptive/adaptive>`_. If you find any
bugs or have any feature suggestions please file a GitLab
`issue <https://gitlab.kwant-project.org/qt/adaptive/issues/new?issue>`_
or submit a `merge
request <https://gitlab.kwant-project.org/qt/adaptive/merge_requests>`_.
.. references-start
.. |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://img.shields.io/badge/install%20with-conda-green.svg
:target: https://anaconda.org/conda-forge/adaptive
.. |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
:target: https://gitlab.kwant-project.org/qt/adaptive/pipelines
.. |DOI| image:: https://img.shields.io/badge/doi-10.5281%2Fzenodo.1182437-blue.svg
:target: https://doi.org/10.5281/zenodo.1182437
.. |Binder| image:: https://mybinder.org/badge.svg
:target: https://mybinder.org/v2/gh/python-adaptive/adaptive/master?filepath=learner.ipynb
.. |Gitter| image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg
:target: https://gitter.im/python-adaptive/adaptive
.. |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
......@@ -8,18 +8,17 @@ from . import learner
from . import runner
from . import utils
from .learner import (Learner1D, Learner2D, LearnerND, AverageLearner,
BalancingLearner, make_datasaver, DataSaver,
IntegratorLearner)
from .learner import (BaseLearner, Learner1D, Learner2D, LearnerND,
AverageLearner, BalancingLearner, make_datasaver,
DataSaver, IntegratorLearner)
with suppress(ImportError):
# Only available if 'scikit-optimize' is installed
from .learner import SKOptLearner
from .runner import Runner, BlockingRunner
from . import version
from .runner import Runner, AsyncRunner, BlockingRunner
__version__ = version.version
from ._version import __version__
del _version
del notebook_integration # to avoid confusion with `notebook_extension`
del version
# This file is part of 'miniver': https://github.com/jbweston/miniver
#
# This file will be overwritten by setup.py when a source or binary
# distribution is made. The magic value "__use_git__" is interpreted by
# version.py.
# _version.py.
version = "__use_git__"
# These values are only set if the distribution was created with 'git archive'
refnames = "$Format:%D$"
refnames = "$Format:%D$"
git_hash = "$Format:%h$"
# -*- coding: utf-8 -*-
# This file is part of 'miniver': https://github.com/jbweston/miniver
#
from collections import namedtuple
import os
import subprocess
import sys
from distutils.command.build import build as build_orig
from distutils.command.build_py import build_py as build_py_orig
from setuptools.command.sdist import sdist as sdist_orig
Version = namedtuple('Version', ('release', 'dev', 'labels'))
......@@ -19,31 +21,39 @@ STATIC_VERSION_FILE = '_static_version.py'
def get_version(version_file=STATIC_VERSION_FILE):
version_info = {}
with open(os.path.join(package_root, version_file), 'rb') as f:
exec(f.read(), {}, version_info)
version_info = get_static_version_info(version_file)
version = version_info['version']
version_is_from_git = (version == "__use_git__")
if version_is_from_git:
if version == "__use_git__":
version = get_version_from_git()
if not version:
version = get_version_from_git_archive(version_info)
if not version:
version = Version("unknown", None, None)
return semver_format(version)
return pep440_format(version)
else:
return version
def semver_format(version_info):
def get_static_version_info(version_file=STATIC_VERSION_FILE):
version_info = {}
with open(os.path.join(package_root, version_file), 'rb') as f:
exec(f.read(), {}, version_info)
return version_info
def version_is_from_git(version_file=STATIC_VERSION_FILE):
return get_static_version_info(version_file)['version'] == '__use_git__'
def pep440_format(version_info):
release, dev, labels = version_info
version_parts = [release]
if dev:
if release.endswith('-dev'):
if release.endswith('-dev') or release.endswith('.dev'):
version_parts.append(dev)
else:
version_parts.append('-dev{}'.format(dev))
else: # prefer PEP440 over strict adhesion to semver
version_parts.append('.dev{}'.format(dev))
if labels:
version_parts.append('+')
......@@ -64,26 +74,42 @@ def get_version_from_git():
if not os.path.samefile(p.communicate()[0].decode().rstrip('\n'),
distr_root):
# The top-level directory of the current Git repository is not the same
# as the root directory of the Kwant distribution: do not extract the
# as the root directory of the distribution: do not extract the
# version from Git.
return
# git describe --first-parent does not take into account tags from branches
# that were merged-in.
# that were merged-in. The '--long' flag gets us the 'dev' version and
# git hash, '--always' returns the git hash even if there are no tags.
for opts in [['--first-parent'], []]:
try:
p = subprocess.Popen(['git', 'describe', '--long'] + opts,
cwd=distr_root,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
['git', 'describe', '--long', '--always'] + opts,
cwd=distr_root,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError:
return
if p.wait() == 0:
break
else:
return
description = p.communicate()[0].decode().strip('v').rstrip('\n')
release, dev, git = description.rsplit('-', 2)
description = (
p.communicate()[0]
.decode()
.strip('v') # Tags can have a leading 'v', but the version should not
.rstrip('\n')
.rsplit('-', 2) # Split the latest tag, commits since tag, and hash
)
try:
release, dev, git = description
except ValueError: # No tags, only the git hash
# prepend 'g' to match with format returned by 'git describe'
git = 'g{}'.format(*description)
release = 'unknown'
dev = None
labels = []
if dev == "0":
dev = None
......@@ -93,7 +119,7 @@ def get_version_from_git():
try:
p = subprocess.Popen(['git', 'diff', '--quiet'], cwd=distr_root)
except OSError:
labels.append('confused') # This should never happen.
labels.append('confused') # This should never happen.
else:
if p.wait() == 1:
labels.append('dirty')
......@@ -126,14 +152,15 @@ def get_version_from_git_archive(version_info):
release, *_ = sorted(version_tags) # prefer e.g. "2.0" over "2.0rc1"
return Version(release, dev=None, labels=None)
else:
return Version('unknown', dev=None, labels=[f'g{git_hash}'])
return Version('unknown', dev=None, labels=['g{}'.format(git_hash)])
__version__ = get_version()
version = get_version()
# The following section defines a module global 'cmdclass',
# which can be used from setup.py. The 'package_name' and
# 'version' module globals are used (but not modified).
# '__version__' module globals are used (but not modified).
def _write_version(fname):
# This could be a hard link, so try to delete it first. Is there any way
......@@ -144,10 +171,10 @@ def _write_version(fname):
pass
with open(fname, 'w') as f:
f.write("# This file has been created by setup.py.\n"
"version = '{}'\n".format(version))
"version = '{}'\n".format(__version__))
class _build(build_orig):
class _build_py(build_py_orig):
def run(self):
super().run()
_write_version(os.path.join(self.build_lib, package_name,
......@@ -161,4 +188,4 @@ class _sdist(sdist_orig):
STATIC_VERSION_FILE))
cmdclass = dict(sdist=_sdist, build=_build)
cmdclass = dict(sdist=_sdist, build_py=_build_py)
# -*- coding: utf-8 -*-
import itertools
from math import sqrt
import numpy as np
from ..notebook_integration import ensure_holoviews
from .base_learner import BaseLearner
from ..notebook_integration import ensure_holoviews
from ..utils import cache_latest
class AverageLearner(BaseLearner):
......@@ -17,9 +17,9 @@ class AverageLearner(BaseLearner):
Parameters
----------
atol : float
Desired absolute tolerance
Desired absolute tolerance.
rtol : float
Desired relative tolerance
Desired relative tolerance.
Attributes
----------
......@@ -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,15 +83,23 @@ 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
return sqrt((self.sum_f_sq - n * self.mean**2) / (n - 1))
numerator = self.sum_f_sq - n * self.mean**2
if numerator < 0:
# in this case the numerator ~ -1e-15
return 0
return sqrt(numerator / (n - 1))
@cache_latest
def loss(self, real=True, *, n=None):
if n is None:
n = self.npoints if real else self.n_requested
......@@ -101,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)
......@@ -113,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:
......@@ -120,3 +136,9 @@ class AverageLearner(BaseLearner):
num_bins = int(max(5, sqrt(self.npoints)))
vals = hv.Points(vals)
return hv.operation.histogram(vals, num_bins=num_bins, dimension=1)
def _get_data(self):
return (self.data, self.npoints, self.sum_f, self.sum_f_sq)
def _set_data(self, data):
self.data, self.npoints, self.sum_f, self.sum_f_sq = data
......@@ -3,10 +3,13 @@ from collections import defaultdict
from contextlib import suppress
from functools import partial
from operator import itemgetter
import os.path
import numpy as np
from .base_learner import BaseLearner
from ..notebook_integration import ensure_holoviews
from ..utils import restore, named_product
from ..utils import cache_latest, named_product, restore
def dispatch(child_functions, arg):
......@@ -19,34 +22,52 @@ 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
in `plot`.
Example inputs that all give identical results:
- sequence of dicts:
>>> cdims = [{'A': True, 'B': 0},
... {'A': True, 'B': 1},
... {'A': False, 'B': 0},
... {'A': False, 'B': 1}]`
- tuple with (keys, iterable of values):
>>> cdims = (['A', 'B'], itertools.product([True, False], [0, 1]))
>>> 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
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.
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):
def __init__(self, learners, *, cdims=None, strategy='loss_improvements'):
self.learners = learners
# Naively we would make 'function' a method, but this causes problems
......@@ -61,9 +82,35 @@ class BalancingLearner(BaseLearner):
if len(set(learner.__class__ for learner in self.learners)) > 1:
raise TypeError('A BalacingLearner can handle only one type'
'of learners.')
' of learners.')
self.strategy = strategy
@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
def strategy(self, strategy):
self._strategy = strategy
if strategy == 'loss_improvements':
self._ask_and_tell = self._ask_and_tell_based_on_loss_improvements
elif strategy == 'loss':
self._ask_and_tell = self._ask_and_tell_based_on_loss
elif strategy == 'npoints':
self._ask_and_tell = self._ask_and_tell_based_on_npoints
else:
raise ValueError(
'Only strategy="loss_improvements", strategy="loss", or'
' strategy="npoints" is implemented.')
def _ask_and_tell(self, n):
def _ask_and_tell_based_on_loss_improvements(self, n):
points = []
loss_improvements = []
for _ in range(n):
......@@ -84,6 +131,32 @@ class BalancingLearner(BaseLearner):
return points, loss_improvements
def _ask_and_tell_based_on_loss(self, n):
points = []
loss_improvements = []
for _ in range(n):
losses = self._losses(real=False)
max_ind = np.argmax(losses)
xs, ls = self.learners[max_ind].ask(1)
points.append((max_ind, xs[0]))
loss_improvements.append(ls[0])
return points, loss_improvements
def _ask_and_tell_based_on_npoints(self, n):
points = []
loss_improvements = []
npoints = [l.npoints + len(l.pending_points)
for l in self.learners]
n_left = n
while n_left > 0:
i = np.argmin(npoints)
xs, ls = self.learners[i].ask(1)
npoints[i] += 1
n_left -= 1
points.append((i, xs[0]))
loss_improvements.append(ls[0])
return points, loss_improvements
def ask(self, n, tell_pending=True):
"""Chose points for learners."""
if not tell_pending:
......@@ -105,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
......@@ -116,11 +189,12 @@ class BalancingLearner(BaseLearner):
return losses
@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):
def plot(self, cdims=None, plotter=None, dynamic=True):
"""Returns a DynamicMap with sliders.
Parameters
......@@ -128,22 +202,34 @@ class BalancingLearner(BaseLearner):
cdims : sequence of dicts, or (keys, iterable of values), optional
Constant dimensions; the parameters that label the learners.
Example inputs that all give identical results:
- sequence of dicts:
>>> cdims = [{'A': True, 'B': 0},
... {'A': True, 'B': 1},
... {'A': False, 'B': 0},
... {'A': False, 'B': 1}]`
- tuple with (keys, iterable of values):
>>> cdims = (['A', 'B'], itertools.product([True, False], [0, 1]))
>>> cdims = (['A', 'B'], [(True, 0), (True, 1),
... (False, 0), (False, 1)])
plotter : callable, optional
A function that takes the learner as a argument and returns a
holoviews object. By default learner.plot() will be called.
holoviews object. By default ``learner.plot()`` will be called.
dynamic : bool, default True
Return a `holoviews.core.DynamicMap` if True, else a
`holoviews.core.HoloMap`. The `~holoviews.core.DynamicMap` is
rendered as the sliders change and can therefore not be exported
to html. The `~holoviews.core.HoloMap` does not have this problem.
Returns
-------
dm : holoviews.DynamicMap object
A DynamicMap with sliders that are defined by 'cdims'.
dm : `holoviews.core.DynamicMap` (default) or `holoviews.core.HoloMap`
A `DynamicMap` ``(dynamic=True)`` or `HoloMap`
``(dynamic=False)`` with sliders that are defined by `cdims`.
"""
hv = ensure_holoviews()
cdims = cdims or self._cdims_default
......@@ -168,7 +254,15 @@ class BalancingLearner(BaseLearner):
return learner.plot() if plotter is None else plotter(learner)
dm = hv.DynamicMap(plot_function, kdims=list(d.keys()))
return dm.redim.values(**d)
dm = dm.redim.values(**d)
if dynamic:
return dm
else:
# XXX: change when https://github.com/ioam/holoviews/issues/3085
# is fixed.
vals = {d.name: d.values for d in dm.dimensions() if d.values}
return hv.HoloMap(dm.select(**vals))
def remove_unfinished(self):
"""Remove uncomputed data from the learners."""
......@@ -179,13 +273,13 @@ class BalancingLearner(BaseLearner):
def from_product(cls, f, learner_type, learner_kwargs, combos):
"""Create a `BalancingLearner` with learners of all combinations of
named variables’ values. The `cdims` will be set correctly, so calling
`learner.plot` will be a `holoviews.HoloMap` with the correct labels.
`learner.plot` will be a `holoviews.core.HoloMap` with the correct labels.
Parameters
----------
f : callable
Function to learn, must take arguments provided in in `combos`.
learner_type : BaseLearner
learner_type : `BaseLearner`
The learner that should wrap the function. For example `Learner1D`.
learner_kwargs : dict
Keyword argument for the `learner_type`. For example `dict(bounds=[0, 1])`.
......@@ -214,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)
......@@ -222,3 +316,75 @@ class BalancingLearner(BaseLearner):
learner = learner_type(function=partial(f, **combo), **learner_kwargs)
learners.append(learner)
return cls(learners, cdims=arguments)
def save(self, folder, compress=True):
"""Save the data of the child learners into pickle files
in a directory.
Parameters
----------
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
using compression, one must load it with compression too.
Notes
-----
The child learners need to have a 'fname' attribute in order to use
this method.
Example
-------
>>> def combo_fname(val):
... return '__'.join([f'{k}_{v}.p' for k, v in val.items()])
...
... def f(x, a, b): return a * x**2 + b
...
>>> learners = []
>>> for combo in adaptive.utils.named_product(a=[1, 2], b=[1]):
... l = Learner1D(functools.partial(f, combo=combo))
... l.fname = combo_fname(combo) # 'a_1__b_1.p', 'a_2__b_1.p' etc.
... learners.append(l)
... learner = BalancingLearner(learners)
... # Run the learner
... runner = adaptive.Runner(learner)
... # Then save
... learner.save('data_folder') # use 'load' in the same way
"""
if len(self.learners) != len(set(l.fname for l in self.learners)):
raise RuntimeError("The 'learner.fname's are not all unique.")
for l in self.learners:
l.save(os.path.join(folder, l.fname), compress=compress)
def load(self, folder, compress=True):
"""Load the data of the child learners from pickle files
in a directory.
Parameters
----------
folder : str
Directory from which the learners's data will be loaded.
compress : bool, default True
If the data is compressed when saved, one must load it
with compression too.
Notes
-----
The child learners need to have a 'fname' attribute in order to use
this method.
Example
-------
See the example in the `BalancingLearner.save` doc-string.
"""
for l in self.learners:
l.load(os.path.join(folder, l.fname), compress=compress)
def _get_data(self):
return [l._get_data() for l in learner.learners]
def _set_data(self, data):
for l, _data in zip(self.learners, data):
l._set_data(_data)
# -*- coding: utf-8 -*-
import abc
import collections
from contextlib import suppress
from copy import deepcopy
from ..utils import save, load
class BaseLearner(metaclass=abc.ABCMeta):
"""Base class for algorithms for learning a function 'f: X → Y'.
......@@ -12,14 +14,19 @@ class BaseLearner(metaclass=abc.ABCMeta):
function : callable: X → Y
The function to learn.
data : dict: X → Y
'function' evaluated at certain points.
`function` evaluated at certain points.
The values can be 'None', which indicates that the point
will be evaluated, but that we do not have the result yet.
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.
Subclasses may define a 'plot' method that takes no parameters
Notes
-----
Subclasses may define a ``plot`` method that takes no parameters
and returns a holoviews plot.
"""
......@@ -76,15 +83,94 @@ class BaseLearner(metaclass=abc.ABCMeta):
n : int
The number of points to choose.
tell_pending : bool, default: True
If True, add the chosen points to this
learner's 'data' with 'None' for the 'y'
values. Set this to False if you do not
If True, add the chosen points to this learner's
`pending_points`. Set this to False if you do not
want to modify the state of the learner.
"""
pass
@abc.abstractmethod
def _get_data(self):
pass
@abc.abstractmethod
def _set_data(self):
pass
def copy_from(self, other):
"""Copy over the data from another learner.
Parameters
----------
other : BaseLearner object
The learner from which the data is copied.
"""
self._set_data(other._get_data())
def save(self, fname=None, compress=True):
"""Save the data of the learner into a pickle file.
Parameters
----------
fname : str, optional
The filename of the learner's pickle data file. If None use
the 'fname' attribute, like 'learner.fname = "example.p".
compress : bool, default True
Compress the data upon saving using 'gzip'. When saving
using compression, one must load it with compression too.
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()``.
"""
fname = fname or self.fname
data = self._get_data()
save(fname, data, compress)
def load(self, fname=None, compress=True):
"""Load the data of a learner from a pickle file.
Parameters
----------
fname : str, optional
The filename of the saved learner's pickled data file.
If None use the 'fname' attribute, like
'learner.fname = "example.p".
compress : bool, default True
If the data is compressed when saved, one must load it
with compression too.
Notes
-----
See the notes in the `save` doc-string.
"""
fname = fname or self.fname
with suppress(FileNotFoundError, EOFError):
data = load(fname, compress)
self._set_data(data)
def __getstate__(self):
return deepcopy(self.__dict__)
def __setstate__(self, state):
self.__dict__ = state
@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
except AttributeError:
raise AttributeError("Set 'learner.fname' or use the 'fname'"
" argument when using 'learner.save' or 'learner.load'.")
@fname.setter
def fname(self, fname):
self._fname = fname
# -*- coding: utf-8 -*-
from collections import OrderedDict
import functools
from .base_learner import BaseLearner
from ..utils import copy_docstring_from
class DataSaver:
"""Save extra data associated with the values that need to be learned.
Parameters
----------
learner : Learner object
learner : `~adaptive.BaseLearner` instance
The learner that needs to be wrapped.
arg_picker : function
Function that returns the argument that needs to be learned.
......@@ -16,10 +19,11 @@ class DataSaver:
Example
-------
Imagine we have a function that returns a dictionary
of the form: `{'y': y, 'err_est': err_est}`.
of the form: ``{'y': y, 'err_est': err_est}``.
>>> from operator import itemgetter
>>> _learner = Learner1D(f, bounds=(-1.0, 1.0))
>>> learner = DataSaver(_learner, arg_picker=operator.itemgetter('y'))
>>> learner = DataSaver(_learner, arg_picker=itemgetter('y'))
"""
def __init__(self, learner, arg_picker):
......@@ -31,14 +35,35 @@ 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)
def _get_data(self):
return self.learner._get_data(), self.extra_data
def _set_data(self, data):
learner_data, self.extra_data = data
self.learner._set_data(learner_data)
@copy_docstring_from(BaseLearner.save)
def save(self, fname=None, compress=True):
# We copy this method because the 'DataSaver' is not a
# subclass of the 'BaseLearner'.
BaseLearner.save(self, fname, compress)
@copy_docstring_from(BaseLearner.load)
def load(self, fname=None, compress=True):
# We copy this method because the 'DataSaver' is not a
# subclass of the 'BaseLearner'.
BaseLearner.load(self, fname, compress)
def _ds(learner_type, arg_picker, *args, **kwargs):
args = args[2:] # functools.partial passes the first 2 arguments in 'args'!
......@@ -46,12 +71,12 @@ def _ds(learner_type, arg_picker, *args, **kwargs):
def make_datasaver(learner_type, arg_picker):
"""Create a DataSaver of a `learner_type` that can be instantiated
"""Create a `DataSaver` of a `learner_type` that can be instantiated
with the `learner_type`'s key-word arguments.
Parameters
----------
learner_type : BaseLearner
learner_type : `~adaptive.BaseLearner` type
The learner type that needs to be wrapped.
arg_picker : function
Function that returns the argument that needs to be learned.
......@@ -59,14 +84,16 @@ def make_datasaver(learner_type, arg_picker):
Example
-------
Imagine we have a function that returns a dictionary
of the form: `{'y': y, 'err_est': err_est}`.
of the form: ``{'y': y, 'err_est': err_est}``.
>>> DataSaver = make(Learner1D, arg_picker=operator.itemgetter('y'))
>>> from operator import itemgetter
>>> DataSaver = make_datasaver(Learner1D, arg_picker=itemgetter('y'))
>>> learner = DataSaver(function=f, bounds=(-1.0, 1.0))
Or when using `BalacingLearner.from_product`:
Or when using `adaptive.BalancingLearner.from_product`:
>>> learner_type = make_datasaver(adaptive.Learner1D,
... arg_picker=operator.itemgetter('y'))
... arg_picker=itemgetter('y'))
>>> learner = adaptive.BalancingLearner.from_product(
... jacobi, learner_type, dict(bounds=(0, 1)), combos)
"""
......
......@@ -15,7 +15,7 @@ from .integrator_coeffs import (b_def, T_left, T_right, ns, hint,
ndiv_max, min_sep, eps, xi, V_inv,
Vcond, alpha, gamma)
from ..notebook_integration import ensure_holoviews
from ..utils import restore
from ..utils import cache_latest, restore
def _downdate(c, nans, depth):
......@@ -206,7 +206,7 @@ class _Interval:
div = (self.parent.c00 and self.c00 / self.parent.c00 > 2)
self.ndiv += div
if self.ndiv > ndiv_max and 2*self.ndiv > self.rdepth:
if self.ndiv > ndiv_max and 2 * self.ndiv > self.rdepth:
raise DivergentIntegralError
if div:
......@@ -215,7 +215,7 @@ class _Interval:
def update_ndiv_recursively(self):
self.ndiv += 1
if self.ndiv > ndiv_max and 2*self.ndiv > self.rdepth:
if self.ndiv > ndiv_max and 2 * self.ndiv > self.rdepth:
raise DivergentIntegralError
for child in self.children:
......@@ -330,7 +330,7 @@ class IntegratorLearner(BaseLearner):
The integral value in `self.bounds`.
err : float
The absolute error associated with `self.igral`.
max_ivals : int, default 1000
max_ivals : int, default: 1000
Maximum number of intervals that can be present in the calculation
of the integral. If this amount exceeds max_ivals, the interval
with the smallest error will be discarded.
......@@ -418,7 +418,6 @@ class IntegratorLearner(BaseLearner):
self._stack.append(x)
self.ivals.add(ival)
def ask(self, n, tell_pending=True):
"""Choose points for learners."""
if not tell_pending:
......@@ -488,6 +487,7 @@ class IntegratorLearner(BaseLearner):
@property
def npoints(self):
"""Number of evaluated points."""
return len(self.done_points)
@property
......@@ -514,6 +514,7 @@ class IntegratorLearner(BaseLearner):
or (err - err_excess < abs(igral) * self.tol < err_excess)
or not self.ivals)
@cache_latest
def loss(self, real=True):
return abs(abs(self.igral) * self.tol - self.err)
......@@ -525,3 +526,30 @@ class IntegratorLearner(BaseLearner):
xs, ys = zip(*[(x, y) for ival in ivals
for x, y in sorted(ival.done_points.items())])
return hv.Path((xs, ys))
def _get_data(self):
# Change the defaultdict of SortedSets to a normal dict of sets.
x_mapping = {k: set(v) for k, v in self.x_mapping.items()}
return (self.priority_split,
self.done_points,
self.pending_points,
self._stack,
x_mapping,
self.ivals,
self.first_ival)
def _set_data(self, data):
self.priority_split, self.done_points, self.pending_points, \
self._stack, x_mapping, self.ivals, self.first_ival = data
# Add the pending_points to the _stack such that they are evaluated again
for x in self.pending_points:
if x not in self._stack:
self._stack.append(x)
# x_mapping is a data structure that can't easily be saved
# so we recreate it here
self.x_mapping = defaultdict(lambda: SortedSet([], key=attrgetter('rdepth')))
for k, _set in x_mapping.items():
self.x_mapping[k].update(_set)
......@@ -7,8 +7,9 @@ import math
import numpy as np
import sortedcontainers
from ..notebook_integration import ensure_holoviews
from .base_learner import BaseLearner
from ..notebook_integration import ensure_holoviews
from ..utils import cache_latest
def uniform_loss(interval, scale, function_values):
......@@ -33,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
......@@ -48,7 +49,7 @@ def default_loss(interval, scale, function_values):
else:
dy = (y_right - y_left) / y_scale
try:
_ = len(dy)
len(dy)
loss = np.hypot(dx, dy).max()
except TypeError:
loss = math.hypot(dx, dy)
......@@ -93,17 +94,24 @@ class Learner1D(BaseLearner):
If not provided, then a default is used, which uses the scaled distance
in the x-y plane as the loss. See the notes for more details.
Attributes
----------
data : dict
Sampled points and values.
pending_points : set
Points that still have to be evaluated.
Notes
-----
'loss_per_interval' takes 3 parameters: interval, scale, and function_values,
and returns a scalar; the loss over the interval.
`loss_per_interval` takes 3 parameters: ``interval``, ``scale``, and
``function_values``, and returns a scalar; the loss over the interval.
interval : (float, float)
The bounds of the interval.
scale : (float, float)
The x and y scale over all the intervals, useful for rescaling the
interval loss.
function_values : dict(float -> float)
function_values : dict(float float)
A map containing evaluated function values. It is guaranteed
to have values for both of the points in 'interval'.
"""
......@@ -140,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()))
......@@ -154,13 +168,15 @@ class Learner1D(BaseLearner):
@property
def npoints(self):
"""Number of evaluated points."""
return len(self.data)
@cache_latest
def loss(self, real=True):
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:
......@@ -178,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).
......@@ -193,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.
......@@ -221,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)
......@@ -230,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
......@@ -282,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)
......@@ -300,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):
......@@ -361,7 +377,7 @@ class Learner1D(BaseLearner):
x_left, x_right = ival
a, b = to_interpolate[-1] if to_interpolate else (None, None)
if b == x_left and (a, b) not in self.losses:
# join (a, b) and (x_left, x_right) --> (a, x_right)
# join (a, b) and (x_left, x_right) (a, x_right)
to_interpolate[-1] = (a, x_right)
else:
to_interpolate.append((x_left, x_right))
......@@ -370,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:
......@@ -383,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
......@@ -407,7 +423,7 @@ class Learner1D(BaseLearner):
# If the loss is infinite we return the
# distance between the two points.
return (loss if not math.isinf(loss)
else (xs[1] - xs[0]) / self._scale[0])
else (xs[1] - xs[0]) / self._scale[0])
quals = [(-finite_loss(loss, x), x, 1)
for x, loss in self.losses_combined.items()]
......@@ -457,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([])
......@@ -476,3 +500,9 @@ class Learner1D(BaseLearner):
self.pending_points = set()
self.losses_combined = deepcopy(self.losses)
self.neighbors_combined = deepcopy(self.neighbors)
def _get_data(self):
return self.data
def _set_data(self, data):
self.tell_many(*zip(*data.items()))
......@@ -7,13 +7,27 @@ from math import sqrt
import numpy as np
from scipy import interpolate
from ..notebook_integration import ensure_holoviews
from .base_learner import BaseLearner
from ..notebook_integration import ensure_holoviews
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)
......@@ -36,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
......@@ -62,8 +90,8 @@ def uniform_loss(ip):
def resolution_loss(ip, min_distance=0, max_distance=1):
"""Loss function that is similar to the default loss function, but you can
set the maximimum and minimum size of a triangle.
"""Loss function that is similar to the `default_loss` function, but you
can set the maximimum and minimum size of a triangle.
Works with `~adaptive.Learner2D` only.
......@@ -85,13 +113,13 @@ def resolution_loss(ip, min_distance=0, max_distance=1):
"""
A = areas(ip)
dev = np.sum(deviations(ip), axis=0)
# similar to the default_loss
loss = np.sqrt(A) * dev + A
# Setting areas with a small area to zero such that they won't be chosen again
loss[A < min_distance**2] = 0
loss[A < min_distance**2] = 0
# Setting triangles that have a size larger than max_distance to infinite loss
# such that these triangles will be picked
loss[A > max_distance**2] = np.inf
......@@ -99,6 +127,43 @@ def resolution_loss(ip, min_distance=0, max_distance=1):
return loss
def minimize_triangle_surface_loss(ip):
"""Loss function that is similar to the default loss function in the
`~adaptive.Learner1D`. The loss is the area spanned by the 3D
vectors of the vertices.
Works with `~adaptive.Learner2D` only.
Examples
--------
>>> from adaptive.learner.learner2D import minimize_triangle_surface_loss
>>> def f(xy):
... x, y = xy
... return x**2 + y**2
>>>
>>> learner = adaptive.Learner2D(f, bounds=[(-1, -1), (1, 1)],
... loss_per_triangle=minimize_triangle_surface_loss)
>>>
"""
tri = ip.tri
points = tri.points[tri.vertices]
values = ip.values[tri.vertices]
values = values / (ip.values.ptp(axis=0).max() or 1)
def _get_vectors(points):
delta = points - points[:, -1, :][:, None, :]
vectors = delta[:, :2, :]
return vectors[:, 0, :], vectors[:, 1, :]
a_xy, b_xy = _get_vectors(points)
a_z, b_z = _get_vectors(values)
a = np.hstack([a_xy, a_z])
b = np.hstack([b_xy, b_z])
return np.linalg.norm(np.cross(a, b) / 2, axis=1)
def default_loss(ip):
dev = np.sum(deviations(ip), axis=0)
A = areas(ip)
......@@ -169,15 +234,15 @@ class Learner2D(BaseLearner):
pending_points : set
Points that still have to be evaluated and are currently
interpolated, see `data_combined`.
stack_size : int, default 10
stack_size : int, default: 10
The size of the new candidate points stack. Set it to 1
to recalculate the best points at each call to `ask`.
aspect_ratio : float, int, default 1
Average ratio of `x` span over `y` span of a triangle. If
there is more detail in either `x` or `y` the `aspect_ratio`
needs to be adjusted. When `aspect_ratio > 1` the
triangles will be stretched along `x`, otherwise
along `y`.
aspect_ratio : float, int, default: 1
Average ratio of ``x`` span over ``y`` span of a triangle. If
there is more detail in either ``x`` or ``y`` the ``aspect_ratio``
needs to be adjusted. When ``aspect_ratio > 1`` the
triangles will be stretched along ``x``, otherwise
along ``y``.
Methods
-------
......@@ -202,13 +267,13 @@ class Learner2D(BaseLearner):
This sampling procedure is not extremely fast, so to benefit from
it, your function needs to be slow enough to compute.
'loss_per_triangle' takes a single parameter, 'ip', which is a
`loss_per_triangle` takes a single parameter, `ip`, which is a
`scipy.interpolate.LinearNDInterpolator`. You can use the
*undocumented* attributes 'tri' and 'values' of 'ip' to get a
*undocumented* attributes ``tri`` and ``values`` of `ip` to get a
`scipy.spatial.Delaunay` and a vector of function values.
These can be used to compute the loss. The functions
`adaptive.learner.learner2D.areas` and
`adaptive.learner.learner2D.deviations` to calculate the
`~adaptive.learner.learner2D.areas` and
`~adaptive.learner.learner2D.deviations` to calculate the
areas and deviations from a linear interpolation
over each triangle.
"""
......@@ -230,7 +295,6 @@ class Learner2D(BaseLearner):
self._stack.update({p: np.inf for p in self._bounds_points})
self.function = function
self._ip = self._ip_combined = None
self._loss = np.inf
self.stack_size = 10
......@@ -252,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()))
......@@ -300,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)
......@@ -312,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)
......@@ -401,13 +479,13 @@ class Learner2D(BaseLearner):
return points[:n], loss_improvements[:n]
@cache_latest
def loss(self, real=True):
if not self.bounds_are_done:
return np.inf
ip = self.ip() if real else self.ip_combined()
losses = self.loss_per_triangle(ip)
self._loss = losses.max()
return self._loss
return losses.max()
def remove_unfinished(self):
self.pending_points = set()
......@@ -428,19 +506,21 @@ class Learner2D(BaseLearner):
Number of points in x and y. If None (default) this number is
evaluated by looking at the size of the smallest triangle.
tri_alpha : float
The opacity (0 <= tri_alpha <= 1) of the triangles overlayed on
top of the image. By default the triangulation is not visible.
The opacity ``(0 <= tri_alpha <= 1)`` of the triangles overlayed
on top of the image. By default the triangulation is not visible.
Returns
-------
plot : holoviews.Overlay or holoviews.HoloMap
A `holoviews.Overlay` of `holoviews.Image * holoviews.EdgePaths`.
If the `learner.function` returns a vector output, a
`holoviews.HoloMap` of the `holoviews.Overlay`s wil be returned.
plot : `holoviews.core.Overlay` or `holoviews.core.HoloMap`
A `holoviews.core.Overlay` of
``holoviews.Image * holoviews.EdgePaths``. If the
`learner.function` returns a vector output, a
`holoviews.core.HoloMap` of the
`holoviews.core.Overlay`\s wil be returned.
Notes
-----
The plot object that is returned if `learner.function` returns a
The plot object that is returned if ``learner.function`` returns a
vector *cannot* be used with the live_plotting functionality.
"""
hv = ensure_holoviews()
......@@ -484,3 +564,13 @@ class Learner2D(BaseLearner):
no_hover = dict(plot=dict(inspection_policy=None, tools=[]))
return im.opts(style=im_opts) * tris.opts(style=tri_opts, **no_hover)
def _get_data(self):
return self.data
def _set_data(self, data):
self.data = data
# Remove points from stack if they already exist
for point in copy(self._stack):
if point in self.data:
self._stack.pop(point)
......@@ -10,10 +10,10 @@ import scipy.spatial
from .base_learner import BaseLearner
from ..notebook_integration import ensure_holoviews
from ..notebook_integration import ensure_holoviews, ensure_plotly
from .triangulation import (Triangulation, point_in_simplex,
circumsphere, simplex_volume_in_embedding)
from ..utils import restore
from ..utils import restore, cache_latest
def volume(simplex, ys=None):
......@@ -59,8 +59,8 @@ def default_loss(simplex, ys):
def choose_point_in_simplex(simplex, transform=None):
"""Choose a new point in inside a simplex.
Pick the center of the simplex if the shape is nice (that is, the
circumcenter lies within the simplex). Otherwise take the middle of the
Pick the center of the simplex if the shape is nice (that is, the
circumcenter lies within the simplex). Otherwise take the middle of the
longest edge.
Parameters
......@@ -94,7 +94,7 @@ def choose_point_in_simplex(simplex, transform=None):
if transform is not None:
point = np.linalg.solve(transform, point) # undo the transform
return point
......@@ -124,15 +124,8 @@ class LearnerND(BaseLearner):
Coordinates of the currently known points
values : numpy array
The values of each of the known points
Methods
-------
plot(n)
If dim == 2, this method will plot the function being learned.
plot_slice(cut_mapping, n)
plot a slice of the function using interpolation of the current data.
the cut_mapping contains the fixed parameters, the other parameters are
used as axes for plotting.
pending_points : set
Points that still have to be evaluated.
Notes
-----
......@@ -161,7 +154,7 @@ class LearnerND(BaseLearner):
self.loss_per_simplex = loss_per_simplex or default_loss
self.bounds = tuple(tuple(map(float, b)) for b in bounds)
self.data = OrderedDict()
self._pending = set()
self.pending_points = set()
self._bounds_points = list(map(tuple, itertools.product(*bounds)))
......@@ -169,10 +162,10 @@ class LearnerND(BaseLearner):
self._tri = None
self._losses = dict()
self._pending_to_simplex = dict() # vertex -> simplex
self._pending_to_simplex = dict() # vertex simplex
# triangulation of the pending points inside a specific simplex
self._subtriangulations = dict() # simplex -> triangulation
self._subtriangulations = dict() # simplex triangulation
# scale to unit
self._transform = np.linalg.inv(np.diag(np.diff(bounds).flat))
......@@ -180,7 +173,7 @@ class LearnerND(BaseLearner):
# create a private random number generator with fixed seed
self._random = random.Random(1)
# all real triangles that have not been subdivided and the pending
# all real triangles that have not been subdivided and the pending
# triangles heap of tuples (-loss, real simplex, sub_simplex or None)
# _simplex_queue is a heap of tuples (-loss, real_simplex, sub_simplex)
......@@ -195,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()))
......@@ -212,11 +212,15 @@ 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)
@property
def tri(self):
"""An `adaptive.learner.triangulation.Triangulation` instance
with all the points of the learner."""
if self._tri is not None:
return self._tri
......@@ -226,16 +230,18 @@ class LearnerND(BaseLearner):
return self._tri
except ValueError:
# A ValueError is raised if we do not have enough points or
# the provided points are coplanar, so we need more points to create
# a valid triangulation
# the provided points are coplanar, so we need more points to
# create a valid triangulation
return None
@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):
......@@ -247,7 +253,7 @@ class LearnerND(BaseLearner):
if value is None:
return self.tell_pending(point)
self._pending.discard(point)
self.pending_points.discard(point)
tri = self.tri
self.data[point] = value
......@@ -267,14 +273,15 @@ 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):
point = tuple(point)
if not self.inside_bounds(point):
return
self._pending.add(point)
self.pending_points.add(point)
if self.tri is None:
return
......@@ -333,7 +340,7 @@ class LearnerND(BaseLearner):
def _ask_bound_point(self):
# get the next bound point that is still available
new_point = next(p for p in self._bounds_points
if p not in self.data and p not in self._pending)
if p not in self.data and p not in self.pending_points)
self.tell_pending(new_point)
return new_point, np.inf
......@@ -354,21 +361,21 @@ class LearnerND(BaseLearner):
# find the simplex with the highest loss, we do need to check that the
# simplex hasn't been deleted yet
while len(self._simplex_queue):
loss, simplex, subsimplex = heapq.heappop(self._simplex_queue)
loss, simplex, subsimplex = heapq.heappop(self._simplex_queue)
if (subsimplex is None
and simplex in self.tri.simplices
and simplex not in self._subtriangulations):
and simplex in self.tri.simplices
and simplex not in self._subtriangulations):
return abs(loss), simplex, subsimplex
if (simplex in self._subtriangulations
and simplex in self.tri.simplices
and subsimplex in self._subtriangulations[simplex].simplices):
and simplex in self.tri.simplices
and subsimplex in self._subtriangulations[simplex].simplices):
return abs(loss), simplex, subsimplex
# Could not find a simplex, this code should never be reached
assert self.tri is not None
raise AssertionError(
"Could not find a simplex to subdivide. Yet there should always be"
"a simplex available if LearnerND.tri() is not None."
"Could not find a simplex to subdivide. Yet there should always"
" be a simplex available if LearnerND.tri() is not None."
)
def _ask_best_point(self):
......@@ -394,7 +401,7 @@ class LearnerND(BaseLearner):
@property
def _bounds_available(self):
return any((p not in self._pending and p not in self.data)
return any((p not in self.pending_points and p not in self.data)
for p in self._bounds_points)
def _ask(self):
......@@ -435,8 +442,8 @@ class LearnerND(BaseLearner):
heapq.heappush(self._simplex_queue, (-loss, simplex, None))
continue
self._update_subsimplex_losses(simplex,
self._subtriangulations[simplex].simplices)
self._update_subsimplex_losses(
simplex, self._subtriangulations[simplex].simplices)
def losses(self):
"""Get the losses of each simplex in the current triangulation, as dict
......@@ -452,13 +459,14 @@ class LearnerND(BaseLearner):
return self._losses
@cache_latest
def loss(self, real=True):
losses = self.losses() # XXX: compute pending loss if real == False
return max(losses.values()) if losses else float('inf')
def remove_unfinished(self):
# XXX: implement this method
self._pending = set()
self.pending_points = set()
self._subtriangulations = dict()
self._pending_to_simplex = dict()
......@@ -516,13 +524,14 @@ class LearnerND(BaseLearner):
return im.opts(style=im_opts) * tris.opts(style=tri_opts, **no_hover)
def plot_slice(self, cut_mapping, n=None):
"""Plot a 1d or 2d interpolated slice of a N-dimensional function.
"""Plot a 1D or 2D interpolated slice of a N-dimensional function.
Parameters
----------
cut_mapping : dict (int -> float)
cut_mapping : dict (int float)
for each fixed dimension the value, the other dimensions
are interpolated
are interpolated. e.g. ``cut_mapping = {0: 1}``, so from
dimension 0 ('x') to value 1.
n : int
the number of boxes in the interpolation grid along each axis
"""
......@@ -575,3 +584,72 @@ class LearnerND(BaseLearner):
return im.opts(style=dict(cmap='viridis'))
else:
raise ValueError("Only 1 or 2-dimensional plots can be generated.")
def plot_3D(self, with_triangulation=False):
"""Plot the learner's data in 3D using plotly.
Parameters
----------
with_triangulation : bool, default: False
Add the verticices to the plot.
Returns
-------
plot : plotly.offline.iplot object
The 3D plot of ``learner.data``.
"""
plotly = ensure_plotly()
plots = []
vertices = self.tri.vertices
if with_triangulation:
Xe, Ye, Ze = [], [], []
for simplex in self.tri.simplices:
for s in itertools.combinations(simplex, 2):
Xe += [vertices[i][0] for i in s] + [None]
Ye += [vertices[i][1] for i in s] + [None]
Ze += [vertices[i][2] for i in s] + [None]
plots.append(plotly.graph_objs.Scatter3d(
x=Xe, y=Ye, z=Ze, mode='lines',
line=dict(color='rgb(125,125,125)', width=1),
hoverinfo='none'
))
Xn, Yn, Zn = zip(*vertices)
colors = [self.data[p] for p in self.tri.vertices]
marker = dict(symbol='circle', size=3, color=colors,
colorscale='Viridis',
line=dict(color='rgb(50,50,50)', width=0.5))
plots.append(plotly.graph_objs.Scatter3d(
x=Xn, y=Yn, z=Zn, mode='markers',
name='actors', marker=marker,
hoverinfo='text'
))
axis = dict(
showbackground=False,
showline=False,
zeroline=False,
showgrid=False,
showticklabels=False,
title='',
)
layout = plotly.graph_objs.Layout(
showlegend=False,
scene=dict(xaxis=axis, yaxis=axis, zaxis=axis),
margin=dict(t=100),
hovermode='closest')
fig = plotly.graph_objs.Figure(data=plots, layout=layout)
return plotly.offline.iplot(fig)
def _get_data(self):
return self.data
def _set_data(self, data):
self.tell_many(*zip(*data.items()))
# -*- coding: utf-8 -*-
import numpy as np
from skopt import Optimizer
from ..notebook_integration import ensure_holoviews
from .base_learner import BaseLearner
from skopt import Optimizer
from ..notebook_integration import ensure_holoviews
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):
self.function = function
self.pending_points = set()
super().__init__(**kwargs)
def tell(self, x, y, fit=True):
def tell(self, x, y, fit=True):
self.pending_points.discard(x)
super().tell([x], y, fit)
def tell_pending(self, x):
# 'skopt.Optimizer' takes care of points we
# have not got results for.
pass
self.pending_points.add(x)
def remove_unfinished(self):
pass
@cache_latest
def loss(self, real=True):
if not self.models:
return np.inf
......@@ -61,6 +63,7 @@ class SKOptLearner(Optimizer, BaseLearner):
@property
def npoints(self):
"""Number of evaluated points."""
return len(self.Xi)
def plot(self, nsamples=200):
......@@ -97,3 +100,10 @@ class SKOptLearner(Optimizer, BaseLearner):
plot_bounds = (bounds[0] - margin, bounds[1] + margin)
return p.redim(x=dict(range=plot_bounds))
def _get_data(self):
return [x[0] for x in self.Xi], self.yi
def _set_data(self, data):
xs, ys = data
self.tell_many(xs, ys)
......@@ -214,7 +214,7 @@ def simplex_volume_in_embedding(vertices) -> float:
# Heron's formula
a, b, c = scipy.spatial.distance.pdist(vertices, metric='euclidean')
s = 0.5 * (a + b + c)
return math.sqrt(s*(s-a)*(s-b)*(s-c))
return math.sqrt(s * (s - a) * (s - b) * (s - c))
# β_ij = |v_i - v_k|²
sq_dists = scipy.spatial.distance.pdist(vertices, metric='sqeuclidean')
......@@ -409,8 +409,10 @@ class Triangulation:
if orientation_inside == -orientation_new_point:
# if the orientation of the new vertex is zero or directed
# towards the center, do not add the simplex
self.add_simplex((*face, pt_index))
new_simplices.add((*face, pt_index))
simplex = (*face, pt_index)
if not self._simplex_is_almost_flat(simplex):
self.add_simplex(simplex)
new_simplices.add(simplex)
if len(new_simplices) == 0:
# We tried to add an internal point, revert and raise.
......@@ -492,13 +494,13 @@ class Triangulation:
# Get all simplices that share at least a point with the simplex
neighbours = set.union(*[self.vertex_to_simplices[p]
for p in todo_points])
for p in todo_points])
# Filter out the already evaluated simplices
neighbours = neighbours - done_simplices
# Keep only the simplices sharing a whole face with the current simplex
neighbours = set(
simpl for simpl in neighbours
simpl for simpl in neighbours
if len(set(simpl) & set(simplex)) == self.dim # they share a face
)
queue.update(neighbours)
......@@ -510,13 +512,27 @@ class Triangulation:
for face in hole_faces:
if pt_index not in face:
if self.volume((*face, pt_index)) < 1e-8:
continue
self.add_simplex((*face, pt_index))
simplex = (*face, pt_index)
if not self._simplex_is_almost_flat(simplex):
self.add_simplex(simplex)
new_triangles = self.vertex_to_simplices[pt_index]
return bad_triangles - new_triangles, new_triangles - bad_triangles
def _simplex_is_almost_flat(self, simplex):
return self._relative_volume(simplex) < 1e-8
def _relative_volume(self, simplex):
"""Compute the volume of a simplex divided by the average (Manhattan)
distance of its vertices. The advantage of this is that the relative
volume is only dependent on the shape of the simplex and not on the
absolute size. Due to the weird scaling, the only use of this method
is to check that a simplex is almost flat."""
vertices = np.array(self.get_vertices(simplex))
vectors = vertices[1:] - vertices[0]
average_edge_length = np.mean(np.abs(vectors))
return self.volume(simplex) / (average_edge_length ** self.dim)
def add_point(self, point, simplex=None, transform=None):
"""Add a new vertex and create simplices as appropriate.
......@@ -552,7 +568,7 @@ class Triangulation:
else:
reduced_simplex = self.get_reduced_simplex(point, simplex)
if not reduced_simplex:
self.vertex_to_simplices.pop() # revert adding vertex
self.vertex_to_simplices.pop() # revert adding vertex
raise ValueError('Point lies outside of the specified simplex.')
else:
simplex = reduced_simplex
......@@ -596,7 +612,7 @@ class Triangulation:
Parameters
----------
check : bool, default True
check : bool, default: True
Whether to raise an error if the computed hull is different from
stored.
......
......@@ -3,42 +3,71 @@ import importlib
import asyncio
from contextlib import suppress
import datetime
from pkg_resources import parse_version
import warnings
_async_enabled = False
_plotting_enabled = False
_holoviews_enabled = False
_ipywidgets_enabled = False
_plotly_enabled = False
def notebook_extension():
"""Enable ipywidgets, holoviews, and asyncio notebook integration."""
if not in_ipynb():
raise RuntimeError('"adaptive.notebook_extension()" may only be run '
'from a Jupyter notebook.')
global _plotting_enabled
_plotting_enabled = False
global _async_enabled, _holoviews_enabled, _ipywidgets_enabled
# Load holoviews
try:
if not _holoviews_enabled:
import holoviews
holoviews.notebook_extension('bokeh', logo=False)
_holoviews_enabled = True
except ModuleNotFoundError:
warnings.warn("holoviews is not installed; plotting "
"is disabled.", RuntimeWarning)
# Load ipywidgets
try:
import ipywidgets
import holoviews
holoviews.notebook_extension('bokeh')
_plotting_enabled = True
if not _ipywidgets_enabled:
import ipywidgets
_ipywidgets_enabled = True
except ModuleNotFoundError:
warnings.warn("holoviews and (or) ipywidgets are not installed; plotting "
warnings.warn("ipywidgets is not installed; live_info "
"is disabled.", RuntimeWarning)
global _async_enabled
get_ipython().magic('gui asyncio')
_async_enabled = True
# Enable asyncio integration
if not _async_enabled:
get_ipython().magic('gui asyncio')
_async_enabled = True
def ensure_holoviews():
try:
return importlib.import_module('holoviews')
except ModuleNotFounderror:
except ModuleNotFoundError:
raise RuntimeError('holoviews is not installed; plotting is disabled.')
def ensure_plotly():
global _plotly_enabled
try:
import plotly
if not _plotly_enabled:
import plotly.graph_objs
import plotly.figure_factory
import plotly.offline
# This injects javascript and should happen only once
plotly.offline.init_notebook_mode()
_plotly_enabled = True
return plotly
except ModuleNotFoundError:
raise RuntimeError('plotly is not installed; plotting is disabled.')
def in_ipynb():
try:
# If we are running in IPython, then `get_ipython()` is always a global
......@@ -57,23 +86,23 @@ def live_plot(runner, *, plotter=None, update_interval=2, name=None):
Parameters
----------
runner : Runner
runner : `Runner`
plotter : function
A function that takes the learner as a argument and returns a
holoviews object. By default learner.plot() will be called.
holoviews object. By default ``learner.plot()`` will be called.
update_interval : int
Number of second between the updates of the plot.
name : hasable
Name for the `live_plot` task in `adaptive.active_plotting_tasks`.
By default the name is `None` and if another task with the same name
already exists that other live_plot is canceled.
By default the name is None and if another task with the same name
already exists that other `live_plot` is canceled.
Returns
-------
dm : holoviews.DynamicMap
The plot that automatically updates every update_interval.
dm : `holoviews.core.DynamicMap`
The plot that automatically updates every `update_interval`.
"""
if not _plotting_enabled:
if not _holoviews_enabled:
raise RuntimeError("Live plotting is not enabled; did you run "
"'adaptive.notebook_extension()'?")
......@@ -127,7 +156,7 @@ def live_info(runner, *, update_interval=0.5):
Returns an interactive ipywidget that can be
visualized in a Jupyter notebook.
"""
if not _plotting_enabled:
if not _holoviews_enabled:
raise RuntimeError("Live plotting is not enabled; did you run "
"'adaptive.notebook_extension()'?")
......@@ -174,7 +203,10 @@ def _info_html(runner):
with suppress(Exception):
info.append(('# of points', runner.learner.npoints))
template = '<dt>{}</dt><dd>{}</dd>'
with suppress(Exception):
info.append(('latest loss', f'{runner.learner._cache["loss"]:.3f}'))
template = '<dt class="ignore-css">{}</dt><dd>{}</dd>'
table = '\n'.join(template.format(k, v) for k, v in info)
return f'''
......
......@@ -54,64 +54,60 @@ else:
class BaseRunner:
"""Base class for runners that use concurrent.futures.Executors.
"""Base class for runners that use `concurrent.futures.Executors`.
Parameters
----------
learner : adaptive.learner.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 when we should
stop requesting more points.
executor : concurrent.futures.Executor, distributed.Client,
or ipyparallel.Client, optional
executor : `concurrent.futures.Executor`, `distributed.Client`,\
or `ipyparallel.Client`, optional
The executor in which to evaluate the function to be learned.
If not provided, a new `ProcessPoolExecutor` is used on Unix systems
while on Windows a `distributed.Client` is used if `distributed` is
installed.
If not provided, a new `~concurrent.futures.ProcessPoolExecutor`
is used on Unix systems while on Windows a `distributed.Client`
is used if `distributed` is installed.
ntasks : int, optional
The number of concurrent function evaluations. Defaults to the number
of cores available in 'executor'.
of cores available in `executor`.
log : bool, default: False
If True, record the method calls made to the learner by this runner.
shutdown_executor : Bool, default: False
shutdown_executor : bool, default: False
If True, shutdown the executor when the runner has completed. If
'executor' is not provided then the executor created internally
`executor` is not provided then the executor created internally
by the runner is shut down, regardless of this parameter.
retries : int, default: 0
Maximum amount of retries of a certain point 'x' in
'learner.function(x)'. After 'retries' is reached for 'x' the
point is present in 'runner.failed'.
Maximum amount of retries of a certain point ``x`` in
``learner.function(x)``. After `retries` is reached for ``x``
the point is present in ``runner.failed``.
raise_if_retries_exceeded : bool, default: True
Raise the error after a point 'x' failed 'retries'.
Raise the error after a point ``x`` failed `retries`.
Attributes
----------
learner : Learner
learner : `~adaptive.BaseLearner` instance
The underlying learner. May be queried for its state.
log : list or None
Record of the method calls made to the learner, in the format
'(method_name, *args)'.
``(method_name, *args)``.
to_retry : dict
Mapping of {point: n_fails, ...}. When a point has failed
'runner.retries' times it is removed but will be present
in 'runner.tracebacks'.
Mapping of ``{point: n_fails, ...}``. When a point has failed
``runner.retries`` times it is removed but will be present
in ``runner.tracebacks``.
tracebacks : dict
A mapping of point to the traceback if that point failed.
pending_points : dict
A mapping of 'concurrent.Future's to points, {Future: point, ...}.
A mapping of `~concurrent.futures.Future`\s to points.
Methods
-------
overhead : callable
The overhead in percent of using Adaptive. This includes the
overhead of the executor. Essentially, this is
100 * (1 - total_elapsed_function_time / self.elapsed_time()).
``100 * (1 - total_elapsed_function_time / self.elapsed_time())``.
Properties
----------
failed : set
Set of points that failed 'retries' times.
"""
def __init__(self, learner, goal, *,
......@@ -145,7 +141,7 @@ class BaseRunner:
self.to_retry = {}
self.tracebacks = {}
def max_tasks(self):
def _get_max_tasks(self):
return self._max_tasks or _get_ncores(self.executor)
def _do_raise(self, e, x):
......@@ -173,7 +169,7 @@ class BaseRunner:
def overhead(self):
"""Overhead of using Adaptive and the executor in percent.
This is measured as 100 * (1 - t_function / t_elapsed).
This is measured as ``100 * (1 - t_function / t_elapsed)``.
Notes
-----
......@@ -213,7 +209,7 @@ class BaseRunner:
# Launch tasks to replace the ones that completed
# on the last iteration, making sure to fill workers
# that have started since the last iteration.
n_new_tasks = max(0, self.max_tasks() - len(self.pending_points))
n_new_tasks = max(0, self._get_max_tasks() - len(self.pending_points))
if self.do_log:
self.log.append(('ask', n_new_tasks))
......@@ -243,57 +239,57 @@ class BaseRunner:
@property
def failed(self):
"""Set of points that failed 'self.retries' times."""
"""Set of points that failed ``runner.retries`` times."""
return set(self.tracebacks) - set(self.to_retry)
class BlockingRunner(BaseRunner):
"""Run a learner synchronously in an executor.
Parameters
----------
learner : adaptive.learner.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 when we should
stop requesting more points.
executor : concurrent.futures.Executor, distributed.Client,
or ipyparallel.Client, optional
executor : `concurrent.futures.Executor`, `distributed.Client`,\
or `ipyparallel.Client`, optional
The executor in which to evaluate the function to be learned.
If not provided, a new `ProcessPoolExecutor` is used on Unix systems
while on Windows a `distributed.Client` is used if `distributed` is
installed.
If not provided, a new `~concurrent.futures.ProcessPoolExecutor`
is used on Unix systems while on Windows a `distributed.Client`
is used if `distributed` is installed.
ntasks : int, optional
The number of concurrent function evaluations. Defaults to the number
of cores available in 'executor'.
of cores available in `executor`.
log : bool, default: False
If True, record the method calls made to the learner by this runner.
shutdown_executor : Bool, default: False
shutdown_executor : bool, default: False
If True, shutdown the executor when the runner has completed. If
'executor' is not provided then the executor created internally
`executor` is not provided then the executor created internally
by the runner is shut down, regardless of this parameter.
retries : int, default: 0
Maximum amount of retries of a certain point 'x' in
'learner.function(x)'. After 'retries' is reached for 'x' the
point is present in 'runner.failed'.
Maximum amount of retries of a certain point ``x`` in
``learner.function(x)``. After `retries` is reached for ``x``
the point is present in ``runner.failed``.
raise_if_retries_exceeded : bool, default: True
Raise the error after a point 'x' failed 'retries'.
Raise the error after a point ``x`` failed `retries`.
Attributes
----------
learner : Learner
learner : `~adaptive.BaseLearner` instance
The underlying learner. May be queried for its state.
log : list or None
Record of the method calls made to the learner, in the format
'(method_name, *args)'.
``(method_name, *args)``.
to_retry : dict
Mapping of {point: n_fails, ...}. When a point has failed
'runner.retries' times it is removed but will be present
in 'runner.tracebacks'.
Mapping of ``{point: n_fails, ...}``. When a point has failed
``runner.retries`` times it is removed but will be present
in ``runner.tracebacks``.
tracebacks : dict
A mapping of point to the traceback if that point failed.
pending_points : dict
A mapping of 'concurrent.Future's to points, {Future: point, ...}.
A mapping of `~concurrent.futures.Future`\to points.
Methods
-------
......@@ -303,12 +299,8 @@ class BlockingRunner(BaseRunner):
overhead : callable
The overhead in percent of using Adaptive. This includes the
overhead of the executor. Essentially, this is
100 * (1 - total_elapsed_function_time / self.elapsed_time()).
``100 * (1 - total_elapsed_function_time / self.elapsed_time())``.
Properties
----------
failed : set
Set of points that failed 'retries' times.
"""
def __init__(self, learner, goal, *,
......@@ -330,7 +322,7 @@ class BlockingRunner(BaseRunner):
def _run(self):
first_completed = concurrent.FIRST_COMPLETED
if self.max_tasks() < 1:
if self._get_max_tasks() < 1:
raise RuntimeError('Executor has no workers')
try:
......@@ -354,58 +346,58 @@ 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
----------
learner : adaptive.learner.BaseLearner
learner : `~adaptive.BaseLearner` instance
goal : callable, optional
The end condition for the calculation. This function must take
the learner as its sole argument, and return True when we should
stop requesting more points. If not provided, the runner will run
forever, or until 'self.task.cancel()' is called.
executor : concurrent.futures.Executor, distributed.Client,
or ipyparallel.Client, optional
forever, or until ``self.task.cancel()`` is called.
executor : `concurrent.futures.Executor`, `distributed.Client`,\
or `ipyparallel.Client`, optional
The executor in which to evaluate the function to be learned.
If not provided, a new `ProcessPoolExecutor` is used on Unix systems
while on Windows a `distributed.Client` is used if `distributed` is
installed.
If not provided, a new `~concurrent.futures.ProcessPoolExecutor`
is used on Unix systems while on Windows a `distributed.Client`
is used if `distributed` is installed.
ntasks : int, optional
The number of concurrent function evaluations. Defaults to the number
of cores available in 'executor'.
of cores available in `executor`.
log : bool, default: False
If True, record the method calls made to the learner by this runner.
shutdown_executor : Bool, default: False
shutdown_executor : bool, default: False
If True, shutdown the executor when the runner has completed. If
'executor' is not provided then the executor created internally
`executor` is not provided then the executor created internally
by the runner is shut down, regardless of this parameter.
ioloop : asyncio.AbstractEventLoop, optional
ioloop : ``asyncio.AbstractEventLoop``, optional
The ioloop in which to run the learning algorithm. If not provided,
the default event loop is used.
retries : int, default: 0
Maximum amount of retries of a certain point 'x' in
'learner.function(x)'. After 'retries' is reached for 'x' the
point is present in 'runner.failed'.
Maximum amount of retries of a certain point ``x`` in
``learner.function(x)``. After `retries` is reached for ``x``
the point is present in ``runner.failed``.
raise_if_retries_exceeded : bool, default: True
Raise the error after a point 'x' failed 'retries'.
Raise the error after a point ``x`` failed `retries`.
Attributes
----------
task : asyncio.Task
task : `asyncio.Task`
The underlying task. May be cancelled in order to stop the runner.
learner : Learner
learner : `~adaptive.BaseLearner` instance
The underlying learner. May be queried for its state.
log : list or None
Record of the method calls made to the learner, in the format
'(method_name, *args)'.
``(method_name, *args)``.
to_retry : dict
Mapping of {point: n_fails, ...}. When a point has failed
'runner.retries' times it is removed but will be present
in 'runner.tracebacks'.
Mapping of ``{point: n_fails, ...}``. When a point has failed
``runner.retries`` times it is removed but will be present
in ``runner.tracebacks``.
tracebacks : dict
A mapping of point to the traceback if that point failed.
pending_points : dict
A mapping of 'concurrent.Future's to points, {Future: point, ...}.
A mapping of `~concurrent.futures.Future`\s to points.
Methods
-------
......@@ -415,17 +407,13 @@ class AsyncRunner(BaseRunner):
overhead : callable
The overhead in percent of using Adaptive. This includes the
overhead of the executor. Essentially, this is
100 * (1 - total_elapsed_function_time / self.elapsed_time()).
``100 * (1 - total_elapsed_function_time / self.elapsed_time())``.
Properties
----------
failed : set
Set of points that failed 'retries' times.
Notes
-----
This runner can be used when an async function (defined with
'async def') has to be learned. In this case the function will be
``async def``) has to be learned. In this case the function will be
run directly on the event loop (and not in the executor).
"""
......@@ -461,6 +449,7 @@ class AsyncRunner(BaseRunner):
self.function)
self.task = self.ioloop.create_task(self._run())
self.saving_task = None
if in_ipynb() and not self.ioloop.is_running():
warnings.warn("The runner has been scheduled, but the asyncio "
"event loop is not running! If you are "
......@@ -486,7 +475,7 @@ class AsyncRunner(BaseRunner):
def cancel(self):
"""Cancel the runner.
This is equivalent to calling `runner.task.cancel()`.
This is equivalent to calling ``runner.task.cancel()``.
"""
self.task.cancel()
......@@ -495,21 +484,21 @@ class AsyncRunner(BaseRunner):
Parameters
----------
runner : Runner
runner : `Runner`
plotter : function
A function that takes the learner as a argument and returns a
holoviews object. By default learner.plot() will be called.
holoviews object. By default ``learner.plot()`` will be called.
update_interval : int
Number of second between the updates of the plot.
name : hasable
Name for the `live_plot` task in `adaptive.active_plotting_tasks`.
By default the name is `None` and if another task with the same name
already exists that other live_plot is canceled.
By default the name is None and if another task with the same name
already exists that other `live_plot` is canceled.
Returns
-------
dm : holoviews.DynamicMap
The plot that automatically updates every update_interval.
dm : `holoviews.core.DynamicMap`
The plot that automatically updates every `update_interval`.
"""
return live_plot(self, plotter=plotter,
update_interval=update_interval,
......@@ -526,7 +515,7 @@ class AsyncRunner(BaseRunner):
async def _run(self):
first_completed = asyncio.FIRST_COMPLETED
if self.max_tasks() < 1:
if self._get_max_tasks() < 1:
raise RuntimeError('Executor has no workers')
try:
......@@ -553,6 +542,32 @@ class AsyncRunner(BaseRunner):
end_time = time.time()
return end_time - self.start_time
def start_periodic_saving(self, save_kwargs, interval):
"""Periodically save the learner's data.
Parameters
----------
save_kwargs : dict
Key-word arguments for ``learner.save(**save_kwargs)``.
interval : int
Number of seconds between saving the learner.
Example
-------
>>> runner = Runner(learner)
>>> runner.start_periodic_saving(
... save_kwargs=dict(fname='data/test.pickle'),
... interval=600)
"""
async def _saver(save_kwargs=save_kwargs, interval=interval):
while self.status() == 'running':
self.learner.save(**save_kwargs)
await asyncio.sleep(interval)
self.learner.save(**save_kwargs) # one last time
self.saving_task = self.ioloop.create_task(_saver())
return self.saving_task
# Default runner
Runner = AsyncRunner
......@@ -571,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.
......@@ -590,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)
......