methods_page_detail
#14
by odunbar - opened
- .claude/skills/leaderboard-manager/references/populate-metadata.md +13 -3
- CLAUDE.md +22 -1
- src/pages/MethodDetails.py +343 -136
- src/pages/OptimizationLeaderboard.py +1 -1
- src/pages/RawData.py +1 -1
- src/pages/UQLeaderboard.py +1 -1
- src/streamlit_app.py +3 -3
.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 |
-
"
|
| 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 |
-
- `
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
If you don't have a value yet, use `"(citation pending)"` / `"https://example.com"` /
|
| 75 |
-
`"(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 📘
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 5 |
import streamlit as st
|
| 6 |
|
| 7 |
try:
|
| 8 |
-
from
|
| 9 |
except ModuleNotFoundError:
|
| 10 |
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
-
from
|
| 12 |
|
| 13 |
-
st.set_page_config(page_title="Method
|
| 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="
|
| 21 |
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
|
| 22 |
|
| 23 |
-
st.title("Method
|
| 24 |
-
st.caption("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
st.markdown(
|
| 26 |
"For the exact implementation of each algorithm, see "
|
| 27 |
-
"[CalibrationLeaderboard.jl](https://github.com/odunbar/CalibrationLeaderboard.jl)
|
|
|
|
| 28 |
)
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
},
|
| 51 |
"ETKI": {
|
| 52 |
"citation": "Schillings & Stuart, Numer. Math., 2017",
|
| 53 |
"url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
|
| 54 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
"IEKF": {
|
| 57 |
"citation": "Iglesias, Inverse Problems, 2016",
|
| 58 |
"url": "https://doi.org/10.1088/0266-5611/32/2/025002",
|
| 59 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
},
|
| 61 |
"UKI": {
|
| 62 |
"citation": "Huang, Huang & Stuart, Physica D, 2022",
|
| 63 |
"url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
|
| 64 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
},
|
| 66 |
"ABC": {
|
| 67 |
-
"citation": "
|
| 68 |
-
"url": "https://
|
| 69 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
},
|
| 71 |
"HM": {
|
| 72 |
"citation": "Williamson et al. 2013; King et al. 2025",
|
| 73 |
"url": "https://example.com/hm",
|
| 74 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
},
|
| 91 |
"ADAM": {
|
| 92 |
"citation": "Kingma & Ba, ICLR, 2015",
|
| 93 |
"url": "https://doi.org/10.48550/arXiv.1412.6980",
|
| 94 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
},
|
| 96 |
"LM": {
|
| 97 |
"citation": "Levenberg, 1944; Marquardt, 1963; Fletcher, 1971",
|
| 98 |
"url": "https://doi.org/10.1090/qam/10666",
|
| 99 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
},
|
| 101 |
"GBOED": {
|
| 102 |
"citation": "Holthuijzen et al. 2026",
|
| 103 |
"url": "https://arxiv.org/abs/2508.13071",
|
| 104 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
},
|
| 106 |
"LM-KGMM": {
|
| 107 |
"citation": "Giorgini, Bischoff & Souza, arXiv:2503.18054, 2025",
|
| 108 |
"url": "https://arxiv.org/abs/2503.18054",
|
| 109 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
},
|
| 111 |
"LM-DSM": {
|
| 112 |
"citation": "Giorgini, Bischoff & Souza, arXiv:2509.19660, 2025",
|
| 113 |
"url": "https://arxiv.org/abs/2509.19660",
|
| 114 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
},
|
| 116 |
}
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
# Selection UI (defaults to query param if valid)
|
| 119 |
-
|
| 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} — {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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="
|
| 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="
|
| 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="
|
| 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 **📘
|
| 104 |
-
"
|
| 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 |
{
|