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 (15)
# -*- coding: utf-8 -*-
from collections import OrderedDict, Iterable
import functools
import heapq
import itertools
import random
......@@ -470,6 +471,10 @@ class LearnerND(BaseLearner):
self._subtriangulations = dict()
self._pending_to_simplex = dict()
##########################
# Plotting related stuff #
##########################
def plot(self, n=None, tri_alpha=0):
"""Plot the function we want to learn, only works in 2D.
......@@ -588,6 +593,9 @@ class LearnerND(BaseLearner):
def plot_3D(self, with_triangulation=False):
"""Plot the learner's data in 3D using plotly.
Does *not* work with the
`adaptive.notebook_integration.live_plot` functionality.
Parameters
----------
with_triangulation : bool, default: False
......@@ -595,7 +603,7 @@ class LearnerND(BaseLearner):
Returns
-------
plot : plotly.offline.iplot object
plot : `plotly.offline.iplot` object
The 3D plot of ``learner.data``.
"""
plotly = ensure_plotly()
......@@ -653,3 +661,182 @@ class LearnerND(BaseLearner):
def _set_data(self, data):
self.tell_many(*zip(*data.items()))
def _get_iso(self, level=0.0, which='surface'):
if which == 'surface':
if self.ndim != 3 or self.vdim != 1:
raise Exception('Isosurface plotting is only supported'
' for a 3D input and 1D output')
get_surface = True
get_line = False
elif which == 'line':
if self.ndim != 2 or self.vdim != 1:
raise Exception('Isoline plotting is only supported'
' for a 2D input and 1D output')
get_surface = False
get_line = True
vertices = [] # index -> (x,y,z)
faces_or_lines = [] # tuple of indices of the corner points
@functools.lru_cache()
def _get_vertex_index(a, b):
vertex_a = self.tri.vertices[a]
vertex_b = self.tri.vertices[b]
value_a = self.data[vertex_a]
value_b = self.data[vertex_b]
da = abs(value_a - level)
db = abs(value_b - level)
dab = da + db
new_pt = (db / dab * np.array(vertex_a)
+ da / dab * np.array(vertex_b))
new_index = len(vertices)
vertices.append(new_pt)
return new_index
for simplex in self.tri.simplices:
plane_or_line = []
for a, b in itertools.combinations(simplex, 2):
va = self.data[self.tri.vertices[a]]
vb = self.data[self.tri.vertices[b]]
if min(va, vb) < level <= max(va, vb):
vi = _get_vertex_index(a, b)
should_add = True
for pi in plane_or_line:
if np.allclose(vertices[vi], vertices[pi]):
should_add = False
if should_add:
plane_or_line.append(vi)
if get_surface and len(plane_or_line) == 3:
faces_or_lines.append(plane_or_line)
elif get_surface and len(plane_or_line) == 4:
faces_or_lines.append(plane_or_line[:3])
faces_or_lines.append(plane_or_line[1:])
elif get_line and len(plane_or_line) == 2:
faces_or_lines.append(plane_or_line)
if len(faces_or_lines) == 0:
r_min = min(self.data[v] for v in self.tri.vertices)
r_max = max(self.data[v] for v in self.tri.vertices)
raise ValueError(
f"Could not draw isosurface for level={level}, as"
" this value is not inside the function range. Please choose"
f" a level strictly inside interval ({r_min}, {r_max})"
)
return vertices, faces_or_lines
def plot_isoline(self, level=0.0, n=None, tri_alpha=0):
"""Plot the isoline at a specific level, only works in 2D.
Parameters
----------
level : float, default: 0
The value of the function at which you would like to see
the isoline.
n : int
The number of boxes in the interpolation grid along each axis.
This is passed to `plot`.
tri_alpha : float
The opacity of the overlaying triangulation. This is passed
to `plot`.
Returns
-------
`holoviews.core.Overlay`
The plot of the isoline(s). This overlays a `plot` with a
`holoviews.element.Path`.
"""
hv = ensure_holoviews()
if n == -1:
plot = hv.Path([])
else:
plot = self.plot(n=n, tri_alpha=tri_alpha)
if isinstance(level, Iterable):
for l in level:
plot = plot * self.plot_isoline(level=l, n=-1)
return plot
vertices, lines = self.self._get_iso(level, which='line')
paths = [[vertices[i], vertices[j]] for i, j in lines]
contour = hv.Path(paths)
contour_opts = dict(color='black')
contour = contour.opts(style=contour_opts)
return plot * contour
def plot_isosurface(self, level=0.0, hull_opacity=0.2):
"""Plots a linearly interpolated isosurface.
This is the 3D analog of an isoline. Does *not* work with the
`adaptive.notebook_integration.live_plot` functionality.
Parameters
----------
level : float, default: 0.0
the function value which you are interested in.
hull_opacity : float, default: 0.0
the opacity of the hull of the domain.
Returns
-------
plot : `plotly.offline.iplot` object
The plot object of the isosurface.
"""
plotly = ensure_plotly()
vertices, faces = self._get_iso(level, which='surface')
x, y, z = zip(*vertices)
fig = plotly.figure_factory.create_trisurf(
x=x, y=y, z=z, plot_edges=False,
simplices=faces, title="Isosurface")
isosurface = fig.data[0]
isosurface.update(lighting=dict(ambient=1, diffuse=1,
roughness=1, specular=0, fresnel=0))
if hull_opacity < 1e-3:
# Do not compute the hull_mesh.
return plotly.offline.iplot(fig)
hull_mesh = self._get_hull_mesh(opacity=hull_opacity)
return plotly.offline.iplot([isosurface, hull_mesh])
def _get_hull_mesh(self, opacity=0.2):
plotly = ensure_plotly()
hull = scipy.spatial.ConvexHull(self._bounds_points)
# Find the colors of each plane, giving triangles which are coplanar
# the same color, such that a square face has the same color.
color_dict = {}
def _get_plane_color(simplex):
simplex = tuple(simplex)
# If the volume of the two triangles combined is zero then they
# belong to the same plane.
for simplex_key, color in color_dict.items():
points = [hull.points[i] for i in set(simplex_key + simplex)]
points = np.array(points)
if np.linalg.matrix_rank(points[1:] - points[0]) < 3:
return color
if scipy.spatial.ConvexHull(points).volume < 1e-5:
return color
color_dict[simplex] = tuple(random.randint(0, 255)
for _ in range(3))
return color_dict[simplex]
colors = [_get_plane_color(simplex) for simplex in hull.simplices]
x, y, z = zip(*self._bounds_points)
i, j, k = hull.simplices.T
lighting = dict(ambient=1, diffuse=1, roughness=1,
specular=0, fresnel=0)
return plotly.graph_objs.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k,
facecolor=colors, opacity=opacity,
lighting=lighting)
......@@ -23,6 +23,7 @@ def notebook_extension():
# Load holoviews
try:
_holoviews_enabled = False # After closing a notebook the js is gone
if not _holoviews_enabled:
import holoviews
holoviews.notebook_extension('bokeh', logo=False)
......
......@@ -8,9 +8,9 @@ import os
import time
import traceback
import warnings
import abc
from .notebook_integration import live_plot, live_info, in_ipynb
from .utils import timed
try:
import ipyparallel
......@@ -53,7 +53,7 @@ else:
_default_executor_kwargs = {}
class BaseRunner:
class BaseRunner(metaclass=abc.ABCMeta):
"""Base class for runners that use `concurrent.futures.Executors`.
Parameters
......@@ -104,8 +104,7 @@ class BaseRunner:
Methods
-------
overhead : callable
The overhead in percent of using Adaptive. This includes the
overhead of the executor. Essentially, this is
The overhead in percent of using Adaptive. Essentially, this is
``100 * (1 - total_elapsed_function_time / self.elapsed_time())``.
"""
......@@ -129,8 +128,7 @@ class BaseRunner:
self.learner = learner
self.log = [] if log else None
# Function timing
self.function = functools.partial(timed, self.learner.function)
# Timing
self.start_time = time.time()
self.end_time = None
self._elapsed_function_time = 0
......@@ -182,6 +180,10 @@ class BaseRunner:
but is a rough rule of thumb.
"""
t_function = self._elapsed_function_time
if t_function == 0:
# When no function is done executing, the overhead cannot
# reliably be determined, so 0 is the best we can do.
return 0
t_total = self.elapsed_time()
return (1 - t_function / t_total) * 100
......@@ -189,7 +191,8 @@ class BaseRunner:
for fut in done_futs:
x = self.pending_points.pop(fut)
try:
y, t = fut.result()
y = fut.result()
t = time.time() - fut.start_time # total execution time
except Exception as e:
self.tracebacks[x] = traceback.format_exc()
self.to_retry[x] = self.to_retry.get(x, 0) + 1
......@@ -198,7 +201,7 @@ class BaseRunner:
if self.raise_if_retries_exceeded:
self._do_raise(e, x)
else:
self._elapsed_function_time += t / _get_ncores(self.executor)
self._elapsed_function_time += t / self._get_max_tasks()
self.to_retry.pop(x, None)
self.tracebacks.pop(x, None)
if self.do_log:
......@@ -217,7 +220,10 @@ class BaseRunner:
points, _ = self._ask(n_new_tasks)
for x in points:
self.pending_points[self._submit(x)] = x
start_time = time.time() # so we can measure execution time
fut = self._submit(x)
fut.start_time = start_time
self.pending_points[fut] = x
# Collect and results and add them to the learner
futures = list(self.pending_points.keys())
......@@ -241,6 +247,20 @@ class BaseRunner:
def failed(self):
"""Set of points that failed ``runner.retries`` times."""
return set(self.tracebacks) - set(self.to_retry)
@abc.abstractmethod
def elapsed_time(self):
"""Return the total time elapsed since the runner
was started.
Is called in `overhead`.
"""
pass
@abc.abstractmethod
def _submit(self, x):
"""Is called in `_get_futures`."""
pass
class BlockingRunner(BaseRunner):
......@@ -317,7 +337,7 @@ class BlockingRunner(BaseRunner):
self._run()
def _submit(self, x):
return self.executor.submit(self.function, x)
return self.executor.submit(self.learner.function, x)
def _run(self):
first_completed = concurrent.FIRST_COMPLETED
......@@ -338,6 +358,8 @@ class BlockingRunner(BaseRunner):
self._cleanup()
def elapsed_time(self):
"""Return the total time elapsed since the runner
was started."""
if self.end_time is None:
# This shouldn't happen if the BlockingRunner
# correctly finished.
......@@ -409,7 +431,6 @@ class AsyncRunner(BaseRunner):
overhead of the executor. Essentially, this is
``100 * (1 - total_elapsed_function_time / self.elapsed_time())``.
Notes
-----
This runner can be used when an async function (defined with
......@@ -442,11 +463,6 @@ class AsyncRunner(BaseRunner):
raise RuntimeError('Cannot use an executor when learning an '
'async function.')
self.executor.shutdown() # Make sure we don't shoot ourselves later
self._submit = lambda x: self.ioloop.create_task(self.function(x))
else:
self._submit = functools.partial(self.ioloop.run_in_executor,
self.executor,
self.function)
self.task = self.ioloop.create_task(self._run())
self.saving_task = None
......@@ -456,6 +472,13 @@ class AsyncRunner(BaseRunner):
"in a Jupyter notebook, remember to run "
"'adaptive.notebook_extension()'")
def _submit(self, x):
ioloop = self.ioloop
if inspect.iscoroutinefunction(self.learner.function):
return ioloop.create_task(self.learner.function(x))
else:
return ioloop.run_in_executor(self.executor, self.learner.function, x)
def status(self):
"""Return the runner status as a string.
......@@ -532,6 +555,8 @@ class AsyncRunner(BaseRunner):
self._cleanup()
def elapsed_time(self):
"""Return the total time elapsed since the runner
was started."""
if self.task.done():
end_time = self.end_time
if end_time is None:
......
......@@ -2,11 +2,43 @@
import asyncio
from ..learner import Learner2D
from ..runner import simple, BlockingRunner, AsyncRunner, SequentialExecutor
import pytest
from ..learner import Learner1D, Learner2D
from ..runner import (simple, BlockingRunner, AsyncRunner, SequentialExecutor,
with_ipyparallel, with_distributed)
def test_nonconforming_output():
def blocking_runner(learner, goal):
BlockingRunner(learner, goal, executor=SequentialExecutor())
def async_runner(learner, goal):
runner = AsyncRunner(learner, goal, executor=SequentialExecutor())
asyncio.get_event_loop().run_until_complete(runner.task)
runners = [simple, blocking_runner, async_runner]
def trivial_goal(learner):
return learner.npoints > 10
@pytest.mark.parametrize('runner', runners)
def test_simple(runner):
"""Test that the runners actually run."""
def f(x):
return x
learner = Learner1D(f, (-1, 1))
runner(learner, lambda l: l.npoints > 10)
assert len(learner.data) > 10
@pytest.mark.parametrize('runner', runners)
def test_nonconforming_output(runner):
"""Test that using a runner works with a 2D learner, even when the
learned function outputs a 1-vector. This tests against the regression
flagged in https://gitlab.kwant-project.org/qt/adaptive/issues/58.
......@@ -15,15 +47,63 @@ def test_nonconforming_output():
def f(x):
return [0]
def goal(l):
return l.npoints > 10
runner(Learner2D(f, [(-1, 1), (-1, 1)]), trivial_goal)
learner = Learner2D(f, [(-1, 1), (-1, 1)])
simple(learner, goal)
learner = Learner2D(f, [(-1, 1), (-1, 1)])
BlockingRunner(learner, goal, executor=SequentialExecutor())
def test_aync_def_function():
learner = Learner2D(f, [(-1, 1), (-1, 1)])
runner = AsyncRunner(learner, goal, executor=SequentialExecutor())
async def f(x):
return x
learner = Learner1D(f, (-1, 1))
runner = AsyncRunner(learner, trivial_goal)
asyncio.get_event_loop().run_until_complete(runner.task)
### Test with different executors
@pytest.fixture(scope="session")
def ipyparallel_executor():
from ipyparallel import Client
import pexpect
child = pexpect.spawn('ipcluster start -n 1')
child.expect('Engines appear to have started successfully', timeout=35)
yield Client()
if not child.terminate(force=True):
raise RuntimeError('Could not stop ipcluster')
@pytest.fixture(scope="session")
def dask_executor():
from distributed import LocalCluster, Client
client = Client(n_workers=1)
yield client
client.close()
def linear(x):
return x
def test_concurrent_futures_executor():
from concurrent.futures import ProcessPoolExecutor
BlockingRunner(Learner1D(linear, (-1, 1)), trivial_goal,
executor=ProcessPoolExecutor(max_workers=1))
@pytest.mark.skipif(not with_ipyparallel, reason='IPyparallel is not installed')
def test_ipyparallel_executor(ipyparallel_executor):
learner = Learner1D(linear, (-1, 1))
BlockingRunner(learner, trivial_goal,
executor=ipyparallel_executor)
assert learner.npoints > 0
@pytest.mark.skipif(not with_distributed, reason='dask.distributed is not installed')
def test_distributed_executor(dask_executor):
learner = Learner1D(linear, (-1, 1))
BlockingRunner(learner, trivial_goal,
executor=dask_executor)
assert learner.npoints > 0
......@@ -8,12 +8,6 @@ import pickle
import time
def timed(f, *args, **kwargs):
t_start = time.time()
result = f(*args, **kwargs)
return result, time.time() - t_start
def named_product(**items):
names = items.keys()
vals = items.values()
......
......@@ -9,7 +9,7 @@ dependencies:
- sortedcontainers
- scipy
- holoviews
- bokeh
- bokeh==0.13
- plotly
- ipyparallel
- distributed
......
......@@ -57,7 +57,7 @@ The balancing learner can for example be used to implement a poor-man’s
Often one wants to create a set of ``learner``\ s for a cartesian
product of parameters. For that particular case we’ve added a
``classmethod`` called ``~adaptive.BalancingLearner.from_product``.
``classmethod`` called `~adaptive.BalancingLearner.from_product`.
See how it works below
.. jupyter-execute::
......
......@@ -298,6 +298,15 @@ raise the exception with the stack trace:
runner.task.result()
You can also check ``runner.tracebacks`` which is a mapping from
point → traceback.
.. jupyter-execute::
for point, tb in runner.tracebacks.items():
print(f'point: {point}:\n {tb}')
Logging runners
~~~~~~~~~~~~~~~
......@@ -337,10 +346,16 @@ set of operations on another runner:
learner.plot().Scatter.I.opts(style=dict(size=6)) * reconstructed_learner.plot()
Timing functions
~~~~~~~~~~~~~~~~
Adding coroutines
-----------------
In the following example we'll add a `~asyncio.Task` that times the runner.
This is *only* for demonstration purposes because one can simply
check ``runner.elapsed_time()`` or use the ``runner.live_info()``
widget to see the time since the runner has started.
To time the runner you **cannot** simply use
So let's get on with the example. To time the runner
you **cannot** simply use
.. code:: python
......