calibration_benchmark / src /pages /MethodDetails.py
odunbar's picture
EKS (#20)
3e13282
Raw History Blame Contribute Delete
74.7 kB
from pathlib import Path
import sys
import pandas as pd
import streamlit as st
try:
from common.method_registry import KNOWN_METHODS
except ModuleNotFoundError:
sys.path.append(str(Path(__file__).resolve().parents[1]))
from common.method_registry import KNOWN_METHODS
st.set_page_config(page_title="Method Configurations", page_icon="📘", layout="wide")
# Sidebar navigation
st.sidebar.title("Navigation")
st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
st.sidebar.page_link("pages/MethodDetails.py", label="Method configurations", icon="📘")
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
st.title("Method Configurations")
st.caption("Technical configuration of each calibration / UQ method, as implemented for this benchmark")
st.info(
"We are always seeking new algorithm families to be applied to this benchmark series. "
"If your favorite method is missing, or incorrectly configured, or if any algorithm is "
"lacking appropriate citation - please create an issue on `CalibrationLeaderboard.jl`, "
"and we can investigate this! Just note - we are interested in general behavior of "
"algorithm families, not hyper-optimizing any individual configuration to an individual "
"experiment."
)
st.markdown(
"For the exact implementation of each algorithm, see "
"[CalibrationLeaderboard.jl](https://github.com/odunbar/CalibrationLeaderboard.jl) "
"(`opt_experiments/` and `uq_experiments/`)."
)
# Per-method metadata (can be moved to a JSON/YAML later). Every entry below was read
# directly out of CalibrationLeaderboard.jl (opt_experiments/, uq_experiments/) and, for the
# EKP-based processes, out of EnsembleKalmanProcesses.jl itself. `params` intentionally
# excludes ensemble size and max-iteration count (covered by the leaderboard charts) and
# instead lists the update-specific hyperparameters.
method_meta = {
"TEKI": {
"citation": "Chada et al., SIAM/ASA J. UQ, 2020",
"url": "https://doi.org/10.1137/17M114402X",
"source": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `Inversion(prior)`.",
"math": (
"Ensemble Kalman Inversion (Schillings & Stuart, 2017, eqns. 4-5): each iteration moves every "
"ensemble member by the same linear, empirical-covariance-based correction toward the data,"
),
"math_eq": r"u_{n+1} = u_n + C^{ug}\left(C^{gg}+\Gamma\right)^{-1}\left(y - g(u_n)\right)",
"guardrails": (
"Tikhonov-style regularization is obtained by extending the state: the forward map is "
"augmented as `g ↦ [g; u]` and the data as `y ↦ [y; prior_mean]` with observation covariance "
"`blockdiag(Γ, prior_cov)`, so every step also pulls the ensemble back toward the prior "
"(`impose_prior=true`). A small `default_multiplicative_inflation` (1e-3) regularizes the "
"empirical covariance before it is inverted; `NoLocalization()` applies no covariance tapering; "
"`DefaultScheduler()` takes a fixed unit pseudo-time step each iteration."
),
"params": [
{"Parameter": "impose_prior", "Value": "true (Tikhonov-style state extension)"},
{"Parameter": "default_multiplicative_inflation", "Value": "1e-3"},
{"Parameter": "localization", "Value": "NoLocalization()"},
{"Parameter": "scheduler", "Value": "DefaultScheduler() — fixed unit step"},
],
"notes": "Runs until the whitened RMSE falls below the target or `N_iter` is exhausted; no reported pathology on this benchmark beyond the usual small-ensemble sampling error of stochastic EKI.",
},
"ETKI": {
"citation": "Schillings & Stuart, Numer. Math., 2017",
"url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
"source": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `TransformInversion(prior)`.",
"math": (
"Ensemble Transform Kalman Inversion applies the update in ensemble-anomaly space instead of "
"perturbing observations: normalized anomalies `X=(u-ū)/√(m-1)` and `Y=(g_ext-ḡ_ext)/√(m-1)` are "
"formed (with the same prior extension as TEKI), and a transform matrix built from `Y'Γ⁻¹Y` maps "
"the prior ensemble anomalies onto posterior anomalies, `u ← ū + X·T`."
),
"guardrails": (
"Same Tikhonov-style prior extension and `impose_prior=true` as TEKI; diagonal regularization "
"stabilizes the anomaly Gram matrix before it is inverted; `NoLocalization()`; the transform "
"update removes the perturbed-observation sampling noise present in stochastic EKI, which "
"generally lowers ensemble-collapse risk at small ensemble sizes."
),
"params": [
{"Parameter": "impose_prior", "Value": "true (Tikhonov-style state extension)"},
{"Parameter": "localization", "Value": "NoLocalization()"},
{"Parameter": "scheduler", "Value": "DefaultScheduler() — fixed unit step"},
],
"notes": "Same run-until-RMSE-target harness as TEKI; the transform (square-root) update trades stochastic sampling noise for a deterministic ensemble-anomaly map.",
},
"IEKF": {
"citation": "Iglesias, Inverse Problems, 2016",
"url": "https://doi.org/10.1088/0266-5611/32/2/025002",
"source": "`uq_experiments/GaussNewtonKalmanInversion` — EnsembleKalmanProcesses.jl `GaussNewtonInversion(prior)` (alias GNKI); Chada, Chen & Sanz-Alonso, 2021, Algorithm 4.1.",
"math": (
"Iterated Ensemble Kalman Filter with statistical linearization: each pseudo-time step `Δt` "
"relaxes the ensemble between its current position and a linearized Gauss-Newton solve of the "
"joint parameter/output covariance, evaluated against a fresh perturbed draw around the prior "
"mean rather than the ensemble's own mean — a Gauss-Newton normal-equation solve carried out "
"entirely in empirical-covariance space, without ever forming the model Jacobian explicitly."
),
"guardrails": (
"`perturbation_scale = 2/Δt - 1` rescales the prior-draw noise so the relaxation step's "
"stationary covariance equals the true posterior for every `Δt ∈ (0,1]`, not just `Δt=1`; "
"diagonal regularization + `NoLocalization()` stabilize the joint covariance solve; the pseudo-"
"step is fixed rather than adaptive (`DefaultScheduler(0.1)`, i.e. `Δt=0.1`)."
),
"params": [
{"Parameter": "pseudo-step Δt", "Value": "0.1 (DefaultScheduler(0.1))"},
{"Parameter": "perturbation_scale", "Value": "2/Δt - 1 = 19"},
{"Parameter": "localization", "Value": "NoLocalization()"},
],
"notes": "An iterative Gauss-Newton procedure, so — like other Newton-type ensemble methods — it can be sensitive to ensemble size at small N_ens; only the first `max_iter` (10 on L63, 15 on L96) of the `N_iter=20` allowed steps are scored.",
},
"UKI": {
"citation": "Huang, Huang & Stuart, Physica D, 2022",
"url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
"source": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `Unscented(prior; impose_prior=true)`.",
"math": (
"Unscented Kalman Inversion propagates `2·N_par + 1` deterministic sigma points (the symmetric "
"unscented transform) through the forward model instead of a random ensemble, forms their "
"weighted mean/covariance, and performs the same Kalman-style analysis step as TEKI on this "
"reduced sigma-point set."
),
"guardrails": (
"`α_reg ∈ (0,1]` shrinks the analysis mean toward the prior mean each step, guarding against "
"covariance collapse; `impose_prior=true` adds the same TEKI-style `y_ext`/`Γ_ext` extension; "
"the symmetric sigma-point scheme (`2·N_par+1` points) is used by default rather than the "
"cheaper simplex scheme."
),
"params": [
{"Parameter": "α_reg", "Value": "1.0 (regularization toward prior mean disabled by default)"},
{"Parameter": "sigma_points", "Value": '"symmetric" (2·N_par+1 points)'},
{"Parameter": "impose_prior", "Value": "true"},
],
"notes": "Sigma-point count is fixed by the parameter dimension, not swept as an ensemble size — UKI's per-iteration cost is dimension-driven rather than N_ens-driven.",
},
"ABC": {
"citation": "Csilléry et al., Trends in Ecology & Evolution, 2010",
"url": "https://doi.org/10.1016/j.tree.2010.04.001",
"source": "`uq_experiments/approximate_bayesian_computation` — simulation-based rejection sampling (no EKP, no surrogate).",
"math": (
"Each round draws `N_ens` i.i.d. prior candidates `θ ~ π(θ)`, evaluates the true forward model "
"`G(θ)`, and accepts a candidate iff its whitened misfit falls inside a chi-squared acceptance "
"region; accepted candidates across all rounds approximate draws from the ABC posterior."
),
"math_eq": r"\left(G(\theta)-y\right)^{\top}(2R)^{-1}\left(G(\theta)-y\right) \le \chi^2_{n_y}(1-\alpha_{\text{reject}})",
"guardrails": (
"The acceptance threshold is set exactly to the `(1-alpha_reject)` quantile of `χ²(n_y)` rather "
"than hand-tuned; the final pushforward resamples `n_pushforward_samples=1000` draws from a "
"Gaussian fit to the accepted pool, smoothing a possibly sparse accepted set into a usable "
"posterior approximation; batch sizes are deliberately matched to IEKF's `N_ens_sizes`/`max_iter` "
"so the N_ens×k budget axis is directly comparable across methods on the leaderboard."
),
"params": [
{"Parameter": "alpha_reject", "Value": "0.05"},
{"Parameter": "n_pushforward_samples", "Value": "1000"},
],
"notes": "Acceptance rate collapses quickly in higher dimensions (the same \"curse of dimensionality\" documented for HM below); there is no adaptive tightening of the acceptance region as rounds proceed, so all cost-efficiency comes from the batch-size choice. The Gaussian fit to the accepted pool additionally requires at least `p+2` accepted posterior samples before it can be estimated (`p` = parameter dimension); in higher-dimensional experiments this minimum is never reached within the 10,000-evaluation cap, so the pushforward Gaussian is never fit.",
},
"HM": {
"citation": "Williamson et al. 2013; King et al. 2025",
"url": "https://example.com/hm",
"source": "`uq_experiments/HistoryMatching` — custom (non-EKP) wave/rejection scheme.",
"math": (
"History Matching runs a series of waves. At wave k, an independent GP emulator is fit per "
"whitened output mode on an accumulated training set: the current ensemble plus all training "
"points from historical waves that lie within the current NROY space (points that have been "
"ruled out are dropped, so the emulator concentrates its accuracy on the shrinking NROY region); "
"a candidate survives the wave iff "
"its implausibility (whitened, GP-predicted misfit) is below a chi-squared threshold, and the "
"surviving (\"Not Ruled Out Yet\", NROY) region is intersected across all waves run so far."
),
"math_eq": r"I^2(\theta) = \sum_j \frac{\left(\mu_j(\theta)-y_j\right)^2}{1+\sigma_j^2(\theta)} < \chi^2_{k_R}(0.95)",
"guardrails": (
"GP emulators use an automatic-relevance-determination squared-exponential kernel (`SEArd`), "
"refit every wave via `GaussianProcesses.optimize!`; NROY testing exploits monotonicity — a "
"candidate ruled out by an earlier wave is never re-tested by later waves — cutting GP-predict "
"cost from O(waves×batch) to O(surviving candidates); a Poisson/Garwood upper-confidence-bound "
"rule (`poisson_rate_upper_bound`, `early_stop_alpha=1e-6`) aborts a rejection-sampling sweep "
"early once even its best-case acceptance rate can no longer plausibly reach the wanted sample "
"count within the remaining budget, rather than burning the full `max_samples` budget on a "
"doomed sweep; inputs/outputs are PCA-whitened (`retain_var_input`/`retain_var`) before fitting."
),
"params": [
{"Parameter": "confidence (implausibility threshold)", "Value": "0.95 → quantile(Chisq(k_R), 0.95)"},
{"Parameter": "n_candidate_batch", "Value": "2000 per wave"},
{"Parameter": "retain_var (output PCA)", "Value": "0.99"},
{"Parameter": "retain_var_input (input PCA)", "Value": "0.9"},
{"Parameter": "early_stop_alpha", "Value": "1e-6"},
],
"notes": "Acceptance rate collapses toward zero after a couple of waves on higher-dimensional problems (the repo's own documented \"curse of dimensionality\"); the early-stop rule only changes how quickly a doomed sweep gives up, not the eventual pass/fail outcome.",
},
"CES-EKI-DMC": {
"citation": "Cleary et al., J. Comput. Phys., 2021",
"url": "https://doi.org/10.1016/j.jcp.2020.109716",
"source": "`uq_experiments/calibrate_emulate_sample` — Calibrate-Emulate-Sample, calibrate stage: `Inversion()` + `DataMisfitController`.",
"math": (
"Three-stage Calibrate-Emulate-Sample. Calibrate: run EKI to generate an ensemble of "
"`(θ, G(θ))` training pairs. Emulate: fit a random-feature surrogate `Ĝ(θ)` on those pairs. "
"Sample: draw from `π(θ|y) ∝ exp(-½‖y-Ĝ(θ)‖²_Γ)·π(θ)` against the cheap surrogate using "
"preconditioned Crank-Nicolson Metropolis-Hastings (pCN-MH), rather than the true forward model."
),
"guardrails": (
"Calibrate stage uses an adaptive `DataMisfitController(terminate_at=2.0)` schedule so the EKI "
"pseudo-time step grows automatically and training points concentrate near the posterior, "
"rather than requiring a hand-tuned fixed schedule (`terminate_at=2.0` approximates the "
"finite-time posterior); emulate stage keeps 99% of variance in a likelihood-informed PCA "
"encoding before fitting the `ScalarRandomFeatureInterface`; sample stage auto-tunes the pCN "
"step size (`optimize_stepsize`, 2,000-sample dry run from `init_stepsize=0.2`) before the full "
"100,000-sample chain with a 2,000-sample burn-in."
),
"params": [
{"Parameter": "scheduler (calibrate)", "Value": "DataMisfitController(terminate_at=2.0)"},
{"Parameter": "retain_var (emulate PCA)", "Value": "0.99"},
{"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
{"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
{"Parameter": "init_stepsize", "Value": "0.2 (auto-tuned before the full chain)"},
],
"notes": "The emulator is fit only once, after calibration finishes (\"after-optimize\"), so sampled-posterior quality depends entirely on how well the calibrate-stage ensemble has already covered the posterior region.",
},
"CES-EKI-CONST": {
"citation": "Cleary et al., J. Comput. Phys., 2021",
"url": "https://doi.org/10.1016/j.jcp.2020.109716",
"source": "`uq_experiments/calibrate_emulate_sample` — Calibrate-Emulate-Sample, calibrate stage: `Inversion()` + fixed-step scheduler.",
"math": "Identical Calibrate-Emulate-Sample pipeline to CES-EKI-DMC (see that entry for the emulate/sample math).",
"guardrails": (
"Calibrate stage uses a fixed-step `DefaultScheduler(0.1)` instead of the adaptive "
"`DataMisfitController`, trading adaptivity for a simpler, more predictable schedule; emulate "
"and sample stages are unchanged from CES-EKI-DMC (same PCA retention, feature counts, and "
"MCMC step-size tuning)."
),
"params": [
{"Parameter": "scheduler (calibrate)", "Value": "DefaultScheduler(0.1) — fixed step"},
{"Parameter": "retain_var (emulate PCA)", "Value": "0.99"},
{"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
{"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
],
"notes": "Same after-optimize emulator caveat as CES-EKI-DMC; the fixed step trades the DMC variant's adaptive posterior-concentration for predictability.",
},
"CES-IEKF-CONST": {
"citation": "Cleary et al., J. Comput. Phys., 2021; Iglesias, Inverse Problems, 2016",
"url": "https://doi.org/10.1016/j.jcp.2020.109716",
"source": "`uq_experiments/calibrate_emulate_sample` — Calibrate-Emulate-Sample, calibrate stage: `GaussNewtonInversion(prior)` + fixed-step scheduler.",
"math": "Identical Calibrate-Emulate-Sample pipeline to CES-EKI-DMC, but the calibrate stage runs the IEKF/GNKI Gauss-Newton update (see the IEKF entry) instead of plain EKI.",
"guardrails": (
"Calibrate stage uses `GaussNewtonInversion(prior)` with a fixed pseudo-step `DefaultScheduler(0.1)`; "
"emulate and sample stages are unchanged from CES-EKI-DMC (same PCA retention, feature counts, "
"and MCMC step-size tuning)."
),
"params": [
{"Parameter": "scheduler (calibrate)", "Value": "DefaultScheduler(0.1) — fixed pseudo-step"},
{"Parameter": "retain_var (emulate PCA)", "Value": "0.99"},
{"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
{"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
],
"notes": "Same after-optimize emulator caveat as CES-EKI-DMC, with the Gauss-Newton calibrate step's usual small-ensemble sensitivity (see IEKF).",
},
"CES-EKS": {
"citation": "Cleary et al., J. Comput. Phys., 2021; Garbuno-Inigo, Nüsken and Reich, SIAM J. Appl. Dyn. Syst., 2020",
"url": "https://doi.org/10.1016/j.jcp.2020.109716",
"source": "`uq_experiments/calibrate_emulate_sample` — `CALIBRATE_METHOD=eks` (calibrate stage `Sampler(prior)` + `EKSStableScheduler()`), `DIM_REDUCTION=PCA` (leaderboard key `ces-eks-pca`).",
"math": (
"Calibrate-Emulate-Sample whose calibrate stage runs the Ensemble Kalman Sampler (see the EKS entry) instead of "
"plain EKI. Calibrate: run the sampler to generate `(θ, G(θ))` training pairs; iteration `k` trains on "
"the pairs from the first `k` steps. Emulate: fit a scalar random-feature surrogate `Ĝ(θ)` on those "
"pairs. Sample: draw from `π(θ|y) ∝ exp(-½‖y-Ĝ(θ)‖²_Γ)·π(θ)` against the cheap surrogate using "
"preconditioned Crank-Nicolson Metropolis-Hastings (pCN-MH), rather than the true forward model."
),
"guardrails": (
"Calibrate stage: `Sampler(prior)` with the data-dependent `EKSStableScheduler()`. The random-feature "
"kernel has a `1e-6` diagonal nugget. The nullspace-noise injector is switched off for this variant, so directions dropped by the "
"input PCA are decoded at the prior mean rather than filled with prior noise (the experiment config "
"notes that injecting noise inflated the posterior on L96 flux-force, with output spread about 2x too "
"wide and no contraction with `k`). Sample stage auto-tunes the pCN step size (`optimize_stepsize`, "
"2,000-sample dry run) before the full chain."
),
"params": [
{"Parameter": "calibrate sampler / scheduler", "Value": "Sampler(prior) / EKSStableScheduler()"},
{"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
{"Parameter": "kernel nugget", "Value": "1e-6"},
{"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
{"Parameter": "init_stepsize", "Value": "0.2 (L63) / 0.01 (L96), auto-tuned before the full chain"},
{"Parameter": "nullspace noise injector", "Value": "off (threshold Inf)"},
],
"notes": (
"The emulator is fit once per iteration `k` on the sampler's first `k` steps, so quality depends on "
"how well the calibrate-stage ensemble covers the posterior region. In the current UQ data "
"(c = 1.5, best reliable ensemble size) the mean budget is 5.3 on L63, 6.0 on L96 const-force, 87.5 "
"on vec-force (0% failures) and 207 on NN flux-force, which fails on at least 5% of seeds at every "
"size (15% at the best one)."
),
},
"ADAM": {
"citation": "Kingma & Ba, ICLR, 2015",
"url": "https://doi.org/10.48550/arXiv.1412.6980",
"source": "`opt_experiments/adam` — custom (non-Flux) Adam implementation.",
"math": (
"Adam gradient descent on the whitened least-squares loss `L(θ)=½‖R^{-1/2}(y-G(θ))‖²`: "
"first/second raw-gradient moments `m_t=β₁m_{t-1}+(1-β₁)g_t`, `v_t=β₂v_{t-1}+(1-β₂)g_t²` are "
"bias-corrected to `m̂_t, v̂_t` and applied as `θ_{t+1}=θ_t - α·m̂_t/(√v̂_t+ε)`. The gradient "
"`g_t` comes from a whitened Jacobian obtained via `ForwardDiff.jl` forward-mode automatic "
"differentiation through the Lorenz solver itself — not a surrogate."
),
"guardrails": (
"`N_ens` initial-condition perturbations are drawn and averaged into the gradient at every step "
"to reduce gradient noise from the chaotic forward model (`N_ens=1` here, i.e. a single-sample "
"stochastic gradient); beyond Adam's own adaptive per-parameter step scaling there is no line "
"search, trust region, or restart mechanism."
),
"params": [
{"Parameter": "α (step size)", "Value": "0.001"},
{"Parameter": "β₁, β₂", "Value": "0.9, 0.999"},
{"Parameter": "ε", "Value": "1e-8"},
{"Parameter": "N_ens (IC perturbations)", "Value": "1"},
],
"notes": "**Empirically gets stuck in local minima close to the initial parameter draw, regardless of step size** — observed running this benchmark. Consistent with Adam having no mechanism (trust region, restart, ensemble diversity) to escape a bad basin once its gradient estimate vanishes there.",
},
"LM": {
"citation": "Levenberg, 1944; Marquardt, 1963; Fletcher, 1971",
"url": "https://doi.org/10.1090/qam/10666",
"source": "`opt_experiments/levenberg_marquardt` — custom Levenberg-Marquardt implementation.",
"math": (
"Damped Gauss-Newton: each step solves the damped normal equations for the parameter update "
"`Δθ`, using a whitened Jacobian/residual `(J̃, r̃)` obtained via `ForwardDiff.jl`, then accepts "
"the step only if the reduction ratio `ρ` (actual vs. predicted misfit decrease) is favorable."
),
"math_eq": r"\left(\tilde J^{\top}\tilde J + \lambda\,\mathrm{diag}(d)\right)\Delta\theta = \tilde J^{\top}\tilde r",
"guardrails": (
"Damping `λ` adapts every step: ×4 (capped at `λ_max`) on a rejected or non-finite step "
"(`ρ<0.25` or NaN), ÷3 (floored at `λ_min`) on an accepted step (`ρ>0.75`), interpolating between "
"gradient descent (large λ) and Gauss-Newton (small λ); a non-finite `Δθ` is handled gracefully "
"(`ρ=NaN → !isfinite(ρ) → λ↑`) rather than crashing the run."
),
"params": [
{"Parameter": "λ₀ (initial damping)", "Value": "1.0"},
{"Parameter": "λ growth / shrink factor", "Value": "×4 (reject) / ÷3 (accept)"},
{"Parameter": "λ bounds", "Value": "[1e-10, 1e8]"},
{"Parameter": "N_ens (IC perturbations)", "Value": "1"},
],
"notes": "**Empirically also gets stuck in local minima near the initial condition, regardless of the damping schedule** — the same failure mode as ADAM, unsurprising for any single-trajectory local optimizer on this multimodal, chaotic-dynamics calibration surface.",
},
"GBOED-TMCMC": {
"citation": "Holthuijzen et al. 2026",
"url": "https://arxiv.org/abs/2508.13071",
"source": "`uq_experiments/BayesianOptimalExperimentalDesign` — Goal-oriented BOED, Algorithm 2 of Holthuijzen et al. 2026.",
"math": (
"Each iteration: (i) fits a GP surrogate per output dimension to all `(θ, G(θ))` collected so "
"far, (ii) draws posterior samples from the current surrogate-based posterior via Transitional/"
"Sequential MCMC (Ching & Chen, 2007), (iii) selects the next batch of forward-model evaluations "
"by maximizing a closed-form Expected Information Gain (EIG) criterion targeted at the quantity "
"of interest, using the GP's predictive covariance."
),
"math_eq": r"\mathrm{EIG}(X) = \frac{1}{k}\sum_j \frac{1}{2}\log\frac{\det K_{pp}^{(j)}}{\det \Sigma'^{(j)}}, \quad \Sigma'^{(j)} = K_{pp}^{(j)} - K_{pc}^{(j)}\left(K_{cc}^{(j)}\right)^{-1}K_{cp}^{(j)}",
"guardrails": (
"Candidate batches are optimized with `Optim.jl`'s `Fminbox(LBFGS())` under box constraints, "
"with a hard cap on raw EIG evaluations (`eig_call_limit`) and an outer-iteration cap "
"(`eig_outer_iters`) enforced manually — since Optim's own call limits don't cumulate across "
"Fminbox's outer barrier rounds — logging a warning that the returned point may be "
"\"under-optimized\" if either cap is hit, rather than failing silently; a diagonal jitter "
"(`eig_jitter`, relative to the GP signal variance) stabilizes the covariance solves/log-"
"determinants; batch points can be chosen greedily, jointly, or via a hybrid strategy that "
"switches from greedy to joint optimization once a greedy step's cost exceeds a threshold."
),
"params": [
{"Parameter": "n_posterior_samples (TMCMC population)", "Value": "1000"},
{"Parameter": "tmcmc_burnin / tmcmc_thin", "Value": "5 / 1 (below TMCMC's 20 / 3 defaults)"},
{"Parameter": "n_eig_posterior_samples (EIG subsample X')", "Value": "50"},
{"Parameter": "eig_region / eig_ball_quantile", "Value": ":ball / 0.999 (χ² ball in prior-whitened space)"},
{"Parameter": "eig_optim_iters / eig_outer_iters", "Value": "500 / 20"},
{"Parameter": "eig_call_limit", "Value": "2000 EIG evaluations"},
{"Parameter": "eig_g_tol / eig_f_reltol", "Value": "1e-3 / 1e-6"},
{"Parameter": "eig_jitter", "Value": "1e-6 × GP signal variance"},
{"Parameter": "gp_optim_iters / gp_outer_iters", "Value": "50 / 3"},
{"Parameter": "batch_init_strategy", "Value": ":posterior_subsample"},
],
"notes": "**Both the Transitional MCMC (TMCMC) posterior sampling and the EIG batch optimization contribute to performance.** TMCMC needs a sufficiently large population (`n_posterior_samples`) to represent the posterior, particularly in higher dimensions; its cost grows roughly linearly with population size, and `tmcmc_burnin`/`tmcmc_thin` multiply the per-stage cost. The EIG objective uses only a small posterior subsample (`n_eig_posterior_samples`), as EIG costs ~n³ per output mode. **The inner EIG-maximization loop is a frequent source of problems (hitting the call-limit / outer-iteration guardrails, i.e. \"under-optimized\" batches), and every iteration slows down as the cumulative GP training set grows**, which is why the higher-dimensional L96 cases use a smaller `max_iters`.",
},
"LM-KGMM": {
"citation": "Giorgini, Bischoff & Souza, arXiv:2503.18054, 2025",
"url": "https://arxiv.org/abs/2503.18054",
"source": "`opt_experiments/score_based_lm` — Levenberg-Marquardt with a GFDT/KGMM Jacobian surrogate.",
"math": (
"Same LM step as above, but the Jacobian is not differentiated through the solver — it is "
"estimated from trajectory data via the generalized fluctuation-dissipation theorem (GFDT): the "
"response of a fitted statistic `A(x)` to a parameter perturbation is approximated by a lag-"
"integrated, Tukey-tapered correlation with a \"conjugate\" variable `B(x)` built from the score "
"function `s(x)=∇log p(x)`, then chained through `dφ/dm` to the summary-statistic Jacobian."
),
"math_eq": r"\frac{d\,\mathbb{E}[A]}{d\theta} \approx \int_0^{\tau_{\max}} \left\langle A(x_{t+\tau})\,B(x_t)\right\rangle\, w(\tau)\, d\tau",
"guardrails": (
"The score `s(x)` is estimated with a k-means Gaussian-mixture score estimator (KGMM): the "
"trajectory is clustered into components whose mixture has a closed-form score. "
"`stein_recalibrate` rescales the estimated score so `-E[x·s(x)ᵀ]=I` holds on the calibration "
"ensemble (a built-in consistency check/correction); `kgmm_ridge` shrinks per-cluster "
"covariances toward the global covariance to avoid singular clusters; `is_collapsed_window` "
"(minimum trajectory variance below a tolerance) detects windows that have collapsed onto a "
"fixed point — GFDT is undefined there, since there is no invariant measure to differentiate — "
"and falls back to a direct `ForwardDiff.jl` Jacobian on those draws; total cost is capped at "
"`outer_iter × N_ens ≤ budget_total` per cell, trading ensemble size against iteration count "
"under a fixed evaluation budget."
),
"params": [
{"Parameter": "kgmm_ridge", "Value": "0.1"},
{"Parameter": "tukey_alpha (response taper)", "Value": "0.75"},
{"Parameter": "budget_total (outer_iter × N_ens)", "Value": "500"},
{"Parameter": "collapse tolerance (min variance)", "Value": "1e-2"},
],
"notes": "**Much quicker to apply than LM-DSM** — fitting a k-means/Gaussian-mixture score is far cheaper than training a neural score network, so LM-KGMM stayed tractable on problems where LM-DSM's training cost made it impractical.",
},
"LM-DSM": {
"citation": "Giorgini, Bischoff & Souza, arXiv:2509.19660, 2025",
"url": "https://arxiv.org/abs/2509.19660",
"source": "`opt_experiments/score_based_lm` — Levenberg-Marquardt with a GFDT/DSM Jacobian surrogate.",
"math": "Identical LM + GFDT scheme as LM-KGMM (see that entry's update equation), but the score `s(x)=∇log p(x)` is estimated by Denoising Score Matching (DSM) instead of KGMM.",
"guardrails": (
"A neural network is trained to denoise samples perturbed by Gaussian noise at a single fixed "
"scale `σ`; its optimal denoiser equals the noised density's score (Vincent, 2011), and as "
"`σ→0` this recovers the true score at the cost of a harder training problem. `σ` is fixed per "
"benchmark rather than annealed, chosen empirically as the smallest noise level at which the "
"network still receives a usable learning signal. Same Stein recalibration and collapsed-window "
"`ForwardDiff.jl` fallback as LM-KGMM; same Tukey-tapered response integral and evaluation-"
"budget cap."
),
"params": [
{"Parameter": "σ (DSM noise scale)", "Value": "0.2 (L63) / 0.025 (L96 variants)"},
{"Parameter": "tukey_alpha (response taper)", "Value": "0.75"},
{"Parameter": "budget_total (outer_iter × N_ens)", "Value": "500"},
{"Parameter": "collapse tolerance (min variance)", "Value": "1e-2"},
],
"notes": "**Training the DSM score network is very slow, even on moderately sized problems** — a 5-iteration smoke test on the higher-dimensional L96 forcing variants took ~39 minutes, extrapolating to several hours for a full run, and higher-dimensional experiments were killed after ~24h without finishing a sweep. Wall-clock cost, not accuracy, was the binding constraint on scaling LM-DSM to larger problems.",
},
"ADAM-KGMM": {
"citation": "Giorgini, Bischoff & Souza, arXiv:2503.18054, 2025",
"url": "https://arxiv.org/abs/2503.18054",
"source": "`opt_experiments/score_based_adam` — Adam with a GFDT/KGMM gradient surrogate.",
"math": (
"Same Adam moment-based update as above, but the gradient `g_t` is not differentiated through "
"the solver — it is estimated from trajectory data via the generalized fluctuation-dissipation "
"theorem (GFDT), the same lag-integrated, Tukey-tapered response construction used by LM-KGMM "
"(see that entry's update equation), chained through `dφ/dm` to the whitened-loss gradient "
"instead of a Gauss-Newton Jacobian."
),
"guardrails": (
"Same GFDT machinery as LM-KGMM: the score `s(x)=∇log p(x)` is estimated with a k-means "
"Gaussian-mixture score estimator (KGMM), `stein_recalibrate` rescales it so `-E[x·s(x)ᵀ]=I` "
"holds on the calibration ensemble, `kgmm_ridge` shrinks per-cluster covariances toward the "
"global covariance, and `is_collapsed_window` falls back to a direct `ForwardDiff.jl` gradient "
"on windows that have collapsed onto a fixed point; total cost is capped at "
"`outer_iter × N_ens ≤ budget_total` per cell, same as LM-KGMM, trading ensemble size against "
"iteration count under a fixed evaluation budget; beyond that, Adam's own adaptive per-parameter "
"step scaling is the only guardrail on the step itself. Unlike `β₁`/`β₂`/`ε` (shared across all "
"cases), the step size `α` is set per-experiment from the case's prior — sized to ~0.25× the "
"mean prior standard deviation, so a run of consistent-sign steps crosses ~1 prior std in ~4 "
"outer iterations; a single global value tuned for one benchmark's residual scaling left Adam "
"unable to traverse even the L63/L96-const prior within its iteration budget."
),
"params": [
{"Parameter": "α (step size)", "Value": "Per-case, ~0.25× prior std (0.025–0.75 across benchmarks)"},
{"Parameter": "β₁, β₂", "Value": "0.9, 0.999"},
{"Parameter": "kgmm_ridge", "Value": "0.1"},
{"Parameter": "tukey_alpha (response taper)", "Value": "0.75"},
{"Parameter": "budget_total (outer_iter × N_ens)", "Value": "500"},
],
"notes": "**Inherits Adam's local-minimum susceptibility** (no trust region, restart, or ensemble diversity to escape a bad basin) **and LM-KGMM's cheaper-than-DSM score estimation** — the k-means/Gaussian-mixture score fit is far less costly than training a neural score network, so ADAM-KGMM stayed tractable on problems where a DSM-based Adam variant would not.",
},
"ADAM-DSM": {
"citation": "Giorgini, Bischoff & Souza, arXiv:2509.19660, 2025",
"url": "https://arxiv.org/abs/2509.19660",
"source": "`opt_experiments/score_based_adam` — Adam with a GFDT/DSM gradient surrogate.",
"math": "Identical Adam + GFDT scheme as ADAM-KGMM (see that entry's update equation), but the score `s(x)=∇log p(x)` is estimated by Denoising Score Matching (DSM) instead of KGMM.",
"guardrails": (
"Same DSM training scheme as LM-DSM: a neural network is trained to denoise samples perturbed "
"by Gaussian noise at a single fixed scale `σ`, whose optimal denoiser equals the noised "
"density's score (Vincent, 2011); `σ` is fixed per benchmark rather than annealed, chosen "
"empirically as the smallest noise level at which the network still receives a usable learning "
"signal. Same Stein recalibration and collapsed-window `ForwardDiff.jl` fallback, and the same "
"Tukey-tapered response integral and evaluation-budget cap as LM-DSM/ADAM-KGMM. Same per-case "
"step size `α` (shared `β₁`/`β₂`/`ε`) as ADAM-KGMM — sized to ~0.25× the case's prior standard "
"deviation, since a single global value tuned for one benchmark's residual scaling left Adam "
"unable to traverse even the L63/L96-const prior within its iteration budget."
),
"params": [
{"Parameter": "α (step size)", "Value": "Per-case, ~0.25× prior std (0.025–0.75 across benchmarks)"},
{"Parameter": "β₁, β₂", "Value": "0.9, 0.999"},
{"Parameter": "σ (DSM noise scale)", "Value": "0.2 (L63) / 0.025 (L96 variants)"},
{"Parameter": "tukey_alpha (response taper)", "Value": "0.75"},
{"Parameter": "budget_total (outer_iter × N_ens)", "Value": "500"},
],
"notes": "**Inherits both LM-DSM's slow DSM-training cost and Adam's local-minimum susceptibility** — training the DSM score network is the binding wall-clock cost (as with LM-DSM), and Adam's lack of a trust region or ensemble diversity leaves it as prone to getting stuck in a bad basin as ADAM-KGMM.",
},
"RL-DDPG-SHORT": {
"citation": "Lillicrap et al., ICLR, 2016",
"url": "https://arxiv.org/abs/1509.02971",
"source": "`opt_experiments/ReinforcementLearning` — DDPG actor-critic (`ddpg_core.jl`, `rl_env.jl`, `rl_run.jl`), `UPDATE_FREQUENCY=short`.",
"math": (
"The unknown forcing parameterization is the **policy** and the whitened statistics misfit "
"`Φ_k=‖y-G(x_{T_k})‖²_Γ` defines the **reward** `r_k=Φ_{k-1}-Φ_k`. A small critic MLP `q(x,F)` is "
"trained on replay-buffer Bellman targets `-(1-γ)Φ_k+γ·q(x_{k+1},π(x_{k+1}))` (the `Φ_{k-1}` "
"offset cancels exactly), and the policy parameters are ascended along `∇_F q` averaged over "
"buffer states with Adam. No Jacobian of the simulator is ever formed — the gradient comes "
"from differentiating the learned critic. **The prior enters in three ways, but not as a "
"penalty:** the initial policy is a random draw from the prior, the policy is stored in "
"prior-whitened coordinates `F=μ+Lθ̃` (`L` = Cholesky factor of the prior covariance) so the "
"Adam step and the Gaussian exploration noise are measured in prior standard deviations "
"(following the prior's correlations), and the critic sees the whitened action. The reward "
"is the misfit only. In the *short* variant one RL step advances a "
"sliding statistics window by 1/10 of its length, so there are ~10 RL updates per window of "
"simulated time, each costing a fraction of a forward evaluation."
),
"math_eq": r"\theta \leftarrow \theta + \mathrm{Adam}\!\left(\mathbb{E}_{x}\left[\nabla_F\, q_\vartheta(x,F)\right]_{F=F_\theta}\right)",
"guardrails": (
"`γ=0.5<1` is essential: for a state-independent policy at `γ=1` the reward telescopes and "
"`∇_F Q` vanishes, so `γ=1` reproduces the no-learning control. A Polyak-averaged target critic "
"(`τ=0.05`) stabilizes the bootstrap; critic inputs/targets are standardized from warm-up data "
"and frozen; Gaussian exploration noise (0.1 prior std) is added to the applied action, with a "
"warm-up phase before the first update. The statistic is computed over a trailing window of "
"the full length `W` (not the short step) so it is not biased by a shortened estimation "
"window. Cost is counted as simulation only (1 initial full evaluation + stride/T per step); "
"per-cell budget is capped at 10000 units."
),
"params": [
{"Parameter": "γ (discount)", "Value": "0.5"},
{"Parameter": "Polyak rate", "Value": "0.05"},
{"Parameter": "critic_lr / actor_lr", "Value": "1e-3 / 0.02 (prior-std units)"},
{"Parameter": "batch size / updates per step", "Value": "64 / 1"},
{"Parameter": "steps per window / warm-up steps / buffer size", "Value": "10 / 50 / 5000"},
{"Parameter": "exploration noise / warm-up fraction", "Value": "0.1 / 0.5"},
{"Parameter": "critic hidden width", "Value": "64 (L63, L96 const) / 128 (vec, flux)"},
],
"notes": (
"**Cost advantage is partly structural** — the continuing RL trajectory pays spin-up once rather "
"than every step (for L63 `T_start/T=0.75`), and the convergence test runs at every stride on "
"overlapping windows, so scores are not directly comparable to methods that re-pay spin-up each "
"evaluation. **Not tuned per case.** Converges on every seed on "
"L63 and L96-const, but the higher-dimensional forcings are the hard case for a learned "
"critic: on L96 flux-force and vec-force it reaches the targets on only a few of 30 seeds "
"(flux: 1 at target 1.1, 5 at 1.2; vec: 2 at 1.1, 4 at 1.2) and on none at 1.0 within the budget."
),
},
"RL-DDPG": {
"citation": "Lillicrap et al., ICLR, 2016",
"url": "https://arxiv.org/abs/1509.02971",
"source": "`opt_experiments/ReinforcementLearning` — DDPG actor-critic (`ddpg_core.jl`, `rl_env.jl`, `rl_run.jl`), `UPDATE_FREQUENCY=long` (leaderboard name `rl-ddpg-long`).",
"math": (
"Same DDPG actor-critic scheme as RL-DDPG-SHORT (see that entry's update equation): the unknown "
"forcing parameterization is the **policy**, the decrease in the whitened statistics misfit "
"`Φ_k` is the **reward**, and the policy parameters are ascended along `∇_F q` of a small learned "
"critic. The prior enters as in RL-DDPG-SHORT: a random prior draw as the initial policy, "
"prior-whitened policy coordinates (step and exploration noise in prior standard deviations), "
"and no prior penalty in the reward. Here one RL step is the whole statistics window — exactly one standard forward "
"evaluation (spin-up + window), the same statistical window as every other method on the "
"leaderboard — so there is one RL update per forward evaluation and each step costs 1 unit."
),
"math_eq": r"\theta \leftarrow \theta + \mathrm{Adam}\!\left(\mathbb{E}_{x}\left[\nabla_F\, q_\vartheta(x,F)\right]_{F=F_\theta}\right)",
"guardrails": (
"Same `γ=0.5<1` (at `γ=1` the critic has no `F`-dependence for a state-independent policy), "
"Polyak target critic (`τ=0.05`), frozen input/target standardization, and Gaussian exploration "
"noise (0.1 prior std) as RL-DDPG-SHORT. The statistic `G_k` is exactly the standard forward map "
"used by every other method, and the spin-up is paid on every step; per-cell budget is capped "
"at 10000 units."
),
"params": [
{"Parameter": "γ (discount)", "Value": "0.5"},
{"Parameter": "Polyak rate", "Value": "0.05"},
{"Parameter": "critic_lr / actor_lr", "Value": "1e-3 / 0.02 (prior-std units)"},
{"Parameter": "batch size / updates per step", "Value": "64 / 1"},
{"Parameter": "steps per window / warm-up steps / buffer size", "Value": "1 / 20 / 2000"},
{"Parameter": "exploration noise / warm-up fraction", "Value": "0.1 / 0.5"},
{"Parameter": "critic hidden width", "Value": "64 (L63, L96 const) / 128 (vec, flux)"},
],
"notes": (
"**One RL update per forward evaluation, so far more expensive than RL-DDPG-SHORT** — the short "
"variant makes ~10× more updates per unit of cost and pays spin-up only once. **Not tuned per "
"case.** Converges reliably on L63 and L96-const, but never "
"reached the targets on L96 flux-force or vec-force within the budget."
),
},
"RL-TD3-SHORT": {
"citation": "Fujimoto, van Hoof & Meger, ICML, 2018",
"url": "https://arxiv.org/abs/1802.09477",
"source": "`opt_experiments/ReinforcementLearning` — TD3 actor-critic (`ddpg_core.jl`, `rl_env.jl`, `rl_run.jl`), `ALGORITHM=td3`, `UPDATE_FREQUENCY=short`.",
"math": (
"Same actor-critic formulation as RL-DDPG-SHORT (see that entry): the unknown forcing "
"parameterization is the **policy**, the decrease in the whitened statistics misfit is the "
"**reward**, and the policy parameters are ascended along `∇_F q` of a learned critic. TD3 "
"replaces the single critic with **twin critics** whose minimum forms the Bellman target "
"`-(1-γ)Φ_k+γ·min_i q_i(x_{k+1},π(x_{k+1}))`, and the actor ascends critic 1 only. The prior "
"enters as in RL-DDPG-SHORT (random prior draw as the initial policy; prior-whitened policy "
"coordinates for the Adam step, exploration and target-smoothing noise; no prior penalty in "
"the reward). In the *short* variant one RL step advances a sliding statistics window by 1/10 of its length, so "
"there are ~10 RL updates per window of simulated time, each costing a fraction of a forward "
"evaluation."
),
"math_eq": r"\theta \leftarrow \theta + \mathrm{Adam}\!\left(\mathbb{E}_{x}\left[\nabla_F\, q^{(1)}_\vartheta(x,F)\right]_{F=F_\theta}\right)",
"guardrails": (
"The three TD3 robustness heuristics are switched on relative to RL-DDPG: **twin critics** with a "
"min-target (reduces critic overestimation), **delayed policy updates** (the actor and the "
"Polyak target critics update once per 2 critic updates), and **target-policy smoothing** "
"(clipped Gaussian noise added to the target action, in prior-std units, since the actor is a "
"single vector). As for RL-DDPG there is no target actor. Same `γ=0.5<1`, Polyak rate, frozen "
"input/target standardization, 0.1 prior-std Gaussian exploration noise and trailing window of "
"full length `W` as RL-DDPG-SHORT; cost counts simulation only (1 initial evaluation + stride/T "
"per step), capped at 10000 units per cell."
),
"params": [
{"Parameter": "γ (discount)", "Value": "0.5"},
{"Parameter": "Polyak rate", "Value": "0.05"},
{"Parameter": "critics / policy delay", "Value": "2 / 2"},
{"Parameter": "target smoothing noise (clip)", "Value": "0.2 (0.5), prior-std units"},
{"Parameter": "critic_lr / actor_lr", "Value": "1e-3 / 0.02 (prior-std units)"},
{"Parameter": "batch size / updates per step", "Value": "64 / 1"},
{"Parameter": "steps per window / warm-up steps / buffer size", "Value": "10 / 50 / 5000"},
{"Parameter": "exploration noise / warm-up fraction", "Value": "0.1 / 0.5"},
{"Parameter": "critic hidden width", "Value": "64 (L63, L96 const) / 128 (vec, flux)"},
],
"notes": (
"**Same structural cost advantage as RL-DDPG-SHORT** — the continuing trajectory pays spin-up "
"once and the convergence test runs on overlapping windows, so scores are not directly "
"comparable to methods that re-pay spin-up every evaluation. **Not tuned per case.** "
"Converges on every seed on L63 and L96-const (slower than RL-DDPG-SHORT on L63 at targets "
"1.0–1.1, similar at 1.2); on the higher-dimensional forcings it is weaker than RL-DDPG-SHORT: "
"it never reaches the targets on L96 flux-force, and on vec-force only 1 of 30 seeds does "
"(target 1.2) within the budget."
),
},
"RL-TD3": {
"citation": "Fujimoto, van Hoof & Meger, ICML, 2018",
"url": "https://arxiv.org/abs/1802.09477",
"source": "`opt_experiments/ReinforcementLearning` — TD3 actor-critic (`ddpg_core.jl`, `rl_env.jl`, `rl_run.jl`), `ALGORITHM=td3`, `UPDATE_FREQUENCY=long` (leaderboard name `rl-td3-long`).",
"math": (
"Same TD3 actor-critic scheme as RL-TD3-SHORT (twin critics with a min-target; the actor "
"ascends critic 1 only; see that entry and RL-DDPG-SHORT for the formulation, including how "
"the prior sets the initial policy and the whitened coordinates). Here one RL step "
"is the whole statistics window — exactly one standard forward evaluation, the same statistical "
"window as every other method on the leaderboard — so there is one RL update per forward "
"evaluation and each step costs 1 unit."
),
"math_eq": r"\theta \leftarrow \theta + \mathrm{Adam}\!\left(\mathbb{E}_{x}\left[\nabla_F\, q^{(1)}_\vartheta(x,F)\right]_{F=F_\theta}\right)",
"guardrails": (
"Same TD3 heuristics as RL-TD3-SHORT: twin critics with a min-target, delayed policy/target "
"updates (every 2 critic updates), and clipped target-policy smoothing noise. Same `γ=0.5<1`, "
"Polyak rate, frozen standardization and 0.1 prior-std exploration noise; the spin-up is paid "
"on every step; per-cell budget is capped at 10000 units."
),
"params": [
{"Parameter": "γ (discount)", "Value": "0.5"},
{"Parameter": "Polyak rate", "Value": "0.05"},
{"Parameter": "critics / policy delay", "Value": "2 / 2"},
{"Parameter": "target smoothing noise (clip)", "Value": "0.2 (0.5), prior-std units"},
{"Parameter": "critic_lr / actor_lr", "Value": "1e-3 / 0.02 (prior-std units)"},
{"Parameter": "batch size / updates per step", "Value": "64 / 1"},
{"Parameter": "steps per window / warm-up steps / buffer size", "Value": "1 / 20 / 2000"},
{"Parameter": "exploration noise / warm-up fraction", "Value": "0.1 / 0.5"},
{"Parameter": "critic hidden width", "Value": "64 (L63, L96 const) / 128 (vec, flux)"},
],
"notes": (
"**One RL update per forward evaluation, so far more expensive than RL-TD3-SHORT.** **Not tuned "
"per case.** Converges on every seed on L63 and L96-const (slower than RL-DDPG "
"on L96-const, and on L63 faster at targets 1.0–1.1 but slower at 1.2), but never reached the "
"targets on L96 flux-force or vec-force within the budget."
),
},
"IES-IEKF": {
"citation": "Elsaesser et al., J. Adv. Model. Earth Syst., 2025",
"url": "https://doi.org/10.1029/2024MS004713",
"source": "`uq_experiments/BayesianOptimalExperimentalDesign` — `boed_core.jl` (`run_iekf`, `acquire_batch`), variant `:iekf` (leaderboard key `boed-iekf`).",
"math": (
"An iterated emulate-sample loop. Iteration 1 forward-evaluates an "
"`N_ens`-point Latin-hypercube design in the truncated, prior-whitened parameter space. Each later "
"iteration: (i) refits one GP per whitened output mode on all `(θ, G(θ))` pairs collected so far; "
"(ii) draws an approximate posterior by running an IEKF (`GaussNewtonInversion`) on the GP-mean "
"forward map — no true forward evaluations — starting from 1000 LHS prior draws; (iii) takes the next "
"`N_ens` forward-model evaluation points as a random subsample of that IEKF ensemble. The batch is "
"forward-evaluated and appended to the training set, so each iteration emulates the forward model, "
"samples the posterior from the emulator, and spends its forward runs where that posterior has mass."
),
"guardrails": (
"The posterior sampler is a fixed-step IEKF (`DefaultScheduler(0.1)`, 30 steps, total pseudo-time "
"T = 3), so it is a finite-time approximate posterior rather than a converged one; the observation "
"noise in whitened output space is the identity and the GP predictive variance is ignored. GP fits "
"keep kernel and noise hyperparameters inside bounds set relative to each mode's data scale (±2 "
"decades on length scales and signal std; noise std between 1e-3 and 1 of the signal std) to avoid "
"degenerate near-zero-noise fits, with `Fminbox` capped at 50 inner / 3 outer iterations. Inputs "
"and outputs are PCA-whitened and truncated against the prior and the observation covariance."
),
"params": [
{"Parameter": "IEKF ensemble size (n_posterior_samples)", "Value": "1000"},
{"Parameter": "IEKF step size / steps", "Value": "0.1 / 30 (T = 3)"},
{"Parameter": "batch size per iteration", "Value": "N_ens (random subsample of the IEKF ensemble)"},
{"Parameter": "gp_optim_iters / gp_outer_iters", "Value": "50 / 3"},
{"Parameter": "retain_var (output whitening)", "Value": "0.99"},
{"Parameter": "retain_var_input (input whitening)", "Value": "0.99 (L63, L96 const) / 0.9 (vec, flux)"},
],
"notes": (
"**Cheap per iteration:** no MCMC and no batch optimization, only GP-mean predictions "
"inside the IEKF. The batch is a plain draw from the posterior with no diversity criterion, so points are "
"not spread out deliberately. In the current UQ data (c = 1.5, best ensemble size) it needs a "
"mean budget of 5.0 on L63 and 5.4 on L96 const-force, but on the two high-dimensional L96 cases "
"(vec-force and NN flux-force) it fails on at least 35% and 40% of seeds, respectively, at every ensemble size."
),
},
"EKS": {
"citation": "Garbuno-Inigo, Nüsken and Reich, SIAM J. Appl. Dyn. Syst., 2020 (ALDI); Garbuno-Inigo, Hoffmann, Li and Stuart, SIAM J. Appl. Dyn. Syst., 2020 (original EKS)",
"url": "https://doi.org/10.1137/19M1304891",
"source": "`uq_experiments/KalmanSampler` — `calibrate_l63.jl`/`calibrate_l96.jl`, `experiment_config.jl` (`METHOD=sampler`, leaderboard key `sampler`); `EKP.Sampler(prior)` with EnsembleKalmanProcesses' default `sampler_type = \"aldi\"`.",
"math": (
"EKS (a.k.a ALDI): the Ensemble Kalman Sampler in its affine-invariant Langevin form (ALDI). It is a "
"derivative-free interacting-particle system that approximately samples the posterior. Each iteration "
"moves every member with an ensemble-Kalman drift (parameter/output ensemble covariances stand in for "
"a simulator Jacobian), a prior term applied implicitly, a finite-sample correction `(d+1)/J` times "
"the member's deviation from the ensemble mean, and noise built from the ensemble square root. Unlike "
"EKI the noise keeps the ensemble from collapsing, so the ensemble approximates the posterior rather "
"than a point estimate. There is no emulator: the ensemble at each iteration is itself the posterior "
"estimate."
),
"math_eq": r"\theta^{(j)}_{n+1} = \left(I + \Delta t_n C_n \Gamma_\theta^{-1}\right)^{-1}\left[\theta^{(j)}_n - \frac{\Delta t_n}{J}\sum_{k=1}^J \langle \mathcal{G}(\theta^{(k)}_n) - \bar{\mathcal{G}}_n,\, \Gamma_y^{-1}(\mathcal{G}(\theta^{(j)}_n) - y)\rangle (\theta^{(k)}_n - \bar\theta_n) + \Delta t_n C_n \Gamma_\theta^{-1} m_\theta + \Delta t_n \frac{d+1}{J}(\theta^{(j)}_n - \bar\theta_n)\right] + \sqrt{\frac{2\Delta t_n}{J}}\sum_{k=1}^J \xi_{jk}\,(\theta^{(k)}_n - \bar\theta_n)",
"guardrails": (
"The step size comes from `EKSStableScheduler()` (numerator 1, nugget `eps()`), so it is adaptive and "
"data-dependent, `Δt = 1/(‖D‖ + eps)` with `D` the ensemble-weighted data-misfit matrix, and needs "
"no hand-tuned schedule."
),
"params": [
{"Parameter": "sampler_type", "Value": "\"aldi\" (EKP default)"},
{"Parameter": "scheduler", "Value": "EKSStableScheduler() (numerator 1, nugget eps())"},
],
"notes": (
"**No emulator and no MCMC; each iteration costs `N_ens` forward runs.** In the current UQ data "
"(c = 1.5, best reliable ensemble size) it needs a mean budget of 4.2 on L63 (15% failures) and 4.2 "
"on L96 const-force, taking only about one iteration. On the two high-dimensional cases it needs "
"7 to 8 iterations, so the budget grows with ensemble size: 353 at size 50 on NN flux-force and "
"460 at size 55 on vec-force (5% failures each); the largest sizes fail on up to 25-30% of seeds."
),
},
"IES-EKS": {
"citation": "Elsaesser et al., J. Adv. Model. Earth Syst., 2025; Garbuno-Inigo, Hoffmann, Li and Stuart, SIAM J. Appl. Dyn. Syst., 2020 (EKS)",
"url": "https://doi.org/10.1029/2024MS004713",
"source": "`uq_experiments/BayesianOptimalExperimentalDesign` — `boed_core.jl` (`run_eks`, `acquire_batch`), variant `:eks` (leaderboard key `boed-eks`); `experiment_config.jl` sets `eks_sampler_type = \"eks\"`.",
"math": (
"The same iterated emulate-sample loop as IES-IEKF and IES-TMCMC (see those entries), with the "
"posterior sampler swapped for the Ensemble Kalman Sampler (EKP `Sampler`). Iteration 1 "
"forward-evaluates an `N_ens`-point Latin-hypercube design in the truncated, prior-whitened space. "
"Each later iteration: (i) refits one GP per whitened output mode on all `(θ, G(θ))` pairs so far; "
"(ii) draws an approximate posterior by running the sampler on the GP-mean forward map (no true "
"forward evaluations), from a 200-member Latin-hypercube standard-normal start with the prior "
"N(0, I) and identity noise; (iii) takes the next `N_ens` forward-model evaluation points as a random "
"subsample of that ensemble."
),
"guardrails": (
"The sampler is `EKP.Sampler` with `sampler_type = \"eks\"` (the original EKS of Garbuno-Inigo, "
"Hoffmann, Li and Stuart, not the ALDI variant used by the EKS and CES-EKS entries), driven by "
"`EKSStableScheduler(alpha = 0.25)` for a fixed 500 steps. `alpha = 0.25` was chosen because it "
"sat closer to the ST-MCMC reference than the default 1, and `alpha >= 3` is unstable. Like the IEKF "
"variant it ignores the GP predictive variance. GP fits use the same data-relative hyperparameter "
"bounds and iteration caps as IES-IEKF. The posterior has 200 draws rather than 1000; the "
"shortfall is padded with NaN rows that the leaderboard stage drops."
),
"params": [
{"Parameter": "sampler_type", "Value": "\"eks\" (original EKS)"},
{"Parameter": "EKS particles (eks_n_particles)", "Value": "200"},
{"Parameter": "scheduler", "Value": "EKSStableScheduler(alpha = 0.25)"},
{"Parameter": "steps (eks_max_iters / eks_T)", "Value": "500 / Inf (fixed step count)"},
{"Parameter": "batch size per iteration", "Value": "N_ens (random subsample of the EKS ensemble)"},
{"Parameter": "gp_optim_iters / gp_outer_iters", "Value": "50 / 3"},
],
"notes": (
"**Cheap in forward runs but with a fixed 500-step sampler cost per iteration** (GP-mean evaluations "
"only). In the current UQ data (c = 1.5, best ensemble size) its mean budget is 5.4 on L63, 4.2 on "
"L96 const-force and 112 on NN flux-force (size 80), all with 0% failures. On vec-force it fails on "
"at least 20% of seeds at every ensemble size, so it is not suitable there under the 20% threshold."
),
},
"GBOED-EKS": {
"citation": "Holthuijzen et al. 2026 (EIG acquisition); Garbuno-Inigo, Hoffmann, Li and Stuart, SIAM J. Appl. Dyn. Syst., 2020 (EKS posterior sampler)",
"url": "https://arxiv.org/abs/2508.13071",
"source": "`uq_experiments/BayesianOptimalExperimentalDesign` — `boed_core.jl` (`run_eks`, `optimize_batch`, `acquire_batch`), variant `:eks_eig` (leaderboard key `boed-eks-eig`); `experiment_config.jl` sets `eks_sampler_type = \"eks\"`.",
"math": (
"GBOED with the posterior sampler swapped for the Ensemble Kalman Sampler (see the GBOED-IEKF and "
"IES-EKS entries). Iteration 1 forward-evaluates an `N_ens`-point Latin-hypercube design in the "
"truncated prior-whitened space. Each later iteration: (i) refits one GP per whitened output mode on "
"all `(θ, G(θ))` pairs so far; (ii) draws an approximate posterior by running the sampler on the "
"GP-mean forward map (no true forward evaluations); (iii) chooses the next `N_ens` evaluation "
"points by jointly maximizing the closed-form Expected Information Gain (EIG) of the whole batch "
"against a 50-member random subsample of the EKS ensemble, starting from a random subsample of "
"the full ensemble."
),
"math_eq": r"\mathrm{EIG}(X) = \frac{1}{k}\sum_j \frac{1}{2}\log\frac{\det K_{pp}^{(j)}}{\det \Sigma'^{(j)}}, \quad \Sigma'^{(j)} = K_{pp}^{(j)} - K_{pc}^{(j)}\left(K_{cc}^{(j)}\right)^{-1}K_{cp}^{(j)}",
"guardrails": (
"The sampler is `EKP.Sampler` with `sampler_type = \"eks\"` (original EKS, not ALDI), run for a "
"fixed 500 steps of `EKSStableScheduler(alpha = 0.25)` with 200 particles, ignoring the GP "
"predictive variance. The batch is optimized with `Optim.jl`'s `LBFGS` inside the χ² ball of the "
"prior-whitened space (radial `tanh` squash, so every candidate has radius below √χ²_k(0.999)), with "
"a hard cap on raw EIG evaluations (`eig_call_limit`) and an outer-iteration cap "
"(`eig_outer_iters`), logging a warning that the returned batch may be \"under-optimized\" if either "
"is hit; a diagonal jitter (`eig_jitter`, relative to the GP signal variance) stabilizes the "
"covariance solves and log-determinants. GP fits use the same data-relative bounds and iteration "
"caps as IES-IEKF."
),
"params": [
{"Parameter": "sampler_type", "Value": "\"eks\" (original EKS)"},
{"Parameter": "EKS particles (eks_n_particles)", "Value": "200"},
{"Parameter": "scheduler", "Value": "EKSStableScheduler(alpha = 0.25)"},
{"Parameter": "steps (eks_max_iters / eks_T)", "Value": "500 / Inf (fixed step count)"},
{"Parameter": "n_eig_posterior_samples (EIG subsample X')", "Value": "50"},
{"Parameter": "eig_region / eig_ball_quantile", "Value": ":ball / 0.999 (χ² ball in prior-whitened space)"},
{"Parameter": "eig_optim_iters / eig_outer_iters", "Value": "500 / 20"},
{"Parameter": "eig_call_limit", "Value": "2000 EIG evaluations"},
{"Parameter": "eig_g_tol / eig_f_reltol", "Value": "1e-3 / 1e-6"},
{"Parameter": "eig_jitter", "Value": "1e-6 × GP signal variance"},
{"Parameter": "gp_optim_iters / gp_outer_iters", "Value": "50 / 3"},
{"Parameter": "batch_init_strategy", "Value": ":posterior_subsample"},
],
"notes": (
"**Adds the EIG batch optimization to IES-EKS**, so it pays for the inner EIG-maximization loop on "
"top of the 500-step sampler; hitting its call or outer-iteration limits gives \"under-optimized\" "
"batches. In the current UQ data (c = 1.5, best ensemble size) its mean budget is 5.2 on L63, 4.2 on "
"L96 const-force and 105 on NN flux-force (size 80, 5% failures). On vec-force it fails on at least "
"25% of seeds at every ensemble size, so it is not suitable there under the 20% failure threshold."
),
},
"GBOED-IEKF": {
"citation": "Holthuijzen et al. 2026 (EIG acquisition); IEKF posterior sampler replaces ST-MCMC",
"url": "https://arxiv.org/abs/2508.13071",
"source": "`uq_experiments/BayesianOptimalExperimentalDesign` — `boed_core.jl` (`run_iekf`, `optimize_batch`, `acquire_batch`), variant `:iekf_eig` (leaderboard key `boed-iekf-eig`).",
"math": (
"GBOED with the posterior sampler swapped for an IEKF. Iteration 1 forward-evaluates an `N_ens`-point "
"Latin-hypercube design in the truncated prior-whitened space. Each later iteration: (i) refits one GP "
"per whitened output mode on all `(θ, G(θ))` pairs so far; (ii) draws an approximate posterior by "
"running an IEKF (`GaussNewtonInversion`) on the GP-mean forward map — no true forward evaluations; "
"(iii) chooses the next `N_ens` evaluation points by jointly maximizing the closed-form Expected "
"Information Gain (EIG) of the whole batch against a 50-member random subsample X' of the IEKF "
"ensemble, starting from a random subsample of the full ensemble."
),
"math_eq": r"\mathrm{EIG}(X) = \frac{1}{k}\sum_j \frac{1}{2}\log\frac{\det K_{pp}^{(j)}}{\det \Sigma'^{(j)}}, \quad \Sigma'^{(j)} = K_{pp}^{(j)} - K_{pc}^{(j)}\left(K_{cc}^{(j)}\right)^{-1}K_{cp}^{(j)}",
"guardrails": (
"The IEKF posterior is a fixed-step, finite-time approximation (`DefaultScheduler(0.1)`, 30 steps, "
"T = 3) that ignores the GP predictive variance. The batch is optimized with `Optim.jl`'s "
"`LBFGS` inside the χ² ball of the prior-whitened space (radial `tanh` squash, so every candidate "
"has radius below √χ²_k(0.999)), with a hard cap on raw EIG evaluations (`eig_call_limit`) and an "
"outer-iteration cap (`eig_outer_iters`) enforced manually, logging a warning that the returned "
"batch may be \"under-optimized\" if either is hit; a diagonal jitter (`eig_jitter`, relative to the "
"GP signal variance) stabilizes the covariance solves and log-determinants. GP fits use the same "
"data-relative hyperparameter bounds and iteration caps as IES-IEKF."
),
"params": [
{"Parameter": "IEKF ensemble size (n_posterior_samples)", "Value": "1000"},
{"Parameter": "IEKF step size / steps", "Value": "0.1 / 30 (T = 3)"},
{"Parameter": "n_eig_posterior_samples (EIG subsample X')", "Value": "50"},
{"Parameter": "eig_region / eig_ball_quantile", "Value": ":ball / 0.999 (χ² ball in prior-whitened space)"},
{"Parameter": "eig_optim_iters / eig_outer_iters", "Value": "500 / 20"},
{"Parameter": "eig_call_limit", "Value": "2000 EIG evaluations"},
{"Parameter": "eig_g_tol / eig_f_reltol", "Value": "1e-3 / 1e-6"},
{"Parameter": "eig_jitter", "Value": "1e-6 × GP signal variance"},
{"Parameter": "gp_optim_iters / gp_outer_iters", "Value": "50 / 3"},
{"Parameter": "batch_init_strategy", "Value": ":posterior_subsample"},
{"Parameter": "retain_var / retain_var_input", "Value": "0.99 / 0.99 (L63, L96 const), 0.9 (vec, flux)"},
],
"notes": (
"**Adds the EIG batch optimization to IES-IEKF while keeping the cheap IEKF posterior**, so it avoids "
"ST-MCMC cost but still pays for the inner EIG-maximization loop, which costs ~n³ per output mode in "
"the subsample size and slows every iteration as the cumulative GP training set grows; hitting its "
"call or outer-iteration limits gives \"under-optimized\" batches. In the current UQ data (c = 1.5, "
"best ensemble size) it has the lowest mean budget of the four variants on L63 (4.8) and ties "
"GBOED-TMCMC on L96 const-force (4.4). On the two high-dimensional L96 cases it has low budgets "
"(55 on vec-force, 50 on NN flux-force) but fails on at least 35% and 40% of seeds, respectively, "
"at every ensemble size, so it is not suitable there under the 20% failure threshold."
),
},
"IES-TMCMC": {
"citation": "Elsaesser et al., J. Adv. Model. Earth Syst., 2025",
"url": "https://doi.org/10.1029/2024MS004713",
"source": "`uq_experiments/BayesianOptimalExperimentalDesign` — `boed_core.jl` (`run_tmcmc`, `boed_loglik`, `acquire_batch`), variant `:tmcmc` (leaderboard key `boed-tmcmc`).",
"math": (
"An iterated emulate-sample loop. Iteration 1 forward-evaluates an "
"`N_ens`-point Latin-hypercube design in the truncated, prior-whitened parameter space. Each later "
"iteration: (i) refits one GP per whitened output mode on all `(θ, G(θ))` pairs collected so far; "
"(ii) draws posterior samples from the GP-based posterior via Transitional/Sequential MCMC "
"(Ching & Chen, 2007), using the GP-marginal Gaussian likelihood below; (iii) takes the next `N_ens` "
"forward-model evaluation points as a random subsample of those posterior draws. The batch is "
"forward-evaluated and appended to the training set, so each iteration emulates the forward model, "
"samples the posterior from the emulator, and spends its forward runs where that posterior has mass."
),
"math_eq": r"\log p(y\mid z) = \sum_j \left[-\frac{(\mu_j(z)-y_j)^2}{2\,(1+\sigma_j^2(z))} - \frac{1}{2}\log\left(2\pi\,(1+\sigma_j^2(z))\right)\right]",
"guardrails": (
"The likelihood includes the GP predictive variance `σ_j²(z)` (the whitened observation noise is the "
"identity), so the posterior widens where the surrogate is uncertain, and keeps the normalizing "
"log(2π·v) term because ST-MCMC needs a properly normalized density. TMCMC runs with burn-in and "
"thinning below its own 20 / 3 defaults since they multiply per-stage cost. GP fits keep "
"hyperparameters inside data-relative bounds (±2 decades on length scales and signal std; noise std "
"between 1e-3 and 1 of the signal std) to avoid the near-zero-noise fits that ST-MCMC resampling "
"can collapse onto, with `Fminbox` capped at 50 inner / 3 outer iterations."
),
"params": [
{"Parameter": "n_posterior_samples (TMCMC population)", "Value": "1000"},
{"Parameter": "tmcmc_burnin / tmcmc_thin", "Value": "5 / 1 (below TMCMC's 20 / 3 defaults)"},
{"Parameter": "batch size per iteration", "Value": "N_ens (random subsample of the TMCMC draws)"},
{"Parameter": "gp_optim_iters / gp_outer_iters", "Value": "50 / 3"},
{"Parameter": "retain_var (output whitening)", "Value": "0.99"},
{"Parameter": "retain_var_input (input whitening)", "Value": "0.99 (L63, L96 const) / 0.9 (vec, flux)"},
],
"notes": (
"**The batch is a plain draw from the ST-MCMC posterior, with no batch optimization.** TMCMC cost grows roughly linearly with "
"the population size, and a small population gives a degenerate posterior in the high-dimensional "
"L96 cases. In the current UQ data (c = 1.5, best ensemble size) its mean budget is 6.4 on L63 and 5.6 on L96 "
"const-force, but on L96 vec-force it is 240 and it fails on at least 85% of seeds at every ensemble size."
),
},
}
# KNOWN_METHODS is the source of truth for the abbreviation/full-name list,
# so this page renders even without any NetCDF data loaded.
abbrs = sorted(meta["abbreviation"] for meta in KNOWN_METHODS.values())
full_name_by_abbr = {meta["abbreviation"]: meta["Method"] for meta in KNOWN_METHODS.values()}
# Selection UI (defaults to query param if valid)
abbr = st.query_params.get("method")
default_idx = abbrs.index(abbr) if isinstance(abbr, str) and abbr in abbrs else 0
sel = str(st.selectbox("Choose a method", options=abbrs, index=default_idx))
# Persist selection to URL
st.query_params["method"] = sel
# Display details
meta = method_meta.get(sel, {})
st.subheader(f"{sel} — {full_name_by_abbr.get(sel, '')}")
if meta.get("source"):
st.caption(meta["source"])
st.markdown("**Update Equation**")
st.markdown(meta.get("math", "Mathematical form pending"))
if meta.get("math_eq"):
st.latex(meta["math_eq"])
st.markdown("**Robustness, Speed & Guardrails**")
st.markdown(meta.get("guardrails", "Guardrail details pending"))
st.markdown("**Configuration Parameters**")
params = meta.get("params")
if params:
st.dataframe(pd.DataFrame(params), hide_index=True, use_container_width=True)
else:
st.write("Parameter details pending")
st.markdown("**Convergence & Speed Notes**")
st.markdown(meta.get("notes", "No notes recorded yet"))
col1, col2 = st.columns(2)
with col1:
with st.container(border=True):
st.markdown("**Citation**")
st.write(meta.get("citation", "Citation pending"))
with col2:
with st.container(border=True):
st.markdown("**Link**")
st.link_button("Open reference", meta.get("url", "https://example.com"))
st.page_link("pages/OptimizationLeaderboard.py", label="⬅ Back to Optimization Leaderboard", icon="↩️")