Multi-fidelity Bayesian optimization¶
The problem it's solving¶
A high-fidelity run (fidelity 7) is accurate but expensive — hours per condition. A low-fidelity run (fidelity 5) is cheap — minutes — but systematically biased relative to the true, converged answer, not just noisier. Running only HF samples wastes the cheap ones; running only LF samples never finds the true optimum, since you're optimizing the wrong (biased) function. Multi-fidelity Bayesian optimization uses many cheap LF samples to learn the shape of the response surface, and a smaller number of expensive HF samples to learn the correction — where and how much the LF surface is wrong.
The surrogate: KRR-LR-GPR¶
train_surrogate.py fits a Kernel Ridge Regression + Linear
Regression + Gaussian Process Regression model (Yi et al.)
to the accumulated LF and HF data:
model.train(X=[X_hf, X_lf], Y=[y_hf, y_lf])
Conceptually: KRR learns the LF response surface cheaply from the many LF
points; a linear correction term is fit from the (few) HF points against
that LF surface; the GP layer provides calibrated uncertainty (mean,
var) at any query point, which is what makes an acquisition function like
EI possible at all — without a variance estimate, there's no way to
quantify "how much could this point still surprise us."
The vendored copy of this model
(scripts/mfbml_local/krr_lr_gpr.py) is deliberately numpy-only, with no
torch dependency, even though torch is listed in pyproject.toml's main
dependencies (pulled in transitively by f3dasm/mfbml/mfpml, not used
by this project's own training code path).
The acquisition function: Expected Improvement¶
suggest.py._expected_improvement is the standard maximization form:
improvement = mean - y_best - xi
EI = improvement * Φ(Z) + std * φ(Z) (0 where std == 0)
y_best is the best value observed so far — the incumbent EI is trying to
beat. xi (currently 0.01) is an exploration margin: a candidate has to
promise more than a small margin over the incumbent before it's rewarded
purely for its predicted mean, which keeps EI from over-exploiting a single
lucky point. Given this project's actual kLa ranges (roughly 0.002–0.02 in
the synthetic testbed's units), xi=0.01 is a substantial fraction of the
whole output range — worth an explicit ablation before trusting BO runs
where exploration/exploitation balance matters a lot.
A concrete illustration of why "incumbent" needs care¶
Until recently, y_best had a real bug: when no HF observations existed
yet, it fell back to the raw LF-observed maximum as the incumbent. That's
exactly the wrong thing to do — LF and HF are expected to disagree
systematically (that's the entire premise of training a bias correction in
the first place), so using an LF-scale value as an HF-scale incumbent
silently corrupts the very first acquisition decisions. The fix: when no HF
data exists, evaluate the already-trained surrogate (not the raw
observations) at the LF-observed inputs, and use its bias-corrected
prediction as the incumbent instead. This is a good example of a
multi-fidelity-specific failure mode that a single-fidelity BO
implementation would never surface — "best observed value" stops being a
simple max() the moment two different fidelities enter the picture.
Testing this cheaply¶
See Your first optimization loop
— the whole DoE→train→acquire→repeat loop runs against a synthetic
objective with a known optimum and a deliberate LF/HF bias, in seconds, with
no Basilisk build required. Two more real bugs (an ExperimentData schema
mismatch, and a crash in the final summary printout) were found by actually
running this loop end to end for the first time, rather than only testing
its pieces in isolation.