Add BO termination conditions#724
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for Bayesian Optimization termination conditions to allow BO runs to stop early based on convergence criteria (initially: the UCB–LCB simple-regret upper bound inspired by Makarova et al., 2022). This integrates termination evaluation into the existing run() benchmark loop and adds a small termination submodule with data models, evaluators, and mapping utilities.
Changes:
- Introduces termination-condition data models (
MaxIterationsTermination,AlwaysContinue,UCBLCBRegretTermination,CombiTerminationCondition). - Adds a termination evaluator (
UCBLCBRegretEvaluator) and mapper utilities to determine which evaluators are needed. - Extends the benchmark runner to optionally stop early and to return a richer
RunResultincluding termination metadata (plus tests).
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| bofire/data_models/termination/termination.py | New termination-condition data models + should_terminate logic. |
| bofire/data_models/termination/api.py | Exports termination-condition data models. |
| bofire/data_models/termination/init.py | Package init (empty). |
| bofire/termination/evaluator.py | Implements evaluator computing UCB–LCB regret bound + noise estimate. |
| bofire/termination/mapper.py | Maps conditions to evaluators; recursively gathers needed evaluators. |
| bofire/termination/api.py | Exports evaluator + mapping helpers. |
| bofire/termination/init.py | Package init (empty). |
| bofire/runners/run.py | Adds termination_condition support, early-stop logic, and new RunResult return type. |
| bofire/runners/api.py | Re-exports RunResult from runners API. |
| tests/bofire/data_models/test_termination.py | Unit tests for termination condition data models + serialization. |
| tests/bofire/termination/test_evaluator.py | Unit + integration tests for evaluator and run() early termination behavior. |
| tests/bofire/termination/test_mapper.py | Tests for termination mapper and evaluator discovery. |
| tests/bofire/termination/init.py | Test package init. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hi @wgst, thanks for the PR and sorry for the late answer. @bertiqwerty will provide a thorough and more detailed review. One general comment from my side: I think it would be the best to incorparate the termination criteria not into the runner but into the strategy itself, we have the Best, JOhannes |
| return iteration >= self.max_iterations | ||
|
|
||
|
|
||
| class AlwaysContinue(TerminationCondition, EvaluatableTermination): |
There was a problem hiding this comment.
This one also already exisits in the StepwiseStrategy as a condition.
There was a problem hiding this comment.
Good idea! I have now moved everything to StepwiseStrategy, so I hope it's in the right place now.
|
Thanks @wgst and @jduerholt. I agree with Johannes. Please use the |
bertiqwerty
left a comment
There was a problem hiding this comment.
Please refactor into the StepwiseStrategy.
…LCB regret bound - Move termination logic from runner into StepwiseStrategy conditions - Add UCBLCBRegretBoundCondition with 4 threshold modes (gp_noise, range, cv, manual) - Add UCBLCBRegretEvaluator with configurable beta (GP-UCB, Srinivas et al. 2010) - Add top-q filtering support for regret bound computation - Extract _min_ucb_evaluated() method for clarity - Add OptimizationComplete exception for clean termination signaling - Remove old termination data models, mapper, and tests (replaced by conditions) - Add comprehensive test suite (test_condition.py, test_evaluator.py) - All 49 tests passing
…evaluators Option A refactoring: both UCBLCBRegretEvaluator and ExpMinRegretGapEvaluator now support lcb_method parameter to choose between "sample" (default) and "optimize" LCB computation over the domain. Changes: - Convert _ucb_lcb_regret_bound from static to instance method - Add lcb_method parameter to ExpMinRegretGapEvaluator (was Makarova-only) - Move _min_lcb_optimize to base TerminationEvaluator class for code reuse - Unify LCB computation logic in both evaluators via _ucb_lcb_regret_bound - Add threshold computation helpers module (thresholds.py) - Update tests to verify both methods work correctly Both evaluators maintain backward compatibility (default "sample" method). Optimize method uses strategy's acqf_optimizer for more efficient LCB search.
…_min_lcb_optimize to base class Convert _ucb_lcb_regret_bound from static to instance method to access self.lcb_method. Move _min_lcb_optimize to TerminationEvaluator base class for code reuse by both Makarova and Ishibashi evaluators. Both evaluators now flexibly support "sample" (default) and "optimize" methods for LCB computation.
Refactor UCBLCBRegretBoundCondition and ExpMinRegretGapCondition to use new threshold computation helpers from thresholds module. Update data model exports.
Update benchmark functions and exports to support termination condition testing.
Add tests for new lcb_method parameter in both Makarova and Ishibashi evaluators. Verify both "sample" and "optimize" methods work correctly. Update condition specs and evaluator tests to reflect refactored code structure.
Moves each evaluator out of the monolithic evaluator.py into its own file (ucb_lcb.py, exp_min_regret_gap.py, log_eipc.py), leaving evaluator.py with just the TerminationEvaluator base class, and renames thresholds.py -> utils.py. Updates the api exports and the termination tests for the new layout. Pure reorganisation; no behaviour change.
Implements the PRB stopping criterion from Wilson (2024), "Stopping Bayesian Optimization with Probabilistic Regret Bounds": - ProbabilisticRegretBoundEvaluator: draws GP posterior paths via Matheron's rule, minimises each path with multistart L-BFGS-B, and runs a sequential Clopper-Pearson level test (cumulative geometric sample schedule + power-law risk budget) matching the reference implementation (j-wilson/trieste_stopping). clopper_pearson_ci is added to utils.py. - ProbabilisticRegretBoundCondition data model, wired into AnyCondition and CombiCondition and usable from StepwiseStrategy, plus a serialization spec. Adds unit, integration and conformance tests: known-answer Psi on a cosine GP, Clopper-Pearson formula identity, and the Algorithm 2 schedule / risk-budget / type-I error behaviour.
- Register LogEIPCCondition in the condition specs so it gets serialization and deserialization test coverage, matching the other termination conditions (UCBLCB, ExpMinRegretGap, PRB). - Seed the RandomStrategy initial design and the SoboStrategy acquisition optimizer in TestRegretBoundConvergence so the run is deterministic; the "generally decreases" assertion was previously flaky because the initial design used an unseeded RNG.
# Conflicts: # bofire/data_models/strategies/stepwise/conditions.py
Relocates bofire/termination/ -> bofire/strategies/stepwise/termination/ so
the termination feature lives entirely within the StepwiseStrategy paradigm,
per maintainer feedback ("use the StepwiseStrategy"). The condition data
models already live under data_models/strategies/stepwise/conditions.py and
run only via StepwiseStrategy; this moves the functional evaluators to mirror
that, removing the odd top-level bofire/termination/ package (which had no
matching data_models/termination/ counterpart).
Pure relocation: updates the import paths in the condition data models, the
package-internal imports, and the tests (also moved to
tests/bofire/strategies/stepwise/termination/). No behaviour change.
UCBLCBRegretBoundCondition built its evaluator with no arguments, so the GP-UCB beta and LCB-sampling knobs were stuck at defaults. Surface them as condition fields: delta, beta_scale, n_samples_lcb, batch_size, lcb_method, fallback_noise_variance (matching the subset ExpMinRegretGapCondition already exposes; the deeper beta-formula internals stay internal). ProbabilisticRegretBoundCondition now also exposes the Clopper-Pearson sample-schedule knobs initial_batch and batch_growth. Updates the serialization specs accordingly so the model_dump roundtrip still matches exactly.
_objective_sign now returns +1 for MaximizeObjective and -1 for MinimizeObjective, matching bofire.utils.multiobjective.get_ref_point_mask and BoTorch's maximize convention (it previously used the inverse). The regret evaluators work in a "lower is better" frame, so each derives its minimisation-frame sign with a single negation (sign = -direction; LogEIPC uses maximize = direction > 0). Behaviour is unchanged — confirmed by the negation-invariance tests.
…aluators - Un-standardize the learned likelihood noise exactly once in both automatic threshold paths: the Ishibashi adaptive threshold previously scaled it twice (noise * y_std**4) and the Makarova threshold not at all, making the default noise_variance=None modes mis-scaled whenever a Standardize outcome transform is active. _get_noise_variance now returns standardized-space noise (the noise_var_override convention) and evaluate() applies the single * y_std**2 conversion. - Compute the Ishibashi beta from the total observation count including the newly evaluated point, matching the reference (sqrt_beta from X.shape[0]) and the GP-UCB pairing of beta_t with the (t-1)-point posterior. - Make the CV threshold incumbent objective-direction aware (compute_threshold_cv gains a sign argument; previously idxmin even for maximization). - Move lcb_method ownership to the RegretBoundEvaluator base and cite Srinivas et al. (2010) in _compute_beta. - Add tests pinning both noise scale conventions; pin the GP-noise condition test to deterministic threshold extremes.
The best-fraction refit (topq/min_topq) lived in the data-model condition; the evaluator now owns it via _apply_topq, keeping the condition a thin delegate like the other termination conditions and making the evaluator self-contained for direct use (default topq=1.0 preserves standalone behaviour; the condition passes its 0.5 default). Also in the conditions module: - pass the objective direction to the CV threshold, - pass threshold_mode explicitly to the Ishibashi evaluator (it was implicitly relying on the adaptive_median default computing both thresholds), - batch_size is now Optional[PositiveInt] = None (single posterior call by default) with specs updated, and the non-serialized state of ExpMinRegretGapCondition is documented. Adds a test asserting top-q actually engages (beta computed from the filtered count).
- Restore the temporarily overridden cost_callable in a try/finally so a failure during evaluation cannot leave the GP-fitted cost callable permanently installed on the evaluator. - Suppress ty unresolved-attribute false positives on botorch Posterior attribute access (mean/variance/distribution exist on the GPyTorch posteriors used at runtime but not on the abstract base), matching the suppression idiom used elsewhere in the codebase. - Move the OptimizationComplete import in test_stepwise.py to the top of the file (E402).
Cover Booth, SixHumpCamel, HolderTable, CrossInTray, Easom, and Rosenbrock (2D default and dim=4) in the parametrized benchmark test, verifying output shapes and that f evaluated at get_optima() matches the claimed optimum.
run.py only differed from the PR base by an accidentally dropped ty-ignore comment (the branch predates the pyright-to-ty migration); restoring the base version removes the file from the PR diff.
|
Sorry, for the delay. I will try to look into this next week. |
|
Hi @jduerholt, and @bertiqwerty, I implemented all four termination conditions that I intended to. This includes:
As suggested by you previously, I've also moved all the conditions to StepwiseStrategy. Best, |
Motivation
BO loops in BoFire currently run for a fixed number of iterations. In practice, especially for chemical experiments, where each iteration is very costly, we need a way to stop optimization early enough to reduce costs but still get a converged result.
This is an attempt to implement different termination conditions methods. The first method currently added in this PR is from Makarova, A. et al. (2022). The idea behind this method is to compute an upper bound on the simple regret of the current best recommendation using the GP posterior, and when this bound falls below a threshold ε_BO (derived from the observation noise variance), then we are confident that any further improvement will be smaller than the noise, and optimization can stop.
Two other termination methods - issues with implementing
EMSR Gap: Ishibashi, H. et al. (2023). This method requires computing an Expected Minimum Simple Regret gap between consecutive iterations, which needs per-iteration model snapshots or knowledge of which experiments belong to which iteration. I have not found any tracking functionalities within BoFire, which makes it hard to implement this method at the moment.
Probabilistic Regret Bound (PRB): Wilson, J. et al. (2024). This is a more probabilistic method, in which a statistically valid stopping rule is constructed using Matheron-rule pathwise trajectory samples (Thompson-style posterior samples via pathwise conditioning). Their implementation is based on TensorFlow and BO packages around it, and some crucial functionalities are not available in BoTorch.
Both methods could be good candidates for future work. Adding the method by Ishibashi et al should be fairly easy once the tracking architecture is settled. Method by Wilson could be a bit more problematic, but let's see.
Have you read the Contributing Guidelines on pull requests?
Yes
Test Plan
Added tests:
test_termination.py)test_evaluator.py)test_mapper.py)Best,
Wojciech