<jupyter_start><jupyter_text>`adaptive` tutorial<jupyter_code>import adaptive
adaptive.notebook_extension()

import random
import numpy as np<jupyter_output><empty_output><jupyter_text>1D function learner: `Learner1D`<jupyter_code>x0 = random.uniform(-0.5, 0.5)

def peak(x, x0=x0, wait=True):
    from random import random
    from time import sleep

    if wait:
        sleep(random())

    a = 0.01
    return x + a**2 / (a**2 + (x - x0)**2)<jupyter_output><empty_output><jupyter_text>We start by initializing a 1D "learner", which will suggest points to evaluate, and adapt its suggestions as more and more points are evaluated.<jupyter_code>learner = adaptive.Learner1D(peak, bounds=(-1, 1))
runner = adaptive.Runner(learner, goal=lambda l: l.loss() < 0.01, log=True)
runner.live_info()<jupyter_output><empty_output><jupyter_text>When instantiated in a Jupyter notebook the runner does its job in the background and does not block the IPython kernel.We can use this to create a plot that updates as new data arrives:<jupyter_code>runner.live_plot(update_interval=0.1)
learner.loss()<jupyter_output><empty_output><jupyter_text>We can now compare the adaptive sampling to a homogeneous sampling with the same number of points:<jupyter_code>from functools import partial
learner2 = adaptive.Learner1D(peak, bounds=learner.bounds)

xs = np.linspace(*learner.bounds, len(learner.data))
learner2.tell(xs, map(partial(peak, wait=False), xs))

learner2.plot().relabel('homogeneous') + learner.plot().relabel('adaptive')<jupyter_output><empty_output><jupyter_text>2D function learner Besides 1D functions, we can also learn 2D functions: $\ f: ℝ^2 → ℝ^N$<jupyter_code>def ring(xy, wait=True):
    import numpy as np
    from time import sleep
    from random import random
    if wait:
        sleep(random() / 3)
    x, y = xy
    a = 0.2
    return x + np.exp(-(x**2 + y**2 - 0.75**2)**2/a**4)

learner = adaptive.Learner2D(ring, bounds=[(-1, 1), (-1, 1)])
runner = adaptive.Runner(learner, goal=lambda l: l.loss() < 0.01)
runner.live_info()
def plot(learner):
    plot = learner.plot(tri_alpha=0.2)
    title = f'loss={learner._loss:.3f}, n_points={learner.npoints}'
    return (plot.Image
            + plot.EdgePaths.I.opts(plot=dict(title_format=title))
            + plot)

runner.live_plot(plotter=plot, update_interval=0.1)
%%opts EdgePaths (color='w')

import itertools

# Create a learner and add data on homogeneous grid, so that we can plot it
learner2 = adaptive.Learner2D(ring, bounds=learner.bounds)
n = int(learner.npoints**0.5)
xs, ys = [np.linspace(*bounds, n) for bounds in learner.bounds]
xys = list(itertools.product(xs, ys))
learner2.tell(xys, map(partial(ring, wait=False), xys))

(learner2.plot(n).relabel('Homogeneous grid') + learner.plot().relabel('With adaptive') + 
 learner2.plot(n, tri_alpha=0.4) + learner.plot(tri_alpha=0.4)).cols(2)<jupyter_output><empty_output><jupyter_text>Averaging learner The next type of learner averages a function until the uncertainty in the average meets some condition.This is useful for sampling a random variable. The function passed to the learner must formally take a single parameter,which should be used like a "seed" for the (pseudo-) random variable (although in the current implementation the seed parameter can be ignored by the function).<jupyter_code>def g(n):
    import random
    from time import sleep
    sleep(random.random() / 4000)
    return random.gauss(mu=0.5, sigma=1)
learner = adaptive.AverageLearner(g, atol=None, rtol=0.01)
runner = adaptive.Runner(learner, goal=lambda l: l.loss() < 1)
runner.live_info()
runner.live_plot(update_interval=0.1)<jupyter_output><empty_output><jupyter_text>1D integration learner with `cquad` This learner learns a 1D function and calculates the integral and error of the integral with it. It is based on Pedro Gonnet's [implementation](https://www.academia.edu/1976055/Adaptive_quadrature_re-revisited).Let's try the following function with cusps (that is difficult to integrate):<jupyter_code>import holoviews as hv

def f24(x):
    return np.floor(np.exp(x))

xs = np.linspace(0, 3, 200)
hv.Scatter((xs, [f24(x) for x in xs]))<jupyter_output><empty_output><jupyter_text>Just to prove that this really is a difficult to integrate function, let's try a familiar function integrator `scipy.integrate.quad`, which will give us warnings that it encounters difficulties.<jupyter_code>import scipy.integrate
scipy.integrate.quad(f24, 0, 3)<jupyter_output><empty_output><jupyter_text>We initialize a learner again and pass the bounds and relative tolerance we want to reach. Then in the `Runner` we pass `goal=lambda l: l.done()` where `learner.done()` is `True` when the relative tolerance has been reached.<jupyter_code>from adaptive.runner import SequentialExecutor

learner = adaptive.IntegratorLearner(f24, bounds=(0, 3), tol=1e-9)
runner = adaptive.Runner(learner, executor=SequentialExecutor(), goal=lambda l: l.done())
runner.live_info()<jupyter_output><empty_output><jupyter_text>Now we could do the live plotting again, but lets just wait untill the runner is done.<jupyter_code>print('The integral value is {} with the corresponding error of {}'.format(learner.igral, learner.err))
learner.plot()<jupyter_output><empty_output><jupyter_text>1D learner with vector output: `f:ℝ → ℝ^N` Sometimes you may want to learn a function with vector output:<jupyter_code>random.seed(0)
offsets = [random.uniform(-0.8, 0.8) for _ in range(3)]

# sharp peaks at random locations in the domain
def f_levels(x, offsets=offsets):
    a = 0.01
    return np.array([offset + x + a**2 / (a**2 + (x - offset)**2)
                     for offset in offsets])<jupyter_output><empty_output><jupyter_text>`adaptive` has you covered! The `Learner1D` can be used for such functions:<jupyter_code>learner = adaptive.Learner1D(f_levels, bounds=(-1, 1))
runner = adaptive.Runner(learner, goal=lambda l: l.loss() < 0.01)
runner.live_info()
runner.live_plot(update_interval=0.1)<jupyter_output><empty_output><jupyter_text>Balancing learner The balancing learner is a "meta-learner" that takes a list of learners. When you request a point from the balancing learner, it will query all of its "children" to figure out which one will give the most improvement.The balancing learner can for example be used to implement a poor-man's 2D learner by using the `Learner1D`.<jupyter_code>def h(x, x0=0):
    a = 0.01
    return x + a**2 / (a**2 + (x - x0)**2)

learners = [adaptive.Learner1D(partial(h,
                                       x0=random.uniform(-1, 1)),
                                       bounds=(-1, 1))
            for i in range(10)]

bal_learner = adaptive.BalancingLearner(learners)
runner = adaptive.Runner(bal_learner, goal=lambda l: l.loss() < 0.01)
runner.live_info()
import holoviews as hv

plotter = lambda learner: hv.Overlay([L.plot() for L in learner.learners])
runner.live_plot(plotter=plotter, update_interval=0.3)<jupyter_output><empty_output><jupyter_text>Custom adaptive logic for 1D and 2D `Learner1D` and `Learner2D` both work on the principle of subdividing their domain into subdomains, and assigning a property to each subdomain, which we call the *loss*. The algorithm for choosing the best place to evaluate our function is then simply *take the subdomain with the largest loss and add a point in the center, creating new subdomains around this point*. The *loss function* that defines the loss per subdomain is the place to define what regions of the domain are "interesting".The default loss function for `Learner1D` and `Learner2D` is sufficient for a wide range of common cases, but it of course does not work in all possible cases. For example, the default loss function will tend to get stuck on divergences.Both the `Learner1D` and `Learner2D` allow you to specify a *custom loss function*.Say we want to properly sample a function that contains divergences. A simple (but naive) strategy is to *uniformly* sample the domain:<jupyter_code>def f_divergent_1d(x):
    return 1 / x**2


def uniform_sampling_1d(interval, xy_scale, function_values):
    # Note that we never use 'function_values'; the loss is just the size of the subdomain
    x_left, x_right = interval
    x_scale, _ = xy_scale
    dx = (x_right - x_left) / x_scale
    return dx


learner = adaptive.Learner1D(f_divergent_1d, bounds=(-1, 1),
                             loss_per_interval=uniform_sampling_1d)
runner = adaptive.BlockingRunner(learner, goal=lambda l: l.loss() < 0.01)
learner.plot().select(y=(0, 10000))
%%opts EdgePaths (color='w') Image [logz=True]

from adaptive.runner import SequentialExecutor

def f_divergent_2d(xy):
    x, y = xy
    return 1 / (x**2 + y**2)


def uniform_sampling_2d(ip):
    from adaptive.learner.learner2D import areas
    A = areas(ip)
    return np.sqrt(A)

learner = adaptive.Learner2D(f_divergent_2d, [(-1, 1), (-1, 1)],
                             loss_per_triangle=uniform_sampling_2d)

runner = adaptive.Runner(learner, goal=lambda l: l.loss() < 0.02)
runner.live_info()
runner.live_plot(update_interval=0.2,
                 plotter=lambda l: l.plot(tri_alpha=0.3).relabel('1 / (x^2 + y^2) in log scale'))<jupyter_output><empty_output><jupyter_text>The uniform sampling strategy is a common case to benchmark against, so the 1D and 2D versions are included in `adaptive` as `adaptive.learner.learner1D.uniform_sampling` and `adaptive.learner.learner2D.uniform_sampling`.  Doing betterOf course, using `adaptive` for uniform sampling is a bit of a waste!Let's see if we can do a bit better. Below we define a loss per subdomain that scales with the degree of nonlinearity of the function (this is very similar to the default loss function for `Learner2D`), but which is 0 for subdomains smaller than a certain area, and infinite for subdomains larger than a certain area.A loss defined in this way means that the adaptive algorithm will first prioritise subdomains that are too large (infinite loss). After all subdomains are appropriately small it will prioritise places where the function is very nonlinear, but will ignore subdomains that are too small (0 loss).<jupyter_code>%%opts EdgePaths (color='w') Image [logz=True]

def resolution_loss(ip, min_distance=0, max_distance=1):
    """min_distance and max_distance should be in between 0 and 1
    because the total area is normalized to 1."""

    from adaptive.learner.learner2D import areas, deviations

    A = areas(ip)

    # 'deviations' returns an array of shape '(n, len(ip))', where
    # 'n' is the  is the dimension of the output of the learned function
    # In this case we know that the learned function returns a scalar,
    # so 'deviations' returns an array of shape '(1, len(ip))'.
    # It represents the deviation of the function value from a linear estimate
    # over each triangular subdomain.
    dev = deviations(ip)[0]
    
    # we add terms of the same dimension: dev == [distance], A == [distance**2]
    loss = A + np.sqrt(A) * dev
    
    # Setting areas with a small area to zero such that they won't be chosen again
    loss[A < min_distance**2] = 0
    
    # Setting triangles that have a size larger than max_distance to infinite loss
    loss[A > max_distance**2] = np.inf

    return loss

loss = partial(resolution_loss, min_distance=0.01)

learner = adaptive.Learner2D(f_divergent_2d, [(-1, 1), (-1, 1)], loss_per_triangle=loss)
runner = adaptive.BlockingRunner(learner, goal=lambda l: l.loss() < 0.02)
learner.plot(tri_alpha=0.3).relabel('1 / (x^2 + y^2) in log scale')<jupyter_output><empty_output><jupyter_text>Awesome! We zoom in on the singularity, but not at the expense of sampling the rest of the domain a reasonable amount.The above strategy is available as `adaptive.learner.learner2D.resolution_loss`.  Using multiple cores Often you will want to evaluate the function on some remote computing resources. `adaptive` works out of the box with any framework that implements a [PEP 3148](https://www.python.org/dev/peps/pep-3148/) compliant executor that returns `concurrent.futures.Future` objects.  [`concurrent.futures`](https://docs.python.org/3/library/concurrent.futures.html) On Unix-like systems by default `adaptive.Runner` creates a `ProcessPoolExecutor`, but you can also pass one explicitly e.g. to limit the number of workers:<jupyter_code>from concurrent.futures import ProcessPoolExecutor

executor = ProcessPoolExecutor(max_workers=4)

learner = adaptive.Learner1D(peak, bounds=(-1, 1))
runner = adaptive.Runner(learner, executor=executor, goal=lambda l: l.loss() < 0.05)
runner.live_info()
runner.live_plot(update_interval=0.1)<jupyter_output><empty_output><jupyter_text>[`ipyparallel`](https://ipyparallel.readthedocs.io/en/latest/intro.html)<jupyter_code>import ipyparallel

client = ipyparallel.Client()  # You will need to start an `ipcluster` to make this work

learner = adaptive.Learner1D(f, bounds=(-1, 1))
runner = adaptive.Runner(learner, executor=client, goal=lambda l: l.loss() < 0.01)
runner.live_info()
runner.live_plot()<jupyter_output><empty_output><jupyter_text>[`distributed`](https://distributed.readthedocs.io/en/latest/)On Windows by default `adaptive.Runner` uses a `distributed.Client`.<jupyter_code>import distributed

client = distributed.Client()

learner = adaptive.Learner1D(f, bounds=(-1, 1))
runner = adaptive.Runner(learner, executor=client, goal=lambda l: l.loss() < 0.01)
runner.live_info()
runner.live_plot(update_interval=0.1)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10654885654885655, 'fraction_numerical': 0.020344520344520344, 'mean_word_length': 4.8181425485961125, 'pattern_counts': {'":': 0, '<': 89, '<?xml version=': 0, '>': 78, 'https://': 5, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '261337', 'license': 'MIT', 'n_tokens_mistral': 4570, 'n_tokens_neox': 4287, 'n_words': 1391, 'path': 'tutorial-notebook.ipynb', 'repo_name': 'basnijholt/adaptive-talk', 'size': '13468'}