Adaptive: choosing evaluation points instead of filling a grid
:chart_with_upwards_trend: Adaptive: parallel active learning of mathematical functions
At a glance
- What is it?
- Adaptive is a Python library that picks where to evaluate a function next rather than sweeping a fixed grid. The mechanism is a learner plus an executor, and the design only pays off when each evaluation is slow enough to justify the selection overhead.
- Who is it for?
- Adopt Adaptive if your function takes roughly 50ms or more per evaluation, returns a scalar or vector, and you can describe its domain as bounds over one or more dimensions. Do not adopt it for cheap vectorised functions, for functions that fail intermittently, or when you need a result at a fixed set of points rather than a good approximation over a region.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The grid you did not need to compute
Dense grid sampling has a fixed cost that is decided before you know anything about the function. If you want a 1D function resolved to some tolerance over an interval, you choose a number of points, evaluate all of them, and only afterwards discover which regions mattered. Adaptive inverts that order. The README describes the library as selecting the best points in the parameter space based on your provided function and bounds, and the summary states plainly that it is most efficient when each function evaluation takes at least around 50ms. That threshold is the whole argument in one number. Below it, the cost of deciding where to sample next exceeds the cost of just sampling everywhere. Above it, skipping uninteresting regions is where the savings come from. The intended user is someone with an expensive black-box function: a simulation, a numerical solver, a model that takes seconds to run, wrapped in a Python callable with declared bounds.
What a learner actually is
The central object is the learner, not the sampler and not the plot. The README states that a learner samples a function at the most interesting locations within its parameter space, and that as more points are evaluated the learner improves its understanding of where to sample next. The definition of interesting is domain-specific, which is why the library ships several learners rather than one. Learner1D covers functions from the reals to a vector of reals. Learner2D covers two input dimensions. LearnerND covers N inputs and M outputs. AverageLearner and AverageLearner1D handle random variables and stochastic 1D functions, estimating a mean rather than a deterministic value. IntegratorLearner targets integration of a 1D function. BalancingLearner runs several learners at once and picks the most promising one as points accumulate. DataSaver exists for functions that return something other than a scalar or a vector. That last entry is worth pausing on: the default learners assume structured output, and if your function returns a dictionary or a nested object you are expected to wrap it. The README does not describe how DataSaver stores those objects, so the exact contract is something to read in the API reference before designing around it.
Runner, executor and the loss goal
The README example is short enough to quote in full as a shape. You call notebook_extension(), define a function, construct Learner1D with bounds, wrap it in a Runner with loss_goal set to 0.01, then call runner.live_info() and runner.live_plot(). The runner is what drives evaluation and decides when to stop; the loss goal is the stopping criterion. This is the part most people underestimate. A learner does not run itself to completion on its own schedule. You give it a target loss and it keeps requesting points until that target is met or you interrupt it. If the function is noisy or the loss plateaus, the runner will keep going. The library also provides primitives for parallel sampling across multiple cores or machines, with the README naming concurrent.futures and mpi4py as supported backends. That is the mechanism behind the parallel claim: the learner proposes candidate points, and an executor evaluates them concurrently. The learner itself is sequential in its decision-making, which is why the parallel speedup is bounded by how many points the learner is willing to hand out at once rather than by core count.
Getting it running
Installation is via conda or PyPI; the README badges point at the conda-forge package adaptive and the PyPI package adaptive. The README also mentions an optional step for faster triangulation, which matters for the 2D and ND learners since they build triangulations of the sampled points. The exact optional dependency is named in the installation section of the documentation rather than in the README excerpt here, so check that page if triangulation performance becomes the bottleneck. The minimal working setup, taken from the README example, is: import notebook_extension, Runner and Learner1D from adaptive; call notebook_extension() once at the top of the notebook; define your function; construct Learner1D(peak, bounds=(-1, 1)); construct Runner(learner, loss_goal=0.01); then call runner.live_info() and runner.live_plot(). Export is done through the learner, not the runner: learner.to_numpy() returns a NumPy array, and learner.to_dataframe() returns a pandas DataFrame if pandas is installed. Those two methods are the boundary between the adaptive sampling loop and whatever analysis you do next.
The 50ms line and other reasons to walk away
The README's own efficiency note is the clearest limitation: below roughly 50ms per evaluation, the overhead of selecting interesting points dominates. A vectorised NumPy function that evaluates a thousand points in a millisecond is the wrong tool for this library, and no amount of tuning changes that, because the cost is in the selection logic rather than in the evaluation. There is a second, less advertised constraint. Active sampling assumes the function is well-behaved enough that the learner's model of it is meaningful. A function that raises exceptions for some inputs, or returns NaN in a region, will confuse a learner that expects to interpolate between successful evaluations. The README does not describe retry or failure-handling policies, so treat intermittent failures as an open question rather than a solved one. Third, if you need values at specific points, for instance to match an experimental grid or to feed a downstream solver that requires a regular mesh, adaptive sampling gives you the opposite of what you want. It chooses the points; you do not. And fourth, the notebook integration is genuinely part of the product: live_plot and live_info are Jupyter-oriented, and outside a notebook you lose the visual feedback that makes the sampling process legible. That is not a defect, but it does mean the library's ergonomics are best in one specific environment.
Where a plain grid or a Bayesian optimiser fits better
The obvious alternative is a uniform grid plus vectorised evaluation. If your function is cheap, vectorised and has modest dimensionality, a grid is simpler, trivially parallel, reproducible and free of stopping criteria. It also gives you a fixed output shape that downstream code can rely on. Adaptive's output is a growing, irregular set of points, and that irregularity propagates into everything you do with the result. A second alternative is Bayesian optimisation libraries, which share the adaptive premise but optimise a different objective. Bayesian optimisation typically seeks the location of a maximum or minimum and treats the surrogate model's uncertainty as the acquisition signal. Adaptive's learners target reconstruction of the function over the domain, or its integral in the case of IntegratorLearner, rather than the location of an extremum. If your question is where is the peak, the two approaches overlap. If your question is what does this function look like across the interval, or what is its integral, Bayesian optimisation is aimed at the wrong target. The distinction is worth keeping straight, because both are described as adaptive sampling and the vocabulary is similar.
Licence, releases and the cost of staying current
The project is BSD-3-Clause, which permits commercial use and modification provided the copyright notice and licence text are retained. That is a permissive licence with no copyleft obligation, and it is the same family used by much of the scientific Python stack. It does not, of course, tell you anything about the quality of the code, and nothing here should be read as legal advice; if you are redistributing a modified version, read the actual licence file. On maintenance: the repository is not archived, the default branch is main, and the most recent release listed is v1.5.2, dated 2026-06-10, with v1.5.1 and v1.5.0 landing the same day. Three releases in one day suggests either a coordinated release push or rapid patch fixes after a larger change. Either way, pinning a specific version in your environment file is the sensible default, because a library whose sampling behaviour feeds into scientific results is not something you want silently upgraded mid-project. The upgrade cost itself is mostly about the learner API: if you subclass a learner or rely on internal triangulation details, minor versions can move under you. If you use the documented learner and runner surface, upgrades are usually a matter of re-running and confirming that the loss goal still terminates in comparable time.
Who this is for, and what to check first
Adaptive fits the case where a function is expensive, deterministic or nearly so, defined over declared bounds, and where you care about the shape of the result rather than a specific set of sample locations. The 50ms figure in the README is the gate. If you are below it, stop. If you are above it and your function returns a scalar or a vector, the learner list probably has an entry for your dimensionality, and the runner plus loss goal gives you a stopping rule you can reason about. Before adopting, verify three things in order. First, match your dimensionality to a learner: 1D, 2D, ND, or one of the stochastic and integration variants, and confirm the output type is one the learner accepts or that DataSaver covers it. Second, confirm your executor is compatible with the concurrency backend you intend to use, since the README names concurrent.futures and mpi4py but does not spell out the constraints on pickling or process startup. Third, run the README's own example on your machine and watch the loss curve. If the loss goal you set is never reached, the problem is your function or your goal, not the library, and you will find that out in the first few minutes rather than after a week of cluster time.
Editorial conclusion
Adopt Adaptive if your function takes roughly 50ms or more per evaluation, returns a scalar or vector, and you can describe its domain as bounds over one or more dimensions. Do not adopt it for cheap vectorised functions, for functions that fail intermittently, or when you need a result at a fixed set of points rather than a good approximation over a region. Before committing, check the learner list against your dimensionality, confirm whether your executor can be pickled, and verify that the loss goal you pick actually terminates on your function.
Community notes