Add gradient-based results with a typical configuration of hyperparameters
#5
by odunbar - opened
- .cache/known_methods_snapshot.json +21 -0
- .claude/skills/leaderboard-manager/references/manage-display.md +54 -0
- CLAUDE.md +5 -1
- data/UKI_results/uki_l96_spatial_forcing_ensemble_results.nc +2 -2
- data/adam_results/leaderboard_adam_l63_2026-06-26.nc +3 -0
- data/adam_results/leaderboard_adam_l96_const-force_2026-06-26.nc +3 -0
- data/adam_results/leaderboard_adam_l96_vec-force_2026-06-26.nc +3 -0
- data/levenberg_marquardt_results/leaderboard_lm_l63_2026-06-29.nc +3 -0
- data/levenberg_marquardt_results/leaderboard_lm_l96_const-force_2026-06-29.nc +3 -0
- data/levenberg_marquardt_results/leaderboard_lm_l96_vec-force_2026-06-29.nc +3 -0
- src/common/leaderboard.py +88 -15
- src/common/method_registry.py +28 -1
- src/data_store.py +26 -9
- src/pages/MethodDetails.py +10 -0
- src/pages/OptimizationLeaderboard.py +1 -0
.cache/known_methods_snapshot.json
CHANGED
|
@@ -57,13 +57,34 @@
|
|
| 57 |
"aliases": [
|
| 58 |
"ces-eki-dmc"
|
| 59 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
},
|
| 62 |
"observed_methods": [
|
| 63 |
"abc",
|
|
|
|
| 64 |
"etki",
|
| 65 |
"hm",
|
| 66 |
"iekf",
|
|
|
|
| 67 |
"teki",
|
| 68 |
"uki"
|
| 69 |
],
|
|
|
|
| 57 |
"aliases": [
|
| 58 |
"ces-eki-dmc"
|
| 59 |
]
|
| 60 |
+
},
|
| 61 |
+
"adam": {
|
| 62 |
+
"abbreviation": "ADAM",
|
| 63 |
+
"Method": "Adaptive Moment Estimation",
|
| 64 |
+
"family": "gradient",
|
| 65 |
+
"aliases": [
|
| 66 |
+
"adam"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
"lm": {
|
| 70 |
+
"abbreviation": "LM",
|
| 71 |
+
"Method": "Levenberg-Marquardt",
|
| 72 |
+
"family": "gradient",
|
| 73 |
+
"aliases": [
|
| 74 |
+
"lm",
|
| 75 |
+
"levenberg_marquardt",
|
| 76 |
+
"levenberg-marquardt",
|
| 77 |
+
"gradient_descent"
|
| 78 |
+
]
|
| 79 |
}
|
| 80 |
},
|
| 81 |
"observed_methods": [
|
| 82 |
"abc",
|
| 83 |
+
"adam",
|
| 84 |
"etki",
|
| 85 |
"hm",
|
| 86 |
"iekf",
|
| 87 |
+
"lm",
|
| 88 |
"teki",
|
| 89 |
"uki"
|
| 90 |
],
|
.claude/skills/leaderboard-manager/references/manage-display.md
CHANGED
|
@@ -206,6 +206,60 @@ x=alt.X(
|
|
| 206 |
|
| 207 |
---
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
## Display change checklist
|
| 210 |
|
| 211 |
1. Edit the relevant file (`leaderboard.py` for both leaderboards; `MethodDetails.py` for
|
|
|
|
| 206 |
|
| 207 |
---
|
| 208 |
|
| 209 |
+
## Plotting conventions and shared display constants
|
| 210 |
+
|
| 211 |
+
### Where shared constants live
|
| 212 |
+
|
| 213 |
+
`src/common/method_registry.py` is the single source of truth for any display property
|
| 214 |
+
that must be consistent across multiple pages or charts. Currently it exports:
|
| 215 |
+
|
| 216 |
+
- **`METHOD_COLORS: dict[str, str]`** — abbreviation → hex color for every known method,
|
| 217 |
+
derived from `KNOWN_METHODS` in declaration order.
|
| 218 |
+
- **`_METHOD_PALETTE`** — the 10-slot Vega tableau10 palette that backs `METHOD_COLORS`.
|
| 219 |
+
|
| 220 |
+
**Rule:** if a constant is used in more than one file, it belongs in `method_registry.py`,
|
| 221 |
+
not defined locally in `leaderboard.py` or a page file.
|
| 222 |
+
|
| 223 |
+
When **adding a new method**, its color is assigned automatically — no manual update
|
| 224 |
+
needed, as long as it is appended to the end of `KNOWN_METHODS`. The palette cycles every
|
| 225 |
+
10 methods.
|
| 226 |
+
|
| 227 |
+
### How to import shared colors in a new chart
|
| 228 |
+
|
| 229 |
+
```python
|
| 230 |
+
try:
|
| 231 |
+
from common.method_registry import METHOD_COLORS
|
| 232 |
+
except ModuleNotFoundError:
|
| 233 |
+
from src.common.method_registry import METHOD_COLORS
|
| 234 |
+
|
| 235 |
+
method_color = alt.Color(
|
| 236 |
+
"abbreviation:N",
|
| 237 |
+
title="Method",
|
| 238 |
+
scale=alt.Scale(domain=list(METHOD_COLORS.keys()), range=list(METHOD_COLORS.values())),
|
| 239 |
+
)
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
Always use the full registry domain/range (not a filtered per-benchmark subset) so colors
|
| 243 |
+
are stable when the user switches benchmarks or navigates between pages.
|
| 244 |
+
|
| 245 |
+
### General plotting conventions
|
| 246 |
+
|
| 247 |
+
Unless the task explicitly asks for something different, follow these defaults for every
|
| 248 |
+
new Altair chart in the app:
|
| 249 |
+
|
| 250 |
+
| Concern | Convention |
|
| 251 |
+
|---|---|
|
| 252 |
+
| Method color | `METHOD_COLORS` via the import above — never a local palette |
|
| 253 |
+
| Benchmark color | `alt.Color("benchmark:N")` — Altair assigns automatically; no custom palette needed |
|
| 254 |
+
| Full-width charts | `st.altair_chart(chart, use_container_width=True)` — never `width="stretch"` |
|
| 255 |
+
| Ensemble-size x-axis (quantitative) | `alt.Axis(values=ens_ticks, format="d")` — derive ticks from data, integer format |
|
| 256 |
+
| Ensemble-size x-axis (ordinal bar) | `ensemble_size:O` with `sort=[str(e) for e in ens_ticks]` and `axis=alt.Axis(labelAngle=0)` |
|
| 257 |
+
| Y-axis scale | Linear by default; add `scale=alt.Scale(type="log")` only when explicitly requested |
|
| 258 |
+
| Tooltips | Always include `abbreviation`, `ensemble_size`, and the primary metric with `format=".4f"` |
|
| 259 |
+
| Failed runs | Mark with a cross point (`mark_point(shape="cross", angle=45, size=200, filled=True)`) at y=0, same color as the method line |
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
## Display change checklist
|
| 264 |
|
| 265 |
1. Edit the relevant file (`leaderboard.py` for both leaderboards; `MethodDetails.py` for
|
CLAUDE.md
CHANGED
|
@@ -61,6 +61,8 @@ calibration_benchmark/
|
|
| 61 |
│ ├── *.nc Kalman results (TEKI, ETKI, IEKF)
|
| 62 |
│ ├── bayesian/ ABC + HM results
|
| 63 |
│ ├── UKI_results/ UKI results
|
|
|
|
|
|
|
| 64 |
│ └── ces-eki-dmc_results/ CES-EKI-DMC ensemble-results files (UQ source)
|
| 65 |
│ ├── ces-eki-dmc_l63_ensemble_results_2026-06-15.nc
|
| 66 |
│ ├── ces-eki-dmc_l96_ensemble_results_2026-06-15.nc
|
|
@@ -86,7 +88,7 @@ calibration_benchmark/
|
|
| 86 |
| `algorithm_type` | Canonical method key (e.g. `teki`) |
|
| 87 |
| `algorithm_alias` | Normalized raw name from NetCDF |
|
| 88 |
| `abbreviation` | Display name (e.g. `TEKI`) |
|
| 89 |
-
| `family` | `Kalman` · `Bayesian` · `calibrate_then_emulate` |
|
| 90 |
| `rmse_target` | Target RMSE level |
|
| 91 |
| `ensemble_size` | Ensemble / particle count |
|
| 92 |
| `metric` | Mean forward-model runs over `random_seed` (lower = better; −1 = failed) |
|
|
@@ -150,6 +152,8 @@ annotations in the suitability table. Update if a new benchmark is added.
|
|
| 150 |
| `abc` | ABC | Bayesian | — |
|
| 151 |
| `hm` | HM | Bayesian | — |
|
| 152 |
| `ces-eki-dmc` | CES-EKI-DMC | calibrate_then_emulate | — |
|
|
|
|
|
|
|
| 153 |
|
| 154 |
## How to extend
|
| 155 |
|
|
|
|
| 61 |
│ ├── *.nc Kalman results (TEKI, ETKI, IEKF)
|
| 62 |
│ ├── bayesian/ ABC + HM results
|
| 63 |
│ ├── UKI_results/ UKI results
|
| 64 |
+
│ ├── adam_results/ ADAM gradient optimizer results
|
| 65 |
+
│ ├── levenberg_marquardt_results/ LM gradient optimizer results
|
| 66 |
│ └── ces-eki-dmc_results/ CES-EKI-DMC ensemble-results files (UQ source)
|
| 67 |
│ ├── ces-eki-dmc_l63_ensemble_results_2026-06-15.nc
|
| 68 |
│ ├── ces-eki-dmc_l96_ensemble_results_2026-06-15.nc
|
|
|
|
| 88 |
| `algorithm_type` | Canonical method key (e.g. `teki`) |
|
| 89 |
| `algorithm_alias` | Normalized raw name from NetCDF |
|
| 90 |
| `abbreviation` | Display name (e.g. `TEKI`) |
|
| 91 |
+
| `family` | `Kalman` · `Bayesian` · `calibrate_then_emulate` · `gradient` |
|
| 92 |
| `rmse_target` | Target RMSE level |
|
| 93 |
| `ensemble_size` | Ensemble / particle count |
|
| 94 |
| `metric` | Mean forward-model runs over `random_seed` (lower = better; −1 = failed) |
|
|
|
|
| 152 |
| `abc` | ABC | Bayesian | — |
|
| 153 |
| `hm` | HM | Bayesian | — |
|
| 154 |
| `ces-eki-dmc` | CES-EKI-DMC | calibrate_then_emulate | — |
|
| 155 |
+
| `adam` | ADAM | gradient | — |
|
| 156 |
+
| `lm` | LM | gradient | gradient_descent |
|
| 157 |
|
| 158 |
## How to extend
|
| 159 |
|
data/UKI_results/uki_l96_spatial_forcing_ensemble_results.nc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c8999ffa5c70d8e891443a24929622e203e0e55bb263be52add1b0585b8a6c7
|
| 3 |
+
size 12343
|
data/adam_results/leaderboard_adam_l63_2026-06-26.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ecd403e20e439541dd463e8776df75b7f07f129b728499a1b0c856513ee6dd07
|
| 3 |
+
size 11440
|
data/adam_results/leaderboard_adam_l96_const-force_2026-06-26.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6b8487b9895121629d875a23dccdbd3fcc32ea138044e87ef6590d531b4d85d2
|
| 3 |
+
size 11440
|
data/adam_results/leaderboard_adam_l96_vec-force_2026-06-26.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:539eb03a55d4ffe0a51b3b45db3b5dc0d0e159c8132e71639ba2eb5c0f72af95
|
| 3 |
+
size 11440
|
data/levenberg_marquardt_results/leaderboard_lm_l63_2026-06-29.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f3fd9735597718164ef37f3f24bd010d6984b6b1ad9cec7d7e8d9f851715f791
|
| 3 |
+
size 11440
|
data/levenberg_marquardt_results/leaderboard_lm_l96_const-force_2026-06-29.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f5de3b6ad488344217323ac4be8b96ad213c2cba03b075ace5fb57ef09d9e67b
|
| 3 |
+
size 11440
|
data/levenberg_marquardt_results/leaderboard_lm_l96_vec-force_2026-06-29.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7a647aeac2cc740665d27164aef3295bf7f07f8d461013dcb396ec843624c47e
|
| 3 |
+
size 11440
|
src/common/leaderboard.py
CHANGED
|
@@ -24,18 +24,24 @@ import altair as alt
|
|
| 24 |
import pandas as pd
|
| 25 |
import streamlit as st
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
"#4c78a8", "#f58518", "#e45756", "#72b7b2", "#54a24b",
|
| 32 |
-
"#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
|
| 33 |
-
]
|
| 34 |
|
| 35 |
_SUITABLE = "#009E73" # Okabe-Ito teal-green (colorblind-safe)
|
| 36 |
_UNSUITABLE = "#C0392B" # dark red
|
| 37 |
_UNTESTED = "#BDBDBD" # gray
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def _render_suitability_table(
|
| 41 |
store: pd.DataFrame,
|
|
@@ -50,7 +56,16 @@ def _render_suitability_table(
|
|
| 50 |
store["benchmark"].unique().tolist(),
|
| 51 |
key=lambda bm: benchmark_dims[bm][0] if (benchmark_dims and bm in benchmark_dims) else bm,
|
| 52 |
)
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
target_str = str(float(suitability_target))
|
| 56 |
target_str_col = f"{target_col}_str"
|
|
@@ -119,8 +134,8 @@ def _render_suitability_table(
|
|
| 119 |
f"Evaluated at target = {suitability_target}. "
|
| 120 |
f"**Green**: at some ensemble size, failure rate < {failure_threshold:.0f}% "
|
| 121 |
f"and mean budget ≤ {ratio_threshold:.0f}× the best method (ratio shown). "
|
| 122 |
-
"**Red**:
|
| 123 |
-
"**Gray**: no data for this
|
| 124 |
)
|
| 125 |
st.dataframe(display_df.style.apply(_style, axis=None), use_container_width=True)
|
| 126 |
|
|
@@ -254,14 +269,12 @@ def render_leaderboard(
|
|
| 254 |
valid_saved = [m for m in saved_methods if m in available_methods]
|
| 255 |
st.session_state[k_methods] = valid_saved if valid_saved else available_methods
|
| 256 |
|
| 257 |
-
#
|
| 258 |
-
#
|
| 259 |
-
color_domain = available_methods
|
| 260 |
-
color_range = [_METHOD_PALETTE[i % len(_METHOD_PALETTE)] for i in range(len(available_methods))]
|
| 261 |
method_color = alt.Color(
|
| 262 |
"abbreviation:N",
|
| 263 |
title="Method",
|
| 264 |
-
scale=alt.Scale(domain=
|
| 265 |
)
|
| 266 |
|
| 267 |
def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
|
|
@@ -377,10 +390,41 @@ def render_leaderboard(
|
|
| 377 |
lambda rank: f"{ {1: '🥇', 2: '🥈', 3: '🥉'}.get(rank, '')} #{rank}".strip()
|
| 378 |
)
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
return scored_df
|
| 381 |
|
| 382 |
leaderboard_df = build_scored_table(filtered, add_rank=True)
|
| 383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
if scoring_mode == "Mean Forward Model Runs":
|
| 385 |
score_basis = "mean forward-model runs at the selected target level (lower is better)"
|
| 386 |
elif scoring_mode == "Minimum Forward Model Runs":
|
|
@@ -538,6 +582,35 @@ def render_leaderboard(
|
|
| 538 |
],
|
| 539 |
)
|
| 540 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
if chart_layers:
|
| 542 |
st.altair_chart(alt.layer(*chart_layers), use_container_width=True)
|
| 543 |
|
|
|
|
| 24 |
import pandas as pd
|
| 25 |
import streamlit as st
|
| 26 |
|
| 27 |
+
try:
|
| 28 |
+
from common.method_registry import METHOD_COLORS
|
| 29 |
+
except ModuleNotFoundError:
|
| 30 |
+
from src.common.method_registry import METHOD_COLORS
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
_SUITABLE = "#009E73" # Okabe-Ito teal-green (colorblind-safe)
|
| 33 |
_UNSUITABLE = "#C0392B" # dark red
|
| 34 |
_UNTESTED = "#BDBDBD" # gray
|
| 35 |
|
| 36 |
+
# Family display order in the leaderboard table (lower = earlier).
|
| 37 |
+
# Unknown families fall back to 99 and appear at the end.
|
| 38 |
+
_FAMILY_ORDER: dict[str, int] = {
|
| 39 |
+
"Kalman": 0,
|
| 40 |
+
"Bayesian": 1,
|
| 41 |
+
"gradient": 2,
|
| 42 |
+
"calibrate_then_emulate": 3,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
|
| 46 |
def _render_suitability_table(
|
| 47 |
store: pd.DataFrame,
|
|
|
|
| 56 |
store["benchmark"].unique().tolist(),
|
| 57 |
key=lambda bm: benchmark_dims[bm][0] if (benchmark_dims and bm in benchmark_dims) else bm,
|
| 58 |
)
|
| 59 |
+
_abbr_family = (
|
| 60 |
+
store[["abbreviation", "family"]].dropna()
|
| 61 |
+
.drop_duplicates("abbreviation")
|
| 62 |
+
.set_index("abbreviation")["family"]
|
| 63 |
+
.to_dict()
|
| 64 |
+
)
|
| 65 |
+
methods = sorted(
|
| 66 |
+
store["abbreviation"].dropna().unique().tolist(),
|
| 67 |
+
key=lambda a: (_FAMILY_ORDER.get(_abbr_family.get(a, ""), 99), a),
|
| 68 |
+
)
|
| 69 |
|
| 70 |
target_str = str(float(suitability_target))
|
| 71 |
target_str_col = f"{target_col}_str"
|
|
|
|
| 134 |
f"Evaluated at target = {suitability_target}. "
|
| 135 |
f"**Green**: at some ensemble size, failure rate < {failure_threshold:.0f}% "
|
| 136 |
f"and mean budget ≤ {ratio_threshold:.0f}× the best method (ratio shown). "
|
| 137 |
+
"**Red** (failed): data present but all runs failed to reach the target. "
|
| 138 |
+
"**Gray (—)**: no data for this benchmark."
|
| 139 |
)
|
| 140 |
st.dataframe(display_df.style.apply(_style, axis=None), use_container_width=True)
|
| 141 |
|
|
|
|
| 269 |
valid_saved = [m for m in saved_methods if m in available_methods]
|
| 270 |
st.session_state[k_methods] = valid_saved if valid_saved else available_methods
|
| 271 |
|
| 272 |
+
# Color scale fixed to the full registry so every chart in the app uses the same
|
| 273 |
+
# color per method regardless of which benchmark or page is shown.
|
|
|
|
|
|
|
| 274 |
method_color = alt.Color(
|
| 275 |
"abbreviation:N",
|
| 276 |
title="Method",
|
| 277 |
+
scale=alt.Scale(domain=list(METHOD_COLORS.keys()), range=list(METHOD_COLORS.values())),
|
| 278 |
)
|
| 279 |
|
| 280 |
def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
|
|
|
|
| 390 |
lambda rank: f"{ {1: '🥇', 2: '🥈', 3: '🥉'}.get(rank, '')} #{rank}".strip()
|
| 391 |
)
|
| 392 |
|
| 393 |
+
# Append methods that had data for this target but every seed failed (all-NaN metric).
|
| 394 |
+
# They appear at the bottom of the table as "DNF" so users can distinguish
|
| 395 |
+
# "tried and failed" from "not tested on this benchmark".
|
| 396 |
+
tried_abbrevs = set(ranking_source["abbreviation"].dropna().unique())
|
| 397 |
+
ranked_abbrevs = set(scored_df["abbreviation"].dropna().unique())
|
| 398 |
+
dnf_abbrevs = tried_abbrevs - ranked_abbrevs
|
| 399 |
+
if dnf_abbrevs:
|
| 400 |
+
dnf_rows = failure_agg[failure_agg["abbreviation"].isin(dnf_abbrevs)].copy()
|
| 401 |
+
for col in [
|
| 402 |
+
"Score", "mean_runs_score", "minimum_runs_score", "ensemble_score",
|
| 403 |
+
"Mean Forward Model Runs", "Minimum Forward Model Runs",
|
| 404 |
+
"Optimal Ensemble Size", "Ensemble Sizes Used",
|
| 405 |
+
]:
|
| 406 |
+
dnf_rows[col] = float("nan")
|
| 407 |
+
if add_rank:
|
| 408 |
+
dnf_rows["Rank"] = float("nan")
|
| 409 |
+
dnf_rows["Placement"] = "DNF"
|
| 410 |
+
scored_df = pd.concat([scored_df, dnf_rows], ignore_index=True)
|
| 411 |
+
|
| 412 |
return scored_df
|
| 413 |
|
| 414 |
leaderboard_df = build_scored_table(filtered, add_rank=True)
|
| 415 |
|
| 416 |
+
# Re-sort by family group, then by performance within each group.
|
| 417 |
+
# Placement numbers still reflect overall performance rank.
|
| 418 |
+
if not leaderboard_df.empty:
|
| 419 |
+
leaderboard_df = leaderboard_df.assign(
|
| 420 |
+
_family_sort=leaderboard_df["family"].map(_FAMILY_ORDER).fillna(99),
|
| 421 |
+
_is_dnf=leaderboard_df["Score"].isna(),
|
| 422 |
+
).sort_values(
|
| 423 |
+
["_family_sort", "_is_dnf", "Score"],
|
| 424 |
+
ascending=[True, True, False],
|
| 425 |
+
na_position="last",
|
| 426 |
+
).drop(columns=["_family_sort", "_is_dnf"]).reset_index(drop=True)
|
| 427 |
+
|
| 428 |
if scoring_mode == "Mean Forward Model Runs":
|
| 429 |
score_basis = "mean forward-model runs at the selected target level (lower is better)"
|
| 430 |
elif scoring_mode == "Minimum Forward Model Runs":
|
|
|
|
| 582 |
],
|
| 583 |
)
|
| 584 |
)
|
| 585 |
+
# Dashed vertical rule for methods with only one ensemble size so they
|
| 586 |
+
# remain easy to spot when multi-ensemble methods dominate the x-axis.
|
| 587 |
+
if not all_ens_combos.empty:
|
| 588 |
+
single_ens_abbrevs = (
|
| 589 |
+
all_ens_combos.groupby("abbreviation")["ensemble_size"]
|
| 590 |
+
.nunique()
|
| 591 |
+
.pipe(lambda s: s[s == 1].index.tolist())
|
| 592 |
+
)
|
| 593 |
+
if single_ens_abbrevs:
|
| 594 |
+
rule_df = (
|
| 595 |
+
all_ens_combos[all_ens_combos["abbreviation"].isin(single_ens_abbrevs)]
|
| 596 |
+
.drop_duplicates()
|
| 597 |
+
)
|
| 598 |
+
chart_layers.append(
|
| 599 |
+
alt.Chart(rule_df)
|
| 600 |
+
.mark_rule(strokeDash=[4, 4], opacity=0.5)
|
| 601 |
+
.encode(
|
| 602 |
+
x=alt.X(
|
| 603 |
+
"ensemble_size:Q",
|
| 604 |
+
axis=alt.Axis(values=ens_ticks, format="d"),
|
| 605 |
+
),
|
| 606 |
+
color=method_color,
|
| 607 |
+
tooltip=[
|
| 608 |
+
alt.Tooltip("abbreviation:N", title="Method"),
|
| 609 |
+
alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
|
| 610 |
+
],
|
| 611 |
+
)
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
if chart_layers:
|
| 615 |
st.altair_chart(alt.layer(*chart_layers), use_container_width=True)
|
| 616 |
|
src/common/method_registry.py
CHANGED
|
@@ -46,7 +46,34 @@ KNOWN_METHODS = {
|
|
| 46 |
"Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
|
| 47 |
"family": "calibrate_then_emulate",
|
| 48 |
"aliases": ["ces-eki-dmc"]
|
| 49 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
|
|
|
|
| 46 |
"Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
|
| 47 |
"family": "calibrate_then_emulate",
|
| 48 |
"aliases": ["ces-eki-dmc"]
|
| 49 |
+
},
|
| 50 |
+
"adam": {
|
| 51 |
+
"abbreviation": "ADAM",
|
| 52 |
+
"Method": "Adaptive Moment Estimation",
|
| 53 |
+
"family": "gradient",
|
| 54 |
+
"aliases": ["adam"],
|
| 55 |
+
},
|
| 56 |
+
"lm": {
|
| 57 |
+
"abbreviation": "LM",
|
| 58 |
+
"Method": "Levenberg-Marquardt",
|
| 59 |
+
"family": "gradient",
|
| 60 |
+
"aliases": ["lm", "levenberg_marquardt", "levenberg-marquardt", "gradient_descent"],
|
| 61 |
+
},
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# Vega tableau10 palette — one slot per method in KNOWN_METHODS declaration order.
|
| 66 |
+
# New methods appended to KNOWN_METHODS get the next slot; existing colors never shift.
|
| 67 |
+
_METHOD_PALETTE = [
|
| 68 |
+
"#4c78a8", "#f58518", "#e45756", "#72b7b2", "#54a24b",
|
| 69 |
+
"#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
# Stable abbreviation → hex color mapping. Import this wherever Altair charts are built
|
| 73 |
+
# so every plot in the app assigns the same color to each method.
|
| 74 |
+
METHOD_COLORS: dict[str, str] = {
|
| 75 |
+
meta["abbreviation"]: _METHOD_PALETTE[i % len(_METHOD_PALETTE)]
|
| 76 |
+
for i, meta in enumerate(KNOWN_METHODS.values())
|
| 77 |
}
|
| 78 |
|
| 79 |
|
src/data_store.py
CHANGED
|
@@ -32,21 +32,27 @@ DATASET_FILES = {
|
|
| 32 |
"l63_ensemble_results.nc",
|
| 33 |
"UKI_results/uki_l63_ensemble_results.nc",
|
| 34 |
"bayesian/l63_abc.nc",
|
| 35 |
-
"bayesian/l63_hm.nc"
|
|
|
|
|
|
|
| 36 |
],
|
| 37 |
"L96": [
|
| 38 |
"l96_ensemble_results.nc",
|
| 39 |
"UKI_results/uki_l96_ensemble_results.nc",
|
| 40 |
-
"bayesian/l96_abc.nc"
|
|
|
|
|
|
|
| 41 |
],
|
| 42 |
"L96_NN_FORCING": [
|
| 43 |
"l96_nn_forcing_ensemble_results.nc",
|
| 44 |
-
"UKI_results/uki_l96_nn_forcing_ensemble_results.nc"
|
| 45 |
],
|
| 46 |
"L96_SPATIAL_FORCING": [
|
| 47 |
"l96_spatial_forcing_ensemble_results.nc",
|
| 48 |
"UKI_results/uki_l96_spatial_forcing_ensemble_results.nc",
|
| 49 |
-
"bayesian/l96_varying_abc.nc"
|
|
|
|
|
|
|
| 50 |
],
|
| 51 |
}
|
| 52 |
|
|
@@ -146,8 +152,8 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
|
|
| 146 |
)
|
| 147 |
continue
|
| 148 |
|
| 149 |
-
# Track failures (
|
| 150 |
-
failures_count = (metric == -1).sum(dim="random_seed")
|
| 151 |
|
| 152 |
failure_rate = (failures_count / dataset.sizes["random_seed"]) * 100
|
| 153 |
|
|
@@ -180,7 +186,12 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
|
|
| 180 |
)
|
| 181 |
continue
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
if not records:
|
| 186 |
return pd.DataFrame(
|
|
@@ -193,10 +204,16 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
|
|
| 193 |
merged["ensemble_size"] = pd.to_numeric(merged["ensemble_size"], errors="coerce")
|
| 194 |
merged["metric"] = pd.to_numeric(merged["metric"], errors="coerce")
|
| 195 |
merged["failure_rate"] = pd.to_numeric(merged["failure_rate"], errors="coerce").fillna(0.0)
|
|
|
|
| 196 |
if drop_nan_metric:
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
else:
|
| 199 |
-
merged = merged.
|
| 200 |
merged["ensemble_size"] = merged["ensemble_size"].astype(int)
|
| 201 |
merged["abbreviation"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("abbreviation"))
|
| 202 |
merged["Method"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("Method"))
|
|
|
|
| 32 |
"l63_ensemble_results.nc",
|
| 33 |
"UKI_results/uki_l63_ensemble_results.nc",
|
| 34 |
"bayesian/l63_abc.nc",
|
| 35 |
+
"bayesian/l63_hm.nc",
|
| 36 |
+
"adam_results/leaderboard_adam_l63_2026-06-26.nc",
|
| 37 |
+
"levenberg_marquardt_results/leaderboard_lm_l63_2026-06-29.nc",
|
| 38 |
],
|
| 39 |
"L96": [
|
| 40 |
"l96_ensemble_results.nc",
|
| 41 |
"UKI_results/uki_l96_ensemble_results.nc",
|
| 42 |
+
"bayesian/l96_abc.nc",
|
| 43 |
+
"adam_results/leaderboard_adam_l96_const-force_2026-06-26.nc",
|
| 44 |
+
"levenberg_marquardt_results/leaderboard_lm_l96_const-force_2026-06-29.nc",
|
| 45 |
],
|
| 46 |
"L96_NN_FORCING": [
|
| 47 |
"l96_nn_forcing_ensemble_results.nc",
|
| 48 |
+
"UKI_results/uki_l96_nn_forcing_ensemble_results.nc",
|
| 49 |
],
|
| 50 |
"L96_SPATIAL_FORCING": [
|
| 51 |
"l96_spatial_forcing_ensemble_results.nc",
|
| 52 |
"UKI_results/uki_l96_spatial_forcing_ensemble_results.nc",
|
| 53 |
+
"bayesian/l96_varying_abc.nc",
|
| 54 |
+
"adam_results/leaderboard_adam_l96_vec-force_2026-06-26.nc",
|
| 55 |
+
"levenberg_marquardt_results/leaderboard_lm_l96_vec-force_2026-06-29.nc",
|
| 56 |
],
|
| 57 |
}
|
| 58 |
|
|
|
|
| 152 |
)
|
| 153 |
continue
|
| 154 |
|
| 155 |
+
# Track failures: -1 sentinel OR NaN (some methods use NaN instead of -1)
|
| 156 |
+
failures_count = ((metric == -1) | metric.isnull()).sum(dim="random_seed")
|
| 157 |
|
| 158 |
failure_rate = (failures_count / dataset.sizes["random_seed"]) * 100
|
| 159 |
|
|
|
|
| 186 |
)
|
| 187 |
continue
|
| 188 |
|
| 189 |
+
# Tag rows from files with exactly one ensemble size. Only those rows
|
| 190 |
+
# are retained when metric is NaN — they represent a method that was
|
| 191 |
+
# genuinely attempted at that size but every seed failed. NaN rows from
|
| 192 |
+
# multi-size files are placeholder entries for sizes never actually run.
|
| 193 |
+
df["_single_ens"] = dataset.sizes.get("ensemble_size", 1) == 1
|
| 194 |
+
records.append(df[required_cols + ["_single_ens"]])
|
| 195 |
|
| 196 |
if not records:
|
| 197 |
return pd.DataFrame(
|
|
|
|
| 204 |
merged["ensemble_size"] = pd.to_numeric(merged["ensemble_size"], errors="coerce")
|
| 205 |
merged["metric"] = pd.to_numeric(merged["metric"], errors="coerce")
|
| 206 |
merged["failure_rate"] = pd.to_numeric(merged["failure_rate"], errors="coerce").fillna(0.0)
|
| 207 |
+
merged = merged.dropna(subset=["ensemble_size"])
|
| 208 |
if drop_nan_metric:
|
| 209 |
+
# Keep NaN-metric rows only when they came from a single-ensemble-size file
|
| 210 |
+
# AND every seed failed (failure_rate ≈ 100 %). Those represent a method that
|
| 211 |
+
# was genuinely attempted but never converged. NaN rows from multi-size files
|
| 212 |
+
# are placeholders for ensemble sizes that were never actually run.
|
| 213 |
+
single_ens = merged.pop("_single_ens").fillna(False)
|
| 214 |
+
merged = merged[merged["metric"].notna() | (single_ens & (merged["failure_rate"] >= 99.9))]
|
| 215 |
else:
|
| 216 |
+
merged = merged.drop(columns=["_single_ens"], errors="ignore")
|
| 217 |
merged["ensemble_size"] = merged["ensemble_size"].astype(int)
|
| 218 |
merged["abbreviation"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("abbreviation"))
|
| 219 |
merged["Method"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("Method"))
|
src/pages/MethodDetails.py
CHANGED
|
@@ -74,6 +74,16 @@ method_meta = {
|
|
| 74 |
"url": "https://doi.org/10.1016/j.jcp.2020.109716",
|
| 75 |
"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.",
|
| 76 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
# Selection UI (defaults to query param if valid)
|
|
|
|
| 74 |
"url": "https://doi.org/10.1016/j.jcp.2020.109716",
|
| 75 |
"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.",
|
| 76 |
},
|
| 77 |
+
"ADAM": {
|
| 78 |
+
"citation": "Kingma & Ba, ICLR, 2015",
|
| 79 |
+
"url": "https://doi.org/10.48550/arXiv.1412.6980",
|
| 80 |
+
"summary": "Adaptive Moment Estimation — gradient-based optimizer that adapts per-parameter learning rates using first and second moment estimates of the gradient.",
|
| 81 |
+
},
|
| 82 |
+
"LM": {
|
| 83 |
+
"citation": "Levenberg, 1944; Marquardt, 1963; Fletcher, 1971",
|
| 84 |
+
"url": "https://doi.org/10.1090/qam/10666",
|
| 85 |
+
"summary": "Levenberg-Marquardt — damped least-squares algorithm that interpolates between gradient descent and Gauss-Newton steps for efficient nonlinear least-squares minimization.",
|
| 86 |
+
},
|
| 87 |
}
|
| 88 |
|
| 89 |
# Selection UI (defaults to query param if valid)
|
src/pages/OptimizationLeaderboard.py
CHANGED
|
@@ -34,4 +34,5 @@ render_leaderboard(
|
|
| 34 |
default_target=1.1,
|
| 35 |
raw_page="pages/RawData.py",
|
| 36 |
benchmark_dims=BENCHMARK_DIMS,
|
|
|
|
| 37 |
)
|
|
|
|
| 34 |
default_target=1.1,
|
| 35 |
raw_page="pages/RawData.py",
|
| 36 |
benchmark_dims=BENCHMARK_DIMS,
|
| 37 |
+
show_failure_panel=True,
|
| 38 |
)
|