Tutorial

This tutorial walks through a complete optimisation run: wrapping a blackbox function in an Objective, configuring the search, executing the run, and reading the reproducibility record it leaves behind.

Setup

git clone https://github.com/AlxndrSchroeder/actgpr.git
cd actgpr
poetry install

Step 1 — wrap your blackbox function

actgpr minimises a scalar blackbox function: something that takes one float and returns one float. In practice that might launch a simulation or trigger an experiment — here we use an analytic stand-in so the tutorial runs instantly:

from actgpr import ObjectiveFn

def my_blackbox(x: float) -> float:
    """Stand-in for a simulation or experiment."""
    return (x - 1) ** 2

objective = ObjectiveFn(my_blackbox)

ObjectiveFn turns any Callable[[float], float] into an Objective. Its evaluate method accepts one or more input points and returns a tuple of outputs:

objective.evaluate(0.0)        # (1.0,)
objective.evaluate(0.0, 3.0)   # (1.0, 4.0)

Errors raised inside your function propagate unchanged, so you can handle them by their original type.

Step 2 — configure the run

Three decisions matter most:

search_bounds

The closed interval [lo, hi] in which the algorithm searches for the minimum. The blackbox is never evaluated outside it.

max_iterations

The budget cap: the maximum number of active optimisation iterations (GPR fit cycles).

ei_threshold

The convergence threshold: the run stops early once the best achievable Expected Improvement falls below this value — meaning the surrogate sees nothing left to gain.

You also choose a fit mode:

  • OptimisationRun.with_training(...) — the GP hyperparameters (lengthscale, outputscale, noise) are re-tuned at every iteration using Adam (GPyTorch’s torch.optim.Adam integration), a gradient-descent variant that maximises the marginal log likelihood — how plausible the observed training data is under the GP. Use this when you do not know good hyperparameters — the usual case.

  • OptimisationRun.without_training(...) — hyperparameters stay fixed at the values you pass. Use this for controlled comparisons or when good values are already known.

from actgpr import GPyTorchSurrogate, OptimisationRun

run = OptimisationRun.with_training(
    objective=objective,
    surrogate=GPyTorchSurrogate(),
    search_bounds=(-3.0, 5.0),   # interval in which the minimum is searched
    initial_train_x=[-3.0, 5.0],  # points where we start looking for the minimum
    max_iterations=20,
    ei_threshold=0.001,
    store_snapshots=True,         # keep per-iteration state for plotting
    run_dir="results",            # write the MRR record
)

Step 3 — execute and interpret

result = run.run()

print(result["best_x"])       # ≈ 1.0  — input point with the lowest output
print(result["best_y"])       # ≈ 0.0  — the lowest output found
print(result["n_iterations"])  # iterations actually executed
print(result["stop_reason"])   # "ei_threshold" or "max_iterations"

result["train_x"] and result["train_y"] hold every input point the run evaluated and the corresponding Objective outputs — the initial points first, then one point per iteration.

Step 4 — browse the iterations

Because the run was created with store_snapshots=True, you can step through the surrogate’s view of the problem iteration by iteration:

run.plot_iterations()

An interactive matplotlib window opens with the GP prediction (mean, 95 % confidence band, training data) on top, the EI landscape below, and a slider to scrub through iterations.

EI typically shrinks by orders of magnitude as a run converges — on a linear axis, later iterations can look like a flat line at zero with no visible structure. Pass log_scale=True to keep that shrinkage visible, with ei_threshold drawn as a reference line:

run.plot_iterations(log_scale=True)

If the run stopped because max_ei fell below ei_threshold, the slider’s final frame is the fit that triggered that convergence — titled “(converged — not evaluated)” since its candidate point was scored but never actually evaluated, so it carries no pred_error/improvement.

Step 5 — the reproducibility record (MRR)

Because run_dir was given, the run created a timestamped folder under results/ holding the five MRR artifacts:

  • config.json — every parameter used, written at the start of the run

  • manifest.json — a SHA-256 checksum of the inputs

  • meta.json — environment: package name, version, and repository, git commit, Python/library versions, platform, timestamps, and output summary

  • run.log — a human-readable, per-iteration audit trail

  • results.h5 — self-describing HDF5: configuration is stored as attributes alongside the data, so the file can be understood on its own

Revisiting a saved run

plot_run_history builds the validation-metrics plot directly from a run directory — no OptimisationRun object needed, so a past run can be revisited at any later time:

from pathlib import Path

from actgpr.plotting import plot_run_history

run_dir = sorted(Path("results").iterdir())[-1]   # newest run
plot_run_history(run_dir)

This plots prediction_error and improvement against iteration: prediction_error shrinking towards zero shows the surrogate learning the blackbox; improvement flattening shows the optimisation converging.

For a custom analysis, read the same series directly:

import h5py

with h5py.File(run_dir / "results.h5") as f:
    iteration = f["history/iteration"][:]
    prediction_error = f["history/prediction_error"][:]
    improvement = f["history/improvement"][:]

Parameter reference

with_training and without_training share the same core parameters and differ only in how the GP hyperparameters are handled.

Shared parameters

Parameter

Meaning

objective

The wrapped blackbox function to minimise (an ObjectiveFn).

surrogate

The GP surrogate backend. Pass a fresh GPyTorchSurrogate() — it holds fitted state internally, so do not reuse one across runs.

search_bounds

The closed interval (lo, hi) in which the algorithm searches for the minimum. The blackbox is never evaluated outside it.

initial_train_x

Points where the search starts — the first surrogate is fitted to these before the loop runs. By convention, use the two search_bounds endpoints.

max_iterations

Budget cap: the maximum number of active optimisation iterations (GPR fit cycles) — not individual blackbox evaluations.

ei_threshold

Convergence threshold: the run stops early once the best achievable Expected Improvement drops below this value.

n_candidates (default 500)

Number of evenly spaced candidate points the acquisition function scores every iteration.

noise (default 1e-4)

Starting observation noise variance for the GP likelihood. In with_training it is only a starting point — Adam tunes it further alongside lengthscale and outputscale. In without_training it stays fixed at this value for the whole run.

store_snapshots (default False)

If True, also keeps each iteration’s full GP/EI arrays (in memory and under results.h5’s iterations/ group) so plot_iterations() can browse them afterward. The prediction_error/improvement history used by plot_run_history() is recorded either way.

run_dir (default None)

If given, writes the MRR record (see Step 5) to a timestamped folder under this path. If None, nothing is written to disk.

with_training only

Parameter

Meaning

training_iter (default 50)

Number of Adam optimisation steps run per surrogate fit, tuning lengthscale, outputscale, and noise to maximise the marginal log likelihood.

without_training only

Parameter

Meaning

lengthscale (default 1.0)

Fixed RBF kernel lengthscale — never tuned.

outputscale (default 1.0)

Fixed kernel signal variance — never tuned.

Where to go next

  • The API reference documents every class and function.

  • The README’s vocabulary section defines every term used in this package.