adaptive

repository·main·Indexed 22 days ago

https://github.com/python-adaptive/adaptive

A Python library for parallel active learning of mathematical functions. It intelligently selects optimal sampling points in a parameter space to evaluate functions more efficiently than dense grid sampling. The library provides various learner types (Learner1D, Learner2D, LearnerND, AverageLearner, IntegratorLearner) and runners (BlockingRunner, AsyncRunner) to orchestrate the learning process, with optional Rust-backed triangulation for performance and Jupyter notebook integration for live plotting and monitoring.

Tokens
21.4K
Snippets
50
Records
139
Agent score
78%

What's inside adaptive

  1. Understand Adaptive Sampling Benchmarks

    main

    Adaptive sampling is used to approximate functions by concentrating sample points in areas with sharp features or rapid changes. This allows for more accurate function representation and faster convergence compared to uniform sampling, especially when using fewer total points.

    In the adaptive benchmarks, effectiveness is measured using an error ratio: the ratio of the error produced by a homogeneous (uniform) learner to the error produced by the adaptive learner.

    Key Concepts:

    • Adaptive Learner: Uses adaptive sampling to focus on complex regions.
    • Homogeneous Learner: Uses a uniform grid of points.
    • Benchmark Functions: The showcase includes 1D and 2D functions such as sharp peaks, Gaussian, sinusoidal, exponential decay, and Lorentzian functions.
  2. What is a Learner in Adaptive?

    main

    A learner is the core abstraction in adaptive. It is responsible for sampling a function at the most 'interesting' locations within its parameter space. As more points are evaluated, the learner improves its model of the function to optimize subsequent sampling locations.

    Available Learners:

    • Learner1D: For 1D functions $f: \mathbb{R} \to \mathbb{R}^N$.
    • Learner2D: For 2D functions $f: \mathbb{R}^2 \to \mathbb{R}^N$.
    • LearnerND: For ND functions $f: \mathbb{R}^N \to \mathbb{R}^M$.
    • AverageLearner: For random variables, allowing averaging of results over multiple evaluations.
    • AverageLearner1D: For stochastic 1D functions, estimating the mean value at each point.
    • IntegratorLearner: For integrating a 1D function $f: \mathbb{R} \to \mathbb{R}$.
  3. What is a BalancingLearner?

    main
    A BalancingLearner is a "meta-learner" that manages a collection of child learners. When a point is requested from the BalancingLearner, it queries all its child learners to determine which one will provide the most improvement. This allows you to compose complex learners from simpler ones; for example, you can approximate a multi-dimensional learner by using multiple Learner1D instances within a BalancingLearner.
  4. How error is calculated in benchmarks

    main

    Error in the benchmarks is estimated using the L1 norm of the difference between the true function values and the interpolated values.

    The calculation process follows these steps:

    1. Create an adaptive learner and a homogeneous learner for a benchmark function.
    2. Complete the adaptive learning process.
    3. Compare the interpolated values from the adaptive learner against the true function values evaluated at the specific points used by the homogeneous learner.
    4. Calculate the L1 norm, which represents the average of the absolute differences between the true and interpolated values (specifically calculated as the square root of the mean of the squared differences).
  5. Parallelize adaptive.Runner with PEP 3148 executors

    main

    The adaptive.Runner class supports parallel evaluation of functions by accepting an executor argument. It works with any framework that implements a PEP 3148 compliant executor that returns concurrent.futures.Future objects.

    By default, adaptive.Runner uses loky.get_reusable_executor(), which is highly flexible because it uses cloudpickle for serialization. This allows you to use functions defined interactively (like in a Jupyter notebook), as well as closures and lambdas.

    from loky import get_reusable_executor
    
    ex = get_reusable_executor()
    f = lambda x: x
    learner = adaptive.Learner1D(f, bounds=(-1, 1))
    
    runner = adaptive.Runner(learner, loss_goal=0.01, executor=ex)
    runner.live_info()
  6. Optimize sampling using curvature loss

    main

    By default, adaptive samples more points where the Euclidean distance between neighbors is large. To focus sampling on regions with high curvature, specify a curvature loss function using the loss_per_interval parameter in Learner1D.

    Available loss functions from adaptive.learner.learner1D include:

    • curvature_loss_function(): Focuses on high curvature regions.
    • default_loss(): The standard Euclidean distance-based loss.
    • uniform_loss(): Results in homogeneous sampling.

    You can use adaptive.runner.simple for a non-parallel, blocking execution when testing different loss strategies.

  7. Achieve deterministic/reproducible runs

    main

    By default, adaptive runners are non-deterministic because they evaluate functions in parallel and process results as they become available. To ensure reproducibility, you have two options:

    1. Use adaptive.runner.simple: This is the simplest way to run a learner deterministically. It blocks execution until the learner is finished.
    2. Use SequentialExecutor: If you want to keep using the non-blocking adaptive.Runner but require determinism, pass a SequentialExecutor to the executor argument.
    # Option 1: Simple blocking deterministic runner
    adaptive.runner.simple(learner, loss_goal=0.01)
    
    # Option 2: Non-blocking deterministic runner
    from adaptive.runner import SequentialExecutor
    runner = adaptive.Runner(learner, executor=SequentialExecutor(), loss_goal=0.01)
  8. Compare adaptive sampling vs uniform sampling performance

    main

    To evaluate the effectiveness of adaptive sampling, you can compare its error against a 'homogeneous' (uniform) learner.

    An error_ratio can be calculated as: error_ratio = uniform_error / learner_error

    • An error_ratio > 1 indicates that the adaptive learner is more efficient (achieving lower error with the same number of points) than uniform sampling.
    • An error_ratio close to 1 suggests that adaptive sampling provides little advantage over uniform sampling (common for smooth functions like Gaussians).
  9. Avoid blocking the IPython kernel when using Runner

    main

    When running adaptive in a Jupyter notebook, adaptive.Runner operates in an asyncio task that runs concurrently with the kernel.

    CRITICAL: If you block the IPython kernel (e.g., using a while loop or time.sleep), the runner will stop making progress.

    Do NOT do this:

    while not runner.task.done():
        pass

    If you do not need live-updating plots and simply want to run the learner until completion in a blocking manner, use adaptive.BlockingRunner instead.

  10. Define stopping criteria for Runners

    main

    Runners can be configured to stop based on a goal function. This function accepts a learner object and returns True to stop the execution or False to continue.

    You can provide a custom function or use built-in convenience parameters:

    • loss_goal=x: Stops when the loss falls below threshold x.
    • npoints_goal=n: Stops after n points have been sampled.

    For more complex logic, you can use adaptive.runner.auto_goal or adaptive.runner.stop_after to generate these goal functions.

  11. Note on benchmark function complexity

    main

    The functions used in the standard benchmark tutorials are analytical and computationally inexpensive.

    Real-world application: While these benchmarks demonstrate the principle, adaptive sampling is most beneficial in real-world scenarios involving expensive simulations where function evaluations are computationally demanding or time-consuming. For more complex use cases, refer to the gallery.

  12. Use `AverageLearner` to sample random variables

    main

    The AverageLearner is used to average a function until the uncertainty in the average meets a specified tolerance condition (atol or rtol).

    When using this learner to sample a random variable, the function passed to it must accept a single parameter. This parameter acts as a "seed" for the random variable.

    Important: RNG State Management If your function uses a global random number generator (like Python's random module), you must save and restore the RNG state within the function to ensure that the learner's sampling process is not corrupted by the function's internal seeding.

    def g(n):
        import random
        from time import sleep
    
        sleep(random.random() / 1000)
        # Properly save and restore the RNG state
        state = random.getstate()
        random.seed(n)
        val = random.gauss(0.5, 1)
        random.setstate(state)
        return val
    
    learner = adaptive.AverageLearner(g, atol=None, rtol=0.01)
    runner = adaptive.Runner(learner, loss_goal=1.0)