.claude/skills/leaderboard-manager/references/populate-metadata.md CHANGED
@@ -57,7 +57,7 @@ method_meta = {
57
  "TEKI": {
58
  "citation": "Chada et al., SIAM/ASA J. UQ, 2020",
59
  "url": "https://doi.org/10.1137/17M114402X",
60
- "summary": "EKI variant with Tikhonov regularization for stability.",
61
  },
62
  ...
63
  }
@@ -69,10 +69,20 @@ method_meta = {
69
  The three fields:
70
  - `citation` — free-form string; typically "Author et al., Journal, Year" or a short title.
71
  - `url` — a full `https://...` URL. Displayed as an "Open reference" button.
72
- - `summary` — one or two sentences. Keep it short enough to fit in a narrow column.
 
 
 
73
 
74
  If you don't have a value yet, use `"(citation pending)"` / `"https://example.com"` /
75
- `"(summary pending)"` as placeholders so the app renders gracefully.
 
 
 
 
 
 
 
76
 
77
  ---
78
 
 
57
  "TEKI": {
58
  "citation": "Chada et al., SIAM/ASA J. UQ, 2020",
59
  "url": "https://doi.org/10.1137/17M114402X",
60
+ "config": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `Inversion(prior)` process. Fixed-step `DefaultScheduler()`, `NoLocalization()`, up to `N_iter=20` iterations.",
61
  },
62
  ...
63
  }
 
69
  The three fields:
70
  - `citation` — free-form string; typically "Author et al., Journal, Year" or a short title.
71
  - `url` — a full `https://...` URL. Displayed as an "Open reference" button.
72
+ - `config` — a short **technical configuration** summary (not a conceptual description):
73
+ the solver/process type, scheduler, key hyperparameters, and the source file path in
74
+ `CalibrationLeaderboard.jl` (`opt_experiments/...` or `uq_experiments/...`). This is what
75
+ renders in the "Technical Configuration" box at the top of the Methods page.
76
 
77
  If you don't have a value yet, use `"(citation pending)"` / `"https://example.com"` /
78
+ `"(configuration details pending)"` as placeholders so the app renders gracefully.
79
+
80
+ Note: the Methods page (`src/pages/MethodDetails.py`) no longer reads `metric_store` or
81
+ renders any benchmark tables/charts — it is a pure documentation page driven by
82
+ `KNOWN_METHODS` (for the abbreviation/full-name list) and `method_meta` (for the
83
+ citation/link/config box). Locating the config details for a method means reading its
84
+ script(s) under `opt_experiments/` or `uq_experiments/` in
85
+ `~/Dropbox/Caltech/CESjl/CalibrationLeaderboard.jl`, not the NetCDF data in this repo.
86
 
87
  ---
88
 
CLAUDE.md CHANGED
@@ -57,7 +57,12 @@ calibration_benchmark/
57
  │ │ └── method_registry.py KNOWN_METHODS registry + name canonicalization
58
  │ └── pages/
59
  │ ├── UQLeaderboard.py 🎯 UQ Leaderboard — calls render_leaderboard(load_uq_store())
60
- │ ├── MethodDetails.py 📘 Methods — per-method charts + metadata
 
 
 
 
 
61
  │ └── RawData.py 🧾 Get Data — raw table + CSV export
62
  ├── data/ NetCDF result files (git-LFS — run `git lfs pull`)
63
  │ ├── *.nc Kalman results (TEKI, ETKI, IEKF)
@@ -231,6 +236,22 @@ the `budget_store` parameter of `render_leaderboard()` in `src/common/leaderboar
231
  (`streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`).
232
  - **Method metadata is split** — `method_registry.py` (scoring) and `pages/MethodDetails.py`
233
  `method_meta` dict (display copy) must both be updated when adding a new method.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  - **`build_leaderboard()` in `data_store.py` is dead code** — both leaderboards are rendered
235
  via `render_leaderboard()` in `src/common/leaderboard.py`.
236
  - **No CI / no formal test suite** — `test_parse.py` and `test_failures.py` are manual scripts.
 
57
  │ │ └── method_registry.py KNOWN_METHODS registry + name canonicalization
58
  │ └── pages/
59
  │ ├── UQLeaderboard.py 🎯 UQ Leaderboard — calls render_leaderboard(load_uq_store())
60
+ │ ├── MethodDetails.py 📘 Method configurations — per-method (selection-driven) technical
61
+ │ │ detail: update equation, guardrails, a small
62
+ │ │ parameter table, and convergence/speed notes,
63
+ │ │ plus citation/link, sourced from KNOWN_METHODS +
64
+ │ │ method_meta (no metric_store/NetCDF dependency,
65
+ │ │ no data-driven tables/charts)
66
  │ └── RawData.py 🧾 Get Data — raw table + CSV export
67
  ├── data/ NetCDF result files (git-LFS — run `git lfs pull`)
68
  │ ├── *.nc Kalman results (TEKI, ETKI, IEKF)
 
236
  (`streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`).
237
  - **Method metadata is split** — `method_registry.py` (scoring) and `pages/MethodDetails.py`
238
  `method_meta` dict (display copy) must both be updated when adding a new method.
239
+ - **`method_meta` fields (per abbreviation)**: `citation`, `url`, `source` (short file-path
240
+ reference into `CalibrationLeaderboard.jl`), `math` (prose update description) + optional
241
+ `math_eq` (a single-line raw LaTeX string rendered via `st.latex()`), `guardrails` (prose:
242
+ regularization/localization/adaptive-scheduling/early-stopping machinery), `params` (a
243
+ list of `{"Parameter", "Value"}` dicts — deliberately **excludes ensemble size and max
244
+ iterations**, which are covered by the leaderboard charts), and `notes` (prose:
245
+ convergence/speed behavior, including known empirical issues). All of it is display copy
246
+ only; the Method configurations page renders `KNOWN_METHODS` (for
247
+ the abbreviation/full-name list) + `method_meta` (for everything else), scoped to
248
+ whichever method is currently selected.
249
+ - **`method_meta` content is manually transcribed, not derived** — the math/guardrails/
250
+ params/notes text was written by reading `CalibrationLeaderboard.jl`'s
251
+ `experiment_config.jl`/`run_*.jl`/`*_core.jl` files (and, for the Kalman-family methods,
252
+ `EnsembleKalmanProcesses.jl`'s own update-equation source) directly. There is no
253
+ automated sync — update it by hand when a method's experiment settings or algorithm
254
+ change upstream.
255
  - **`build_leaderboard()` in `data_store.py` is dead code** — both leaderboards are rendered
256
  via `render_leaderboard()` in `src/common/leaderboard.py`.
257
  - **No CI / no formal test suite** — `test_parse.py` and `test_failures.py` are manual scripts.
src/pages/MethodDetails.py CHANGED
@@ -1,136 +1,434 @@
1
  from pathlib import Path
2
  import sys
3
 
4
- import altair as alt
5
  import streamlit as st
6
 
7
  try:
8
- from data_store import load_metric_store
9
  except ModuleNotFoundError:
10
  sys.path.append(str(Path(__file__).resolve().parents[1]))
11
- from data_store import load_metric_store
12
 
13
- st.set_page_config(page_title="Method Details", page_icon="📘", layout="wide")
14
 
15
  # Sidebar navigation
16
  st.sidebar.title("Navigation")
17
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
18
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
19
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
20
- st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="📘")
21
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
22
 
23
- st.title("Method Details")
24
- st.caption("Method-level view from NetCDF forward-model-run metrics averaged over random seeds")
 
 
 
 
 
 
 
 
25
  st.markdown(
26
  "For the exact implementation of each algorithm, see "
27
- "[CalibrationLeaderboard.jl](https://github.com/odunbar/CalibrationLeaderboard.jl)."
 
28
  )
29
 
30
- # Read selected method from query params
31
- abbr = st.query_params.get("method")
32
- if not isinstance(abbr, str):
33
- abbr = None
34
-
35
- metric_store = load_metric_store()
36
- if metric_store.empty:
37
- st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
38
- st.stop()
39
-
40
- methods_df = metric_store[["Method", "abbreviation"]].drop_duplicates().sort_values("abbreviation")
41
-
42
- abbrs = methods_df["abbreviation"].tolist()
43
-
44
- # Metadata dictionary (can be moved to a JSON/YAML later)
45
  method_meta = {
46
  "TEKI": {
47
  "citation": "Chada et al., SIAM/ASA J. UQ, 2020",
48
  "url": "https://doi.org/10.1137/17M114402X",
49
- "summary": "EKI variant with Tikhonov regularization that adds a penalty term to prevent ensemble collapse and improve stability on nonlinear problems.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  },
51
  "ETKI": {
52
  "citation": "Schillings & Stuart, Numer. Math., 2017",
53
  "url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
54
- "summary": "Ensemble Transform Kalman Inversion — applies an ensemble-space transform update that preserves the ensemble mean while reducing variance inflation.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  },
56
  "IEKF": {
57
  "citation": "Iglesias, Inverse Problems, 2016",
58
  "url": "https://doi.org/10.1088/0266-5611/32/2/025002",
59
- "summary": "Regularizing iterative ensemble Kalman method; repeatedly refines the ensemble around a regularized Gauss-Newton step for nonlinear inverse problems.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  },
61
  "UKI": {
62
  "citation": "Huang, Huang & Stuart, Physica D, 2022",
63
  "url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
64
- "summary": "Unscented Kalman Inversion — propagates a deterministic set of sigma points through the forward model to estimate mean and covariance without linearization.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  },
66
  "ABC": {
67
- "citation": "Approximate Bayesian Calibration",
68
- "url": "https://example.com/abc",
69
- "summary": "Sample without exact likelihoods until error falls below a target convergence.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  },
71
  "HM": {
72
  "citation": "Williamson et al. 2013; King et al. 2025",
73
  "url": "https://example.com/hm",
74
- "summary": "Iterative constraint of parameter space using wave reductions.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  },
76
  "CES-EKI-DMC": {
77
  "citation": "Cleary et al., J. Comput. Phys., 2021",
78
  "url": "https://doi.org/10.1016/j.jcp.2020.109716",
79
- "summary": "Calibrate-Emulate-Sample: uses EKI with a DataMisfitController to select training points, builds a GP emulator of the forward model, then samples the posterior via MCMC.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  },
81
  "CES-EKI-CONST": {
82
  "citation": "Cleary et al., J. Comput. Phys., 2021",
83
  "url": "https://doi.org/10.1016/j.jcp.2020.109716",
84
- "summary": "Calibrate-Emulate-Sample: uses EKI with a constant (fixed) timestep scheduler to select training points, builds a GP emulator of the forward model, then samples the posterior via MCMC.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  },
86
  "CES-IEKF-CONST": {
87
  "citation": "Cleary et al., J. Comput. Phys., 2021; Iglesias, Inverse Problems, 2016",
88
  "url": "https://doi.org/10.1016/j.jcp.2020.109716",
89
- "summary": "Calibrate-Emulate-Sample: uses IEKF with a constant (fixed) timestep scheduler to select training points, builds a GP emulator of the forward model, then samples the posterior via MCMC.",
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  },
91
  "ADAM": {
92
  "citation": "Kingma & Ba, ICLR, 2015",
93
  "url": "https://doi.org/10.48550/arXiv.1412.6980",
94
- "summary": "Adaptive Moment Estimation — gradient-based optimizer that adapts per-parameter learning rates using first and second moment estimates of the gradient.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  },
96
  "LM": {
97
  "citation": "Levenberg, 1944; Marquardt, 1963; Fletcher, 1971",
98
  "url": "https://doi.org/10.1090/qam/10666",
99
- "summary": "Levenberg-Marquardt — damped least-squares algorithm that interpolates between gradient descent and Gauss-Newton steps for efficient nonlinear least-squares minimization.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  },
101
  "GBOED": {
102
  "citation": "Holthuijzen et al. 2026",
103
  "url": "https://arxiv.org/abs/2508.13071",
104
- "summary": "Goal-oriented Bayesian Optimal Experimental Design — selects each batch of forward-model evaluations by maximizing an expected-information-gain criterion targeted at the quantity of interest, refitting the surrogate used by the criterion at every iteration.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  },
106
  "LM-KGMM": {
107
  "citation": "Giorgini, Bischoff & Souza, arXiv:2503.18054, 2025",
108
  "url": "https://arxiv.org/abs/2503.18054",
109
- "summary": "Score-based Levenberg-Marquardt using a K-means Gaussian Mixture Model to estimate the score function, which drives the gradient via the generalized fluctuation-dissipation theorem (GFDT).",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  },
111
  "LM-DSM": {
112
  "citation": "Giorgini, Bischoff & Souza, arXiv:2509.19660, 2025",
113
  "url": "https://arxiv.org/abs/2509.19660",
114
- "summary": "Score-based Levenberg-Marquardt using Denoising Score Matching to estimate the score function, which drives the gradient via the generalized fluctuation-dissipation theorem (GFDT).",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  },
116
  }
117
 
 
 
 
 
 
118
  # Selection UI (defaults to query param if valid)
119
- default_idx = 0
120
- if isinstance(abbr, str) and abbr in abbrs:
121
- default_idx = abbrs.index(abbr)
122
  sel = str(st.selectbox("Choose a method", options=abbrs, index=default_idx))
123
 
124
  # Persist selection to URL
125
  st.query_params["method"] = sel
126
 
127
  # Display details
128
- row = methods_df.loc[methods_df["abbreviation"] == sel].iloc[0]
129
  meta = method_meta.get(sel, {})
130
 
131
- st.subheader(f"{sel} — {row['Method']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- col1, col2, col3 = st.columns(3)
 
 
 
134
  with col1:
135
  with st.container(border=True):
136
  st.markdown("**Citation**")
@@ -139,96 +437,5 @@ with col2:
139
  with st.container(border=True):
140
  st.markdown("**Link**")
141
  st.link_button("Open reference", meta.get("url", "https://example.com"))
142
- with col3:
143
- with st.container(border=True):
144
- st.markdown("**Summary**")
145
- st.write(meta.get("summary", "Summary pending"))
146
-
147
- st.markdown("**Benchmark Slice**")
148
- slice_df = metric_store[metric_store["abbreviation"] == sel].copy()
149
- slice_df = slice_df.sort_values(["benchmark", "rmse_target", "ensemble_size"])
150
-
151
- target_options = sorted(slice_df["rmse_target"].astype(str).unique().tolist())
152
- selected_target = st.radio("RMSE Target Level", options=target_options, horizontal=True)
153
-
154
- best_table_view = slice_df[slice_df["rmse_target"].astype(str) == selected_target]
155
-
156
- best_idx = best_table_view.groupby("benchmark")["metric"].idxmin()
157
- best_ensemble_df = best_table_view.loc[best_idx, ["benchmark", "rmse_target", "ensemble_size", "metric", "failure_rate"]].rename(
158
- columns={"metric": "Mean Forward Model Runs", "failure_rate": "Failure Rate (%)", "ensemble_size": "Optimal Ensemble Size", "rmse_target": "RMSE Target"}
159
- )
160
- st.dataframe(best_ensemble_df, hide_index=True, use_container_width=True)
161
-
162
- if sel == "HM":
163
- st.markdown("### Failure Analysis")
164
- st.write(f"Failed calibration rate for History Matching at RMSE Target {selected_target} (as % of random seeds).")
165
-
166
- chart = (
167
- alt.Chart(best_table_view)
168
- .mark_bar()
169
- .encode(
170
- x=alt.X("ensemble_size:O", title="Ensemble Size"),
171
- y=alt.Y("mean(failure_rate):Q", title="Failure Rate (%)", scale=alt.Scale(domain=[0, 100])),
172
- color="benchmark:N",
173
- tooltip=["benchmark", "ensemble_size", alt.Tooltip("mean(failure_rate):Q", format=".1f", title="Failure Rate (%)")]
174
- )
175
- .properties(height=350)
176
- .interactive()
177
- )
178
- st.altair_chart(chart, use_container_width=True)
179
- best_by_benchmark = (
180
- best_table_view.loc[best_idx, ["benchmark", "metric", "ensemble_size"]]
181
- .rename(
182
- columns={
183
- "metric": "Best Mean Forward Model Runs",
184
- "ensemble_size": "Optimal Ensemble Size",
185
- }
186
- )
187
- .sort_values("benchmark")
188
- )
189
-
190
- st.markdown("**Best by Benchmark**")
191
- st.dataframe(
192
- best_by_benchmark,
193
- hide_index=True,
194
- use_container_width=True,
195
- column_config={
196
- "benchmark": st.column_config.TextColumn("Benchmark"),
197
- "Best Mean Forward Model Runs": st.column_config.NumberColumn("Best Mean Forward Model Runs", format="%.4f"),
198
- "Optimal Ensemble Size": st.column_config.NumberColumn("Optimal Ensemble Size"),
199
- },
200
- )
201
-
202
- st.markdown("**Scaling by Benchmark**")
203
- chart_view = slice_df[slice_df["rmse_target"].astype(str) == selected_target]
204
- chart_df = (
205
- chart_view.groupby(["benchmark", "ensemble_size"], as_index=False)
206
- .agg(mean_forward_runs=("metric", "mean"))
207
- )
208
- chart = (
209
- alt.Chart(chart_df)
210
- .mark_line(point=True)
211
- .encode(
212
- x=alt.X("ensemble_size:Q", title="Ensemble Size"),
213
- y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
214
- color=alt.Color("benchmark:N", title="Benchmark"),
215
- tooltip=["benchmark", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
216
- )
217
- )
218
- st.altair_chart(chart, use_container_width=True)
219
-
220
- st.markdown("**All Averaged Configurations for Method**")
221
- st.dataframe(
222
- slice_df[["benchmark", "algorithm_alias", "rmse_target", "ensemble_size", "metric"]],
223
- hide_index=True,
224
- use_container_width=True,
225
- column_config={
226
- "benchmark": st.column_config.TextColumn("Benchmark"),
227
- "algorithm_alias": st.column_config.TextColumn("Source Alias"),
228
- "rmse_target": st.column_config.TextColumn("RMSE Target"),
229
- "ensemble_size": st.column_config.NumberColumn("Ensemble Size"),
230
- "metric": st.column_config.NumberColumn("Mean Forward Model Runs", format="%.4f"),
231
- },
232
- )
233
 
234
  st.page_link("pages/OptimizationLeaderboard.py", label="⬅ Back to Optimization Leaderboard", icon="↩️")
 
1
  from pathlib import Path
2
  import sys
3
 
4
+ import pandas as pd
5
  import streamlit as st
6
 
7
  try:
8
+ from common.method_registry import KNOWN_METHODS
9
  except ModuleNotFoundError:
10
  sys.path.append(str(Path(__file__).resolve().parents[1]))
11
+ from common.method_registry import KNOWN_METHODS
12
 
13
+ st.set_page_config(page_title="Method Configurations", page_icon="📘", layout="wide")
14
 
15
  # Sidebar navigation
16
  st.sidebar.title("Navigation")
17
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
18
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
19
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
20
+ st.sidebar.page_link("pages/MethodDetails.py", label="Method configurations", icon="📘")
21
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
22
 
23
+ st.title("Method Configurations")
24
+ st.caption("Technical configuration of each calibration / UQ method, as implemented for this benchmark")
25
+ st.info(
26
+ "We are always seeking new algorithm families to be applied to this benchmark series. "
27
+ "If your favorite method is missing, or incorrectly configured, or if any algorithm is "
28
+ "lacking appropriate citation - please create an issue on `CalibrationLeaderboard.jl`, "
29
+ "and we can investigate this! Just note - we are interested in general behavior of "
30
+ "algorithm families, not hyper-optimizing any individual configuration to an individual "
31
+ "experiment."
32
+ )
33
  st.markdown(
34
  "For the exact implementation of each algorithm, see "
35
+ "[CalibrationLeaderboard.jl](https://github.com/odunbar/CalibrationLeaderboard.jl) "
36
+ "(`opt_experiments/` and `uq_experiments/`)."
37
  )
38
 
39
+ # Per-method metadata (can be moved to a JSON/YAML later). Every entry below was read
40
+ # directly out of CalibrationLeaderboard.jl (opt_experiments/, uq_experiments/) and, for the
41
+ # EKP-based processes, out of EnsembleKalmanProcesses.jl itself. `params` intentionally
42
+ # excludes ensemble size and max-iteration count (covered by the leaderboard charts) and
43
+ # instead lists the update-specific hyperparameters.
 
 
 
 
 
 
 
 
 
 
44
  method_meta = {
45
  "TEKI": {
46
  "citation": "Chada et al., SIAM/ASA J. UQ, 2020",
47
  "url": "https://doi.org/10.1137/17M114402X",
48
+ "source": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `Inversion(prior)`.",
49
+ "math": (
50
+ "Ensemble Kalman Inversion (Schillings & Stuart, 2017, eqns. 4-5): each iteration moves every "
51
+ "ensemble member by the same linear, empirical-covariance-based correction toward the data,"
52
+ ),
53
+ "math_eq": r"u_{n+1} = u_n + C^{ug}\left(C^{gg}+\Gamma\right)^{-1}\left(y - g(u_n)\right)",
54
+ "guardrails": (
55
+ "Tikhonov-style regularization is obtained by extending the state: the forward map is "
56
+ "augmented as `g ↦ [g; u]` and the data as `y ↦ [y; prior_mean]` with observation covariance "
57
+ "`blockdiag(Γ, prior_cov)`, so every step also pulls the ensemble back toward the prior "
58
+ "(`impose_prior=true`). A small `default_multiplicative_inflation` (1e-3) regularizes the "
59
+ "empirical covariance before it is inverted; `NoLocalization()` applies no covariance tapering; "
60
+ "`DefaultScheduler()` takes a fixed unit pseudo-time step each iteration."
61
+ ),
62
+ "params": [
63
+ {"Parameter": "impose_prior", "Value": "true (Tikhonov-style state extension)"},
64
+ {"Parameter": "default_multiplicative_inflation", "Value": "1e-3"},
65
+ {"Parameter": "localization", "Value": "NoLocalization()"},
66
+ {"Parameter": "scheduler", "Value": "DefaultScheduler() — fixed unit step"},
67
+ ],
68
+ "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.",
69
  },
70
  "ETKI": {
71
  "citation": "Schillings & Stuart, Numer. Math., 2017",
72
  "url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
73
+ "source": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `TransformInversion(prior)`.",
74
+ "math": (
75
+ "Ensemble Transform Kalman Inversion applies the update in ensemble-anomaly space instead of "
76
+ "perturbing observations: normalized anomalies `X=(u-ū)/√(m-1)` and `Y=(g_ext-ḡ_ext)/√(m-1)` are "
77
+ "formed (with the same prior extension as TEKI), and a transform matrix built from `Y'Γ⁻¹Y` maps "
78
+ "the prior ensemble anomalies onto posterior anomalies, `u ← ū + X·T`."
79
+ ),
80
+ "guardrails": (
81
+ "Same Tikhonov-style prior extension and `impose_prior=true` as TEKI; diagonal regularization "
82
+ "stabilizes the anomaly Gram matrix before it is inverted; `NoLocalization()`; the transform "
83
+ "update removes the perturbed-observation sampling noise present in stochastic EKI, which "
84
+ "generally lowers ensemble-collapse risk at small ensemble sizes."
85
+ ),
86
+ "params": [
87
+ {"Parameter": "impose_prior", "Value": "true (Tikhonov-style state extension)"},
88
+ {"Parameter": "localization", "Value": "NoLocalization()"},
89
+ {"Parameter": "scheduler", "Value": "DefaultScheduler() — fixed unit step"},
90
+ ],
91
+ "notes": "Same run-until-RMSE-target harness as TEKI; the transform (square-root) update trades stochastic sampling noise for a deterministic ensemble-anomaly map.",
92
  },
93
  "IEKF": {
94
  "citation": "Iglesias, Inverse Problems, 2016",
95
  "url": "https://doi.org/10.1088/0266-5611/32/2/025002",
96
+ "source": "`uq_experiments/GaussNewtonKalmanInversion` — EnsembleKalmanProcesses.jl `GaussNewtonInversion(prior)` (alias GNKI); Chada, Chen & Sanz-Alonso, 2021, Algorithm 4.1.",
97
+ "math": (
98
+ "Iterated Ensemble Kalman Filter with statistical linearization: each pseudo-time step `Δt` "
99
+ "relaxes the ensemble between its current position and a linearized Gauss-Newton solve of the "
100
+ "joint parameter/output covariance, evaluated against a fresh perturbed draw around the prior "
101
+ "mean rather than the ensemble's own mean — a Gauss-Newton normal-equation solve carried out "
102
+ "entirely in empirical-covariance space, without ever forming the model Jacobian explicitly."
103
+ ),
104
+ "guardrails": (
105
+ "`perturbation_scale = 2/Δt - 1` rescales the prior-draw noise so the relaxation step's "
106
+ "stationary covariance equals the true posterior for every `Δt ∈ (0,1]`, not just `Δt=1`; "
107
+ "diagonal regularization + `NoLocalization()` stabilize the joint covariance solve; the pseudo-"
108
+ "step is fixed rather than adaptive (`DefaultScheduler(0.1)`, i.e. `Δt=0.1`)."
109
+ ),
110
+ "params": [
111
+ {"Parameter": "pseudo-step Δt", "Value": "0.1 (DefaultScheduler(0.1))"},
112
+ {"Parameter": "perturbation_scale", "Value": "2/Δt - 1 = 19"},
113
+ {"Parameter": "localization", "Value": "NoLocalization()"},
114
+ ],
115
+ "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.",
116
  },
117
  "UKI": {
118
  "citation": "Huang, Huang & Stuart, Physica D, 2022",
119
  "url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
120
+ "source": "`opt_experiments/ensemble_kalman_processes` — EnsembleKalmanProcesses.jl `Unscented(prior; impose_prior=true)`.",
121
+ "math": (
122
+ "Unscented Kalman Inversion propagates `2·N_par + 1` deterministic sigma points (the symmetric "
123
+ "unscented transform) through the forward model instead of a random ensemble, forms their "
124
+ "weighted mean/covariance, and performs the same Kalman-style analysis step as TEKI on this "
125
+ "reduced sigma-point set."
126
+ ),
127
+ "guardrails": (
128
+ "`α_reg ∈ (0,1]` shrinks the analysis mean toward the prior mean each step, guarding against "
129
+ "covariance collapse; `impose_prior=true` adds the same TEKI-style `y_ext`/`Γ_ext` extension; "
130
+ "the symmetric sigma-point scheme (`2·N_par+1` points) is used by default rather than the "
131
+ "cheaper simplex scheme."
132
+ ),
133
+ "params": [
134
+ {"Parameter": "α_reg", "Value": "1.0 (regularization toward prior mean disabled by default)"},
135
+ {"Parameter": "sigma_points", "Value": '"symmetric" (2·N_par+1 points)'},
136
+ {"Parameter": "impose_prior", "Value": "true"},
137
+ ],
138
+ "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.",
139
  },
140
  "ABC": {
141
+ "citation": "Csilléry et al., Trends in Ecology & Evolution, 2010",
142
+ "url": "https://doi.org/10.1016/j.tree.2010.04.001",
143
+ "source": "`uq_experiments/approximate_bayesian_computation` — simulation-based rejection sampling (no EKP, no surrogate).",
144
+ "math": (
145
+ "Each round draws `N_ens` i.i.d. prior candidates `θ ~ π(θ)`, evaluates the true forward model "
146
+ "`G(θ)`, and accepts a candidate iff its whitened misfit falls inside a chi-squared acceptance "
147
+ "region; accepted candidates across all rounds approximate draws from the ABC posterior."
148
+ ),
149
+ "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}})",
150
+ "guardrails": (
151
+ "The acceptance threshold is set exactly to the `(1-alpha_reject)` quantile of `χ²(n_y)` rather "
152
+ "than hand-tuned; the final pushforward resamples `n_pushforward_samples=1000` draws from a "
153
+ "Gaussian fit to the accepted pool, smoothing a possibly sparse accepted set into a usable "
154
+ "posterior approximation; batch sizes are deliberately matched to IEKF's `N_ens_sizes`/`max_iter` "
155
+ "so the N_ens×k budget axis is directly comparable across methods on the leaderboard."
156
+ ),
157
+ "params": [
158
+ {"Parameter": "alpha_reject", "Value": "0.05"},
159
+ {"Parameter": "n_pushforward_samples", "Value": "1000"},
160
+ ],
161
+ "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.",
162
  },
163
  "HM": {
164
  "citation": "Williamson et al. 2013; King et al. 2025",
165
  "url": "https://example.com/hm",
166
+ "source": "`uq_experiments/HistoryMatching` — custom (non-EKP) wave/rejection scheme.",
167
+ "math": (
168
+ "History Matching runs a series of waves. At wave k, an independent GP emulator is fit per "
169
+ "whitened output mode on every parameter evaluated so far; a candidate survives the wave iff "
170
+ "its implausibility (whitened, GP-predicted misfit) is below a chi-squared threshold, and the "
171
+ "surviving (\"Not Ruled Out Yet\", NROY) region is intersected across all waves run so far."
172
+ ),
173
+ "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)",
174
+ "guardrails": (
175
+ "GP emulators use an automatic-relevance-determination squared-exponential kernel (`SEArd`), "
176
+ "refit every wave via `GaussianProcesses.optimize!`; NROY testing exploits monotonicity — a "
177
+ "candidate ruled out by an earlier wave is never re-tested by later waves — cutting GP-predict "
178
+ "cost from O(waves×batch) to O(surviving candidates); a Poisson/Garwood upper-confidence-bound "
179
+ "rule (`poisson_rate_upper_bound`, `early_stop_alpha=1e-6`) aborts a rejection-sampling sweep "
180
+ "early once even its best-case acceptance rate can no longer plausibly reach the wanted sample "
181
+ "count within the remaining budget, rather than burning the full `max_samples` budget on a "
182
+ "doomed sweep; inputs/outputs are PCA-whitened (`retain_var_input`/`retain_var`) before fitting."
183
+ ),
184
+ "params": [
185
+ {"Parameter": "confidence (implausibility threshold)", "Value": "0.95 → quantile(Chisq(k_R), 0.95)"},
186
+ {"Parameter": "n_candidate_batch", "Value": "2000 per wave"},
187
+ {"Parameter": "retain_var (output PCA)", "Value": "0.99"},
188
+ {"Parameter": "retain_var_input (input PCA)", "Value": "0.9"},
189
+ {"Parameter": "early_stop_alpha", "Value": "1e-6"},
190
+ ],
191
+ "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.",
192
  },
193
  "CES-EKI-DMC": {
194
  "citation": "Cleary et al., J. Comput. Phys., 2021",
195
  "url": "https://doi.org/10.1016/j.jcp.2020.109716",
196
+ "source": "`uq_experiments/calibrate_emulate_sample` — Calibrate-Emulate-Sample, calibrate stage: `Inversion()` + `DataMisfitController`.",
197
+ "math": (
198
+ "Three-stage Calibrate-Emulate-Sample. Calibrate: run EKI to generate an ensemble of "
199
+ "`(θ, G(θ))` training pairs. Emulate: fit a random-feature surrogate `Ĝ(θ)` on those pairs. "
200
+ "Sample: draw from `π(θ|y) ∝ exp(-½‖y-Ĝ(θ)‖²_Γ)·π(θ)` against the cheap surrogate using "
201
+ "preconditioned Crank-Nicolson Metropolis-Hastings (pCN-MH), rather than the true forward model."
202
+ ),
203
+ "guardrails": (
204
+ "Calibrate stage uses an adaptive `DataMisfitController(terminate_at=2.0)` schedule so the EKI "
205
+ "pseudo-time step grows automatically and training points concentrate near the posterior, "
206
+ "rather than requiring a hand-tuned fixed schedule (`terminate_at=2.0` approximates the "
207
+ "finite-time posterior); emulate stage keeps 99% of variance in a likelihood-informed PCA "
208
+ "encoding before fitting the `ScalarRandomFeatureInterface`; sample stage auto-tunes the pCN "
209
+ "step size (`optimize_stepsize`, 2,000-sample dry run from `init_stepsize=0.2`) before the full "
210
+ "100,000-sample chain with a 2,000-sample burn-in."
211
+ ),
212
+ "params": [
213
+ {"Parameter": "scheduler (calibrate)", "Value": "DataMisfitController(terminate_at=2.0)"},
214
+ {"Parameter": "retain_var (emulate PCA)", "Value": "0.99"},
215
+ {"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
216
+ {"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
217
+ {"Parameter": "init_stepsize", "Value": "0.2 (auto-tuned before the full chain)"},
218
+ ],
219
+ "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.",
220
  },
221
  "CES-EKI-CONST": {
222
  "citation": "Cleary et al., J. Comput. Phys., 2021",
223
  "url": "https://doi.org/10.1016/j.jcp.2020.109716",
224
+ "source": "`uq_experiments/calibrate_emulate_sample` — Calibrate-Emulate-Sample, calibrate stage: `Inversion()` + fixed-step scheduler.",
225
+ "math": "Identical Calibrate-Emulate-Sample pipeline to CES-EKI-DMC (see that entry for the emulate/sample math).",
226
+ "guardrails": (
227
+ "Calibrate stage uses a fixed-step `DefaultScheduler(0.1)` instead of the adaptive "
228
+ "`DataMisfitController`, trading adaptivity for a simpler, more predictable schedule; emulate "
229
+ "and sample stages are unchanged from CES-EKI-DMC (same PCA retention, feature counts, and "
230
+ "MCMC step-size tuning)."
231
+ ),
232
+ "params": [
233
+ {"Parameter": "scheduler (calibrate)", "Value": "DefaultScheduler(0.1) — fixed step"},
234
+ {"Parameter": "retain_var (emulate PCA)", "Value": "0.99"},
235
+ {"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
236
+ {"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
237
+ ],
238
+ "notes": "Same after-optimize emulator caveat as CES-EKI-DMC; the fixed step trades the DMC variant's adaptive posterior-concentration for predictability.",
239
  },
240
  "CES-IEKF-CONST": {
241
  "citation": "Cleary et al., J. Comput. Phys., 2021; Iglesias, Inverse Problems, 2016",
242
  "url": "https://doi.org/10.1016/j.jcp.2020.109716",
243
+ "source": "`uq_experiments/calibrate_emulate_sample` — Calibrate-Emulate-Sample, calibrate stage: `GaussNewtonInversion(prior)` + fixed-step scheduler.",
244
+ "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.",
245
+ "guardrails": (
246
+ "Calibrate stage uses `GaussNewtonInversion(prior)` with a fixed pseudo-step `DefaultScheduler(0.1)`; "
247
+ "emulate and sample stages are unchanged from CES-EKI-DMC (same PCA retention, feature counts, "
248
+ "and MCMC step-size tuning)."
249
+ ),
250
+ "params": [
251
+ {"Parameter": "scheduler (calibrate)", "Value": "DefaultScheduler(0.1) — fixed pseudo-step"},
252
+ {"Parameter": "retain_var (emulate PCA)", "Value": "0.99"},
253
+ {"Parameter": "n_features / n_features_opt", "Value": "100/60 (L63), 200/160 (L96 variants)"},
254
+ {"Parameter": "MCMC samples / burn-in", "Value": "100,000 / 2,000 (pCN-MH)"},
255
+ ],
256
+ "notes": "Same after-optimize emulator caveat as CES-EKI-DMC, with the Gauss-Newton calibrate step's usual small-ensemble sensitivity (see IEKF).",
257
  },
258
  "ADAM": {
259
  "citation": "Kingma & Ba, ICLR, 2015",
260
  "url": "https://doi.org/10.48550/arXiv.1412.6980",
261
+ "source": "`opt_experiments/adam` — custom (non-Flux) Adam implementation.",
262
+ "math": (
263
+ "Adam gradient descent on the whitened least-squares loss `L(θ)=½‖R^{-1/2}(y-G(θ))‖²`: "
264
+ "first/second raw-gradient moments `m_t=β₁m_{t-1}+(1-β₁)g_t`, `v_t=β₂v_{t-1}+(1-β₂)g_t²` are "
265
+ "bias-corrected to `m̂_t, v̂_t` and applied as `θ_{t+1}=θ_t - α·m̂_t/(√v̂_t+ε)`. The gradient "
266
+ "`g_t` comes from a whitened Jacobian obtained via `ForwardDiff.jl` forward-mode automatic "
267
+ "differentiation through the Lorenz solver itself — not a surrogate."
268
+ ),
269
+ "guardrails": (
270
+ "`N_ens` initial-condition perturbations are drawn and averaged into the gradient at every step "
271
+ "to reduce gradient noise from the chaotic forward model (`N_ens=1` here, i.e. a single-sample "
272
+ "stochastic gradient); beyond Adam's own adaptive per-parameter step scaling there is no line "
273
+ "search, trust region, or restart mechanism."
274
+ ),
275
+ "params": [
276
+ {"Parameter": "α (step size)", "Value": "0.001"},
277
+ {"Parameter": "β₁, β₂", "Value": "0.9, 0.999"},
278
+ {"Parameter": "ε", "Value": "1e-8"},
279
+ {"Parameter": "N_ens (IC perturbations)", "Value": "1"},
280
+ ],
281
+ "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.",
282
  },
283
  "LM": {
284
  "citation": "Levenberg, 1944; Marquardt, 1963; Fletcher, 1971",
285
  "url": "https://doi.org/10.1090/qam/10666",
286
+ "source": "`opt_experiments/levenberg_marquardt` — custom Levenberg-Marquardt implementation.",
287
+ "math": (
288
+ "Damped Gauss-Newton: each step solves the damped normal equations for the parameter update "
289
+ "`Δθ`, using a whitened Jacobian/residual `(J̃, r̃)` obtained via `ForwardDiff.jl`, then accepts "
290
+ "the step only if the reduction ratio `ρ` (actual vs. predicted misfit decrease) is favorable."
291
+ ),
292
+ "math_eq": r"\left(\tilde J^{\top}\tilde J + \lambda\,\mathrm{diag}(d)\right)\Delta\theta = \tilde J^{\top}\tilde r",
293
+ "guardrails": (
294
+ "Damping `λ` adapts every step: ×4 (capped at `λ_max`) on a rejected or non-finite step "
295
+ "(`ρ<0.25` or NaN), ÷3 (floored at `λ_min`) on an accepted step (`ρ>0.75`), interpolating between "
296
+ "gradient descent (large λ) and Gauss-Newton (small λ); a non-finite `Δθ` is handled gracefully "
297
+ "(`ρ=NaN → !isfinite(ρ) → λ↑`) rather than crashing the run."
298
+ ),
299
+ "params": [
300
+ {"Parameter": "λ₀ (initial damping)", "Value": "1.0"},
301
+ {"Parameter": "λ growth / shrink factor", "Value": "×4 (reject) / ÷3 (accept)"},
302
+ {"Parameter": "λ bounds", "Value": "[1e-10, 1e8]"},
303
+ {"Parameter": "N_ens (IC perturbations)", "Value": "1"},
304
+ ],
305
+ "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.",
306
  },
307
  "GBOED": {
308
  "citation": "Holthuijzen et al. 2026",
309
  "url": "https://arxiv.org/abs/2508.13071",
310
+ "source": "`uq_experiments/BayesianOptimalExperimentalDesign` — Goal-oriented BOED, Algorithm 2 of Holthuijzen et al. 2026.",
311
+ "math": (
312
+ "Each iteration: (i) fits a GP surrogate per output dimension to all `(θ, G(θ))` collected so "
313
+ "far, (ii) draws posterior samples from the current surrogate-based posterior via Transitional/"
314
+ "Sequential MCMC (Ching & Chen, 2007), (iii) selects the next batch of forward-model evaluations "
315
+ "by maximizing a closed-form Expected Information Gain (EIG) criterion targeted at the quantity "
316
+ "of interest, using the GP's predictive covariance."
317
+ ),
318
+ "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)}",
319
+ "guardrails": (
320
+ "Candidate batches are optimized with `Optim.jl`'s `Fminbox(LBFGS())` under box constraints, "
321
+ "with a hard cap on raw EIG evaluations (`eig_call_limit`) and an outer-iteration cap "
322
+ "(`eig_outer_iters`) enforced manually — since Optim's own call limits don't cumulate across "
323
+ "Fminbox's outer barrier rounds — logging a warning that the returned point may be "
324
+ "\"under-optimized\" if either cap is hit, rather than failing silently; a diagonal jitter "
325
+ "(`eig_jitter`, relative to the GP signal variance) stabilizes the covariance solves/log-"
326
+ "determinants; batch points can be chosen greedily, jointly, or via a hybrid strategy that "
327
+ "switches from greedy to joint optimization once a greedy step's cost exceeds a threshold."
328
+ ),
329
+ "params": [
330
+ {"Parameter": "eig_call_limit", "Value": "2000 EIG evaluations"},
331
+ {"Parameter": "eig_outer_iters", "Value": "20"},
332
+ {"Parameter": "eig_jitter", "Value": "1e-6 × GP signal variance"},
333
+ {"Parameter": "batch_init_strategy", "Value": ":posterior_subsample"},
334
+ ],
335
+ "notes": "**Empirically, the inner EIG-maximization loop over candidate batches 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** (GP fit/predict cost scales super-linearly in training-set size) — this is also why the repo's own config drops to a smaller `max_iters` on the higher-dimensional L96 cases (\"it's so slow, so let's start here\").",
336
  },
337
  "LM-KGMM": {
338
  "citation": "Giorgini, Bischoff & Souza, arXiv:2503.18054, 2025",
339
  "url": "https://arxiv.org/abs/2503.18054",
340
+ "source": "`opt_experiments/score_based_lm` — Levenberg-Marquardt with a GFDT/KGMM Jacobian surrogate.",
341
+ "math": (
342
+ "Same LM step as above, but the Jacobian is not differentiated through the solver — it is "
343
+ "estimated from trajectory data via the generalized fluctuation-dissipation theorem (GFDT): the "
344
+ "response of a fitted statistic `A(x)` to a parameter perturbation is approximated by a lag-"
345
+ "integrated, Tukey-tapered correlation with a \"conjugate\" variable `B(x)` built from the score "
346
+ "function `s(x)=∇log p(x)`, then chained through `dφ/dm` to the summary-statistic Jacobian."
347
+ ),
348
+ "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",
349
+ "guardrails": (
350
+ "The score `s(x)` is estimated with a k-means Gaussian-mixture score estimator (KGMM): the "
351
+ "trajectory is clustered into components whose mixture has a closed-form score. "
352
+ "`stein_recalibrate` rescales the estimated score so `-E[x·s(x)ᵀ]=I` holds on the calibration "
353
+ "ensemble (a built-in consistency check/correction); `kgmm_ridge` shrinks per-cluster "
354
+ "covariances toward the global covariance to avoid singular clusters; `is_collapsed_window` "
355
+ "(minimum trajectory variance below a tolerance) detects windows that have collapsed onto a "
356
+ "fixed point — GFDT is undefined there, since there is no invariant measure to differentiate — "
357
+ "and falls back to a direct `ForwardDiff.jl` Jacobian on those draws; total cost is capped at "
358
+ "`outer_iter × N_ens ≤ budget_total` per cell, trading ensemble size against iteration count "
359
+ "under a fixed evaluation budget."
360
+ ),
361
+ "params": [
362
+ {"Parameter": "kgmm_ridge", "Value": "0.1"},
363
+ {"Parameter": "tukey_alpha (response taper)", "Value": "0.75"},
364
+ {"Parameter": "budget_total (outer_iter × N_ens)", "Value": "500"},
365
+ {"Parameter": "collapse tolerance (min variance)", "Value": "1e-2"},
366
+ ],
367
+ "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.",
368
  },
369
  "LM-DSM": {
370
  "citation": "Giorgini, Bischoff & Souza, arXiv:2509.19660, 2025",
371
  "url": "https://arxiv.org/abs/2509.19660",
372
+ "source": "`opt_experiments/score_based_lm` — Levenberg-Marquardt with a GFDT/DSM Jacobian surrogate.",
373
+ "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.",
374
+ "guardrails": (
375
+ "A neural network is trained to denoise samples perturbed by Gaussian noise at a single fixed "
376
+ "scale `σ`; its optimal denoiser equals the noised density's score (Vincent, 2011), and as "
377
+ "`σ→0` this recovers the true score at the cost of a harder training problem. `σ` is fixed per "
378
+ "benchmark rather than annealed, chosen empirically as the smallest noise level at which the "
379
+ "network still receives a usable learning signal. Same Stein recalibration and collapsed-window "
380
+ "`ForwardDiff.jl` fallback as LM-KGMM; same Tukey-tapered response integral and evaluation-"
381
+ "budget cap."
382
+ ),
383
+ "params": [
384
+ {"Parameter": "σ (DSM noise scale)", "Value": "0.2 (L63) / 0.025 (L96 variants)"},
385
+ {"Parameter": "tukey_alpha (response taper)", "Value": "0.75"},
386
+ {"Parameter": "budget_total (outer_iter × N_ens)", "Value": "500"},
387
+ {"Parameter": "collapse tolerance (min variance)", "Value": "1e-2"},
388
+ ],
389
+ "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.",
390
  },
391
  }
392
 
393
+ # KNOWN_METHODS is the source of truth for the abbreviation/full-name list,
394
+ # so this page renders even without any NetCDF data loaded.
395
+ abbrs = sorted(meta["abbreviation"] for meta in KNOWN_METHODS.values())
396
+ full_name_by_abbr = {meta["abbreviation"]: meta["Method"] for meta in KNOWN_METHODS.values()}
397
+
398
  # Selection UI (defaults to query param if valid)
399
+ abbr = st.query_params.get("method")
400
+ default_idx = abbrs.index(abbr) if isinstance(abbr, str) and abbr in abbrs else 0
 
401
  sel = str(st.selectbox("Choose a method", options=abbrs, index=default_idx))
402
 
403
  # Persist selection to URL
404
  st.query_params["method"] = sel
405
 
406
  # Display details
 
407
  meta = method_meta.get(sel, {})
408
 
409
+ st.subheader(f"{sel} — {full_name_by_abbr.get(sel, '')}")
410
+ if meta.get("source"):
411
+ st.caption(meta["source"])
412
+
413
+ st.markdown("**Update Equation**")
414
+ st.markdown(meta.get("math", "Mathematical form pending"))
415
+ if meta.get("math_eq"):
416
+ st.latex(meta["math_eq"])
417
+
418
+ st.markdown("**Robustness, Speed & Guardrails**")
419
+ st.markdown(meta.get("guardrails", "Guardrail details pending"))
420
+
421
+ st.markdown("**Configuration Parameters**")
422
+ params = meta.get("params")
423
+ if params:
424
+ st.dataframe(pd.DataFrame(params), hide_index=True, use_container_width=True)
425
+ else:
426
+ st.write("Parameter details pending")
427
 
428
+ st.markdown("**Convergence & Speed Notes**")
429
+ st.markdown(meta.get("notes", "No notes recorded yet"))
430
+
431
+ col1, col2 = st.columns(2)
432
  with col1:
433
  with st.container(border=True):
434
  st.markdown("**Citation**")
 
437
  with st.container(border=True):
438
  st.markdown("**Link**")
439
  st.link_button("Open reference", meta.get("url", "https://example.com"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
441
  st.page_link("pages/OptimizationLeaderboard.py", label="⬅ Back to Optimization Leaderboard", icon="↩️")
src/pages/OptimizationLeaderboard.py CHANGED
@@ -18,7 +18,7 @@ st.sidebar.title("Navigation")
18
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
19
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
20
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
21
- st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="📘")
22
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
23
 
24
  st.markdown(r"**Optimization target.** A run succeeds when the normalized weighted distance from the ensemble-mean forward-model output to the true data falls at or below the (selected in the target controls) RMSE target level $\tau$:")
 
18
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
19
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
20
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
21
+ st.sidebar.page_link("pages/MethodDetails.py", label="Method configurations", icon="📘")
22
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
23
 
24
  st.markdown(r"**Optimization target.** A run succeeds when the normalized weighted distance from the ensemble-mean forward-model output to the true data falls at or below the (selected in the target controls) RMSE target level $\tau$:")
src/pages/RawData.py CHANGED
@@ -16,7 +16,7 @@ st.sidebar.title("Navigation")
16
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
17
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
18
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
19
- st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="📘")
20
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
21
 
22
  st.title("Raw Averaged Forward Model Runs")
 
16
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
17
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
18
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
19
+ st.sidebar.page_link("pages/MethodDetails.py", label="Method configurations", icon="📘")
20
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
21
 
22
  st.title("Raw Averaged Forward Model Runs")
src/pages/UQLeaderboard.py CHANGED
@@ -18,7 +18,7 @@ st.sidebar.title("Navigation")
18
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
19
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
20
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
21
- st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="📘")
22
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
23
 
24
  st.markdown(r"**UQ target.** Marginal coverage, $\widetilde{S}(q)$ measures the fraction of output dimensions where the true observation $y$ falls below the $q$-quantile of the posterior predictive distribution. This effectively treats each output dimension as an independent trial, so we also whitened the outputs and data by $\Gamma$, Though not notated, we also truncate to retain 99% of the variance:")
 
18
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
19
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
20
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
21
+ st.sidebar.page_link("pages/MethodDetails.py", label="Method configurations", icon="📘")
22
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
23
 
24
  st.markdown(r"**UQ target.** Marginal coverage, $\widetilde{S}(q)$ measures the fraction of output dimensions where the true observation $y$ falls below the $q$-quantile of the posterior predictive distribution. This effectively treats each output dimension as an independent trial, so we also whitened the outputs and data by $\Gamma$, Though not notated, we also truncate to retain 99% of the variance:")
src/streamlit_app.py CHANGED
@@ -19,7 +19,7 @@ st.sidebar.title("Navigation")
19
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
20
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
21
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
22
- st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="📘")
23
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
24
 
25
  st.title("Calibration Benchmark")
@@ -100,8 +100,8 @@ st.subheader("Method taxonomy")
100
  st.markdown(
101
  "Every method carries four independent tags — how it searches, what update "
102
  "mechanism drives each step, what it's built to report, and whether/when it uses "
103
- "a surrogate model. See the **📘 Methods** page for citations and per-method "
104
- "performance charts."
105
  )
106
  _taxonomy_rows = [
107
  {
 
19
  st.sidebar.page_link("streamlit_app.py", label="Home", icon="🏠")
20
  st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="📊")
21
  st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="🎯")
22
+ st.sidebar.page_link("pages/MethodDetails.py", label="Method configurations", icon="📘")
23
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
24
 
25
  st.title("Calibration Benchmark")
 
100
  st.markdown(
101
  "Every method carries four independent tags — how it searches, what update "
102
  "mechanism drives each step, what it's built to report, and whether/when it uses "
103
+ "a surrogate model. See the **📘 Method configurations** page to investigate more "
104
+ "about any individual configuration."
105
  )
106
  _taxonomy_rows = [
107
  {