Initial UQ and claude interfacing

#1
This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .cache/known_methods_snapshot.json +7 -86
  2. .claude/settings.json +0 -80
  3. .claude/skills/leaderboard-manager/SKILL.md +0 -200
  4. .claude/skills/leaderboard-manager/references/add-method.md +0 -141
  5. .claude/skills/leaderboard-manager/references/add-uq-data.md +0 -149
  6. .claude/skills/leaderboard-manager/references/assess.md +0 -85
  7. .claude/skills/leaderboard-manager/references/manage-display.md +0 -368
  8. .claude/skills/leaderboard-manager/references/move-between-boards.md +0 -116
  9. .claude/skills/leaderboard-manager/references/populate-metadata.md +0 -150
  10. .gitattributes +0 -1
  11. .gitignore +3 -34
  12. CLAUDE.md +0 -254
  13. README.md +0 -120
  14. data/UKI_results/uki_l96_spatial_forcing_ensemble_results.nc +2 -2
  15. data/adam_results/leaderboard_adam_l63_2026-06-26.nc +0 -3
  16. data/adam_results/leaderboard_adam_l96_const-force_2026-06-26.nc +0 -3
  17. data/adam_results/leaderboard_adam_l96_vec-force_2026-06-26.nc +0 -3
  18. data/ces-eki-const_results/ces-eki-const_l63_ensemble_results_2026-07-09_minimal.nc +0 -3
  19. data/ces-eki-const_results/ces-eki-const_l96_ensemble_results_2026-07-09_minimal.nc +0 -3
  20. data/ces-eki-const_results/ces-eki-const_l96_nn_forcing_ensemble_results_2026-07-09_minimal.nc +0 -3
  21. data/ces-eki-const_results/ces-eki-const_l96_spatial_forcing_ensemble_results_2026-07-09_minimal.nc +0 -3
  22. data/ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15_minimal.nc +0 -3
  23. data/ces-eki-dmc_results/ces-eki-dmc_l96_ensemble_results_2026-06-15_minimal.nc +0 -3
  24. data/ces-eki-dmc_results/ces-eki-dmc_l96_nn_forcing_ensemble_results_2026-06-15_minimal.nc +0 -3
  25. data/ces-eki-dmc_results/ces-eki-dmc_l96_spatial_forcing_ensemble_results_2026-06-15_minimal.nc +0 -3
  26. data/ces-iekf-const_results/ces-iekf-const_l63_ensemble_results_2026-07-17_minimal.nc +0 -3
  27. data/ces-iekf-const_results/ces-iekf-const_l96_ensemble_results_2026-07-17_minimal.nc +0 -3
  28. data/ces-iekf-const_results/ces-iekf-const_l96_nn_forcing_ensemble_results_2026-07-17_minimal.nc +0 -3
  29. data/ces-iekf-const_results/ces-iekf-const_l96_spatial_forcing_ensemble_results_2026-07-17_minimal.nc +0 -3
  30. data/gnki-uq_results/leaderboard_gnki_l63_2026-07-07_minimal.nc +0 -3
  31. data/gnki-uq_results/leaderboard_gnki_l96_const-force_2026-07-07_minimal.nc +0 -3
  32. data/gnki-uq_results/leaderboard_gnki_l96_flux-force_2026-07-07_minimal.nc +0 -3
  33. data/gnki-uq_results/leaderboard_gnki_l96_vec-force_2026-07-07_minimal.nc +0 -3
  34. data/history-matching-uq_results/history-matching_l63_ensemble_results_2026-07-20_minimal.nc +0 -3
  35. data/history-matching-uq_results/history-matching_l96_const-force_2026-07-20_minimal.nc +0 -3
  36. data/history-matching-uq_results/history-matching_l96_flux-force_2026-07-21_minimal.nc +0 -3
  37. data/history-matching-uq_results/history-matching_l96_vec-force_2026-07-21_minimal.nc +0 -3
  38. data/history-matching-uq_results/history-matching_l96_vec-force_2026-07-22_minimal.nc +0 -3
  39. data/levenberg_marquardt_results/leaderboard_lm_l63_2026-06-29.nc +0 -3
  40. data/levenberg_marquardt_results/leaderboard_lm_l96_const-force_2026-06-29.nc +0 -3
  41. data/levenberg_marquardt_results/leaderboard_lm_l96_vec-force_2026-06-29.nc +0 -3
  42. requirements.txt +1 -4
  43. src/common/leaderboard.py +0 -768
  44. src/common/method_registry.py +7 -115
  45. src/data_store.py +42 -303
  46. src/media/posterior_ribbons_20_13_k5.png +0 -3
  47. src/media/posterior_ribbons_const-force_12_1_k3.png +0 -3
  48. src/media/posterior_ribbons_flux-force_80_1_k3.png +0 -3
  49. src/media/posterior_ribbons_vec-force_65_1_k3.png +0 -3
  50. src/pages/MethodDetails.py +19 -46
.cache/known_methods_snapshot.json CHANGED
@@ -3,10 +3,7 @@
3
  "teki": {
4
  "abbreviation": "TEKI",
5
  "Method": "Tikhonov Regularized Ensemble Kalman Inversion",
6
- "parallelism": "parallel-interacting",
7
- "update_type": "kalman",
8
- "method_goal": "optimization",
9
- "emulator_use": "none",
10
  "aliases": [
11
  "teki"
12
  ]
@@ -14,10 +11,7 @@
14
  "etki": {
15
  "abbreviation": "ETKI",
16
  "Method": "Ensemble Transform Kalman Inversion",
17
- "parallelism": "parallel-interacting",
18
- "update_type": "kalman",
19
- "method_goal": "optimization",
20
- "emulator_use": "none",
21
  "aliases": [
22
  "etki"
23
  ]
@@ -25,23 +19,16 @@
25
  "iekf": {
26
  "abbreviation": "IEKF",
27
  "Method": "Iterative Ensemble Kalman Filter",
28
- "parallelism": "parallel-interacting",
29
- "update_type": "kalman",
30
- "method_goal": "uq",
31
- "emulator_use": "none",
32
  "aliases": [
33
  "iekf",
34
- "gnsl",
35
- "gnki"
36
  ]
37
  },
38
  "uki": {
39
  "abbreviation": "UKI",
40
  "Method": "Unscented Kalman Inversion",
41
- "parallelism": "parallel-interacting",
42
- "update_type": "kalman",
43
- "method_goal": "optimization",
44
- "emulator_use": "none",
45
  "aliases": [
46
  "uki"
47
  ]
@@ -49,10 +36,7 @@
49
  "abc": {
50
  "abbreviation": "ABC",
51
  "Method": "Approximate Bayesian Calibration",
52
- "parallelism": "parallel-independent",
53
- "update_type": "general",
54
- "method_goal": "uq",
55
- "emulator_use": "none",
56
  "aliases": [
57
  "abc"
58
  ]
@@ -60,80 +44,17 @@
60
  "hm": {
61
  "abbreviation": "HM",
62
  "Method": "History Matching",
63
- "parallelism": "parallel-independent",
64
- "update_type": "general",
65
- "method_goal": "uq",
66
- "emulator_use": "within-optimize",
67
  "aliases": [
68
  "hm"
69
  ]
70
- },
71
- "ces-eki-dmc": {
72
- "abbreviation": "CES-EKI-DMC",
73
- "Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
74
- "parallelism": "parallel-interacting",
75
- "update_type": "kalman",
76
- "method_goal": "uq",
77
- "emulator_use": "after-optimize",
78
- "aliases": [
79
- "ces-eki-dmc"
80
- ]
81
- },
82
- "ces-eki-const": {
83
- "abbreviation": "CES-EKI-CONST",
84
- "Method": "Calibrate Emulate Sample (EKI-Constant Scheduler)",
85
- "parallelism": "parallel-interacting",
86
- "update_type": "kalman",
87
- "method_goal": "uq",
88
- "emulator_use": "after-optimize",
89
- "aliases": [
90
- "ces-eki-const"
91
- ]
92
- },
93
- "ces-iekf-const": {
94
- "abbreviation": "CES-IEKF-CONST",
95
- "Method": "Calibrate Emulate Sample (IEKF-Constant Scheduler)",
96
- "parallelism": "parallel-interacting",
97
- "update_type": "kalman",
98
- "method_goal": "uq",
99
- "emulator_use": "after-optimize",
100
- "aliases": [
101
- "ces-iekf-const"
102
- ]
103
- },
104
- "adam": {
105
- "abbreviation": "ADAM",
106
- "Method": "Adaptive Moment Estimation",
107
- "parallelism": "serial",
108
- "update_type": "gradient",
109
- "method_goal": "optimization",
110
- "emulator_use": "none",
111
- "aliases": [
112
- "adam"
113
- ]
114
- },
115
- "lm": {
116
- "abbreviation": "LM",
117
- "Method": "Levenberg-Marquardt",
118
- "parallelism": "serial",
119
- "update_type": "gradient",
120
- "method_goal": "optimization",
121
- "emulator_use": "none",
122
- "aliases": [
123
- "lm",
124
- "levenberg_marquardt",
125
- "levenberg-marquardt",
126
- "gradient_descent"
127
- ]
128
  }
129
  },
130
  "observed_methods": [
131
  "abc",
132
- "adam",
133
  "etki",
134
  "hm",
135
  "iekf",
136
- "lm",
137
  "teki",
138
  "uki"
139
  ],
 
3
  "teki": {
4
  "abbreviation": "TEKI",
5
  "Method": "Tikhonov Regularized Ensemble Kalman Inversion",
6
+ "family": "Kalman",
 
 
 
7
  "aliases": [
8
  "teki"
9
  ]
 
11
  "etki": {
12
  "abbreviation": "ETKI",
13
  "Method": "Ensemble Transform Kalman Inversion",
14
+ "family": "Kalman",
 
 
 
15
  "aliases": [
16
  "etki"
17
  ]
 
19
  "iekf": {
20
  "abbreviation": "IEKF",
21
  "Method": "Iterative Ensemble Kalman Filter",
22
+ "family": "Kalman",
 
 
 
23
  "aliases": [
24
  "iekf",
25
+ "gnsl"
 
26
  ]
27
  },
28
  "uki": {
29
  "abbreviation": "UKI",
30
  "Method": "Unscented Kalman Inversion",
31
+ "family": "Kalman",
 
 
 
32
  "aliases": [
33
  "uki"
34
  ]
 
36
  "abc": {
37
  "abbreviation": "ABC",
38
  "Method": "Approximate Bayesian Calibration",
39
+ "family": "Bayesian",
 
 
 
40
  "aliases": [
41
  "abc"
42
  ]
 
44
  "hm": {
45
  "abbreviation": "HM",
46
  "Method": "History Matching",
47
+ "family": "Bayesian",
 
 
 
48
  "aliases": [
49
  "hm"
50
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
  },
53
  "observed_methods": [
54
  "abc",
 
55
  "etki",
56
  "hm",
57
  "iekf",
 
58
  "teki",
59
  "uki"
60
  ],
.claude/settings.json DELETED
@@ -1,80 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
- "autoMemoryEnabled": false,
4
- "permissions": {
5
- "allow": [
6
- "Bash(julia --project *)",
7
- "Read(*)",
8
- "FileWrite(src/*, test/*, docs/*, examples/*, ai/*)",
9
- "Edit(src/*)",
10
- "Write(src/*)",
11
- "Grep",
12
- "Glob",
13
- "Bash(find *)",
14
- "Bash(ls *)",
15
- "Bash(tree *)",
16
- "Bash(wc *)",
17
- "Bash(head *)",
18
- "Bash(tail *)",
19
- "Bash(stat *)",
20
- "Bash(grep *)",
21
- "Bash(git log*)",
22
- "Bash(git diff*)",
23
- "Bash(git status*)",
24
- "Bash(git show*)",
25
- "Bash(git blame*)",
26
- "Bash(git ls-files*)",
27
- "Bash(git rev-parse*)",
28
- "Bash(git describe*)",
29
- "Bash(git shortlog*)",
30
- "Bash(git tag -l*)",
31
- "Bash(git tag --list*)",
32
- "Bash(git stash list*)",
33
- "Bash(git stash show*)",
34
- "Bash(git remote -v*)",
35
- "Bash(git remote show*)",
36
- "Bash(git branch -l*)",
37
- "Bash(git branch --list*)",
38
- "Bash(git branch -a*)",
39
- "Bash(git branch -r*)"
40
- ],
41
- "deny": [
42
- "Read(./.env)",
43
- "Bash(git commit*)",
44
- "Bash(git add*)",
45
- "Bash(git push*)",
46
- "Bash(git pull*)",
47
- "Bash(git merge*)",
48
- "Bash(git rebase*)",
49
- "Bash(git checkout*)",
50
- "Bash(git switch*)",
51
- "Bash(git restore*)",
52
- "Bash(git reset*)",
53
- "Bash(git cherry-pick*)",
54
- "Bash(git clean*)",
55
- "Bash(git rm*)"
56
- ]
57
- },
58
- "hooks": {
59
- "UserPromptSubmit": [
60
- {
61
- "hooks": [
62
- {
63
- "type": "command",
64
- "command": "printf '\\n---\\nSKILL ROUTING: Review the available skills listed in the system context. If one clearly matches this task, invoke it via the Skill tool before proceeding. If no skill clearly matches, use AskUserQuestion to ask the user: (a) whether they would like to apply any of the listed skills to this task anyway, (b) whether they would like to create a new skill using the skill-creator skill — in which case propose a suggested skill name and one-line description tailored to this task — or (c) proceed without a skill. Also use AskUserQuestion to ask the user whether they would like to use subagents (e.g., Explore, Plan, general-purpose) for this task, and if so, how (e.g., for research, parallel work, or isolated execution). Do NOT proceed with the task until the user has answered.\\n'"
65
- }
66
- ]
67
- }
68
- ],
69
- "Stop": [
70
- {
71
- "hooks": [
72
- {
73
- "type": "command",
74
- "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); [sys.stdout.write(json.dumps(dict(decision='block',reason='If this task changed the repo structurally (added, removed, or renamed files; changed architecture, run/build commands, data layout, or method-registry/page conventions), ask the user whether to update root CLAUDE.md; otherwise stop normally.'))+chr(10))] if not d.get('stop_hook_active') else None\""
75
- }
76
- ]
77
- }
78
- ]
79
- }
80
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/SKILL.md DELETED
@@ -1,200 +0,0 @@
1
- ---
2
- name: leaderboard-manager
3
- description: >
4
- Manages the Calibration Benchmark Streamlit leaderboard dashboard — the repository at
5
- calibration_benchmark/ that ranks parameter-calibration methods on forward-model-run
6
- efficiency. Use this skill for ANY of the following tasks, even if the user doesn't use
7
- the exact words below:
8
-
9
- • Adding a new method to the leaderboard (e.g. "add EKS to the dashboard", "register
10
- new method", "wire up the new NetCDF results file", "include algorithm X")
11
- • Registering or replacing benchmark result data ("add new L96 results", "update the
12
- data file", "point the app at the new .nc file")
13
- • Registering UQ ensemble-results data ("add CES-EKI-DMC to the UQ leaderboard",
14
- "register UQ results", "wire up the new ensemble_results.nc", "add UQ data for L96")
15
- • Changing how the leaderboard table or figure is displayed ("change column order",
16
- "rename a column", "show failure rate in the table", "update the chart colours",
17
- "add a scoring mode", "tweak the Altair chart", "fix the x-axis ticks")
18
- • Moving or duplicating a method between the Optimization and UQ leaderboards ("put
19
- TEKI on the UQ board too", "populate UQ leaderboard", "register UQ results")
20
- • Filling in or correcting method metadata ("add the citation for UKI", "fix the
21
- summary text", "update the reference URL", "ETKI citation is wrong")
22
- • Assessing or auditing the leaderboard for bugs, staleness, or dead code ("is
23
- the leaderboard stale?", "check for bugs", "what's broken", "audit the dashboard")
24
-
25
- Always invoke this skill when the user is working on this Streamlit leaderboard
26
- dashboard, even if they phrase the task as a general code edit.
27
- ---
28
-
29
- # Leaderboard Manager
30
-
31
- This skill covers maintenance tasks for the Calibration Benchmark Streamlit dashboard.
32
- Read the **shared model** section below (it applies to every task), then open the
33
- reference file for the specific task.
34
-
35
- ---
36
-
37
- ## Task router
38
-
39
- | User intent | Reference file to open |
40
- |---|---|
41
- | Add a method (+ its result NetCDF) | `references/add-method.md` |
42
- | Register new UQ ensemble-results NC | `references/add-uq-data.md` |
43
- | Change table columns, chart, scoring modes | `references/manage-display.md` |
44
- | Move/duplicate a method to the UQ leaderboard | `references/move-between-boards.md` |
45
- | Fill in or correct citation / summary / URL | `references/populate-metadata.md` |
46
- | Assess for staleness / bugs | `references/assess.md` |
47
-
48
- Open only the relevant file — the shared model here covers what all tasks need.
49
-
50
- ---
51
-
52
- ## Shared data model
53
-
54
- ### Two leaderboards, one registry
55
-
56
- | Leaderboard | Page | Data loader | Target coord | Data dict |
57
- |---|---|---|---|---|
58
- | Optimization | `src/pages/OptimizationLeaderboard.py` | `load_metric_store()` | `rmse_target` | `DATASET_FILES` |
59
- | UQ | `src/pages/UQLeaderboard.py` | `load_uq_store()` → `load_uq_budget_store()` | `uq_target` | `UQ_BUDGET_FILES` |
60
-
61
- Both call `render_leaderboard()` from `src/common/leaderboard.py` — any change there
62
- affects both boards simultaneously.
63
-
64
- The UQ leaderboard uses a **different data pipeline** from the optimization one:
65
- - Optimization: `DATASET_FILES` → `_load_store()` reads NetCDF `metric` variable directly.
66
- - UQ: `UQ_BUDGET_FILES` → `load_uq_budget_store()` reads `output_coverage` and computes
67
- budget-for-coverage on the fly. See `references/add-uq-data.md` for the schema.
68
-
69
- ### Method identity chain
70
-
71
- A method's identity flows through three layers that must stay in sync:
72
-
73
- ```
74
- NetCDF file
75
- └─ algorithm_type dim (raw string, e.g. "gnki", "teki")
76
- └─ normalize_method_name() → lowercase alias
77
- └─ canonicalize_method_name() → canonical key (e.g. "iekf", "teki")
78
- └─ KNOWN_METHODS[canonical] → { abbreviation, Method, parallelism, update_type, method_goal, emulator_use, aliases }
79
- └─ method_meta[abbreviation] → { citation, url, summary }
80
- ```
81
-
82
- `KNOWN_METHODS` lives in `src/common/method_registry.py`.
83
- `method_meta` lives in `src/pages/MethodDetails.py` (keyed by **abbreviation**, not the
84
- canonical key).
85
-
86
- ### Metric semantics
87
-
88
- **Optimization leaderboard:**
89
- - `metric` = mean forward-model runs to reach the RMSE target (lower is better).
90
- - `metric == -1` encodes a **failed** run. `_load_store` strips these before averaging and
91
- records their fraction as `failure_rate`.
92
-
93
- **UQ leaderboard:**
94
- - `metric` = mean budget N_ens·k_iter to reach the coverage tolerance (lower is better).
95
- - `NaN` (not -1) encodes a failed run — target never reached within k_iter range.
96
- - `failure_rate` = % of seeds where coverage condition was never satisfied.
97
- - See `references/add-uq-data.md` for details on the coverage computation.
98
-
99
- ### Required NetCDF dimensions
100
-
101
- **Optimization files:**
102
- ```python
103
- EXPECTED_DIMS = ("algorithm_type", "rmse_target", "ensemble_size", "random_seed")
104
- ```
105
-
106
- **UQ files** use a completely different schema — `output_coverage` variable, not `metric`.
107
- See `references/add-uq-data.md` for the full spec.
108
-
109
- ---
110
-
111
- ## Critical wiring / gotchas
112
-
113
- These traps apply to every task. Read them before touching any file.
114
-
115
- **1. Method metadata is split across three files.**
116
- Adding or editing a method requires changes in all three:
117
- - `src/common/method_registry.py` — canonical key, abbreviation, taxonomy tags (parallelism, update_type, method_goal, emulator_use), aliases
118
- - `src/pages/MethodDetails.py` — citation, URL, summary (in `method_meta` dict, keyed by abbreviation)
119
- - `src/data_store.py` — `DATASET_FILES` (optimization) or `UQ_BUDGET_FILES` (UQ) data path registration
120
-
121
- **2. Sidebar navigation is duplicated across five files.**
122
- `st.sidebar.page_link(...)` blocks are copy-pasted in:
123
- `streamlit_app.py`, `pages/OptimizationLeaderboard.py`, `pages/UQLeaderboard.py`,
124
- `pages/MethodDetails.py`, `pages/RawData.py`.
125
- Adding a new page means editing all five. (This is known tech debt.)
126
-
127
- **3. `_load_store` has filename-based special-casing.**
128
- `src/data_store.py` infers `algorithm_type` and `ensemble_size` from the **filename**
129
- when they are missing from the NetCDF. This only works for filenames containing `"abc"` or
130
- `"hm"`. Every other method must carry `algorithm_type` and `ensemble_size` as dimensions
131
- in the NetCDF itself, or you must add a new branch in `_load_store`.
132
-
133
- **4. `@st.cache_data` — clear the cache after data changes.**
134
- `load_metric_store()`, `load_uq_store()`, and `load_uq_budget_store()` are all cached.
135
- After registering a new file or changing `DATASET_FILES`/`UQ_BUDGET_FILES`, the user must
136
- clear the Streamlit cache (top-right menu → "Clear cache", or restart the app).
137
-
138
- **5. `git lfs pull` is required before the first run.**
139
- `data/*.nc` are Git LFS pointers (130 bytes) until pulled. The app will show no data if
140
- LFS files haven't been fetched.
141
-
142
- **6. Streamlit LaTeX rendering: `st.latex()` for display math, not `st.markdown()`.**
143
- Streamlit's KaTeX parser does not render multi-line `$...$` blocks inside `st.markdown`.
144
- For display-math equations, always use `st.latex(r"...")` (single-line raw string). For
145
- surrounding text with inline math, `st.markdown(r"$x$")` works fine. Mixing them:
146
- ```python
147
- st.markdown(r"The condition is:")
148
- st.latex(r"\left|S(q) - q\right| \leq c\sqrt{\frac{q(1-q)}{N_y}}")
149
- st.markdown(r"where $c$ is the target-scaling level.")
150
- ```
151
-
152
- ---
153
-
154
- ## Known antipatterns
155
-
156
- **`width="stretch"` is invalid Streamlit API.**
157
- `st.dataframe` and `st.altair_chart` accept `use_container_width=True` (bool) or
158
- `width=<int>` (pixels). Use `use_container_width=True` for full-width rendering.
159
-
160
- **Hardcoded RMSE target list in `MethodDetails.py`.**
161
- `target_options` must be derived from the loaded data:
162
- ```python
163
- sorted(slice_df["rmse_target"].astype(str).unique().tolist())
164
- ```
165
- A hardcoded list silently hides any target level present in the data but absent from the
166
- list (e.g. ABC has a `2.0` target).
167
-
168
- **`UQ_DATASET_FILES` is intentionally empty — do not add UQ files there.**
169
- UQ data is loaded via `UQ_BUDGET_FILES` and `load_uq_budget_store()`, not via
170
- `UQ_DATASET_FILES` / `_load_store()`. Adding a file to `UQ_DATASET_FILES` will have no
171
- effect on the UQ leaderboard because `load_uq_store()` derives from the budget store only.
172
-
173
- ---
174
-
175
- ## Smoke-test after any change
176
-
177
- ```bash
178
- # From the repo root
179
- source .venv/bin/activate
180
- streamlit run src/streamlit_app.py
181
- ```
182
-
183
- Then visit `http://localhost:8501` and confirm:
184
- - The new method (or updated display) appears on the relevant leaderboard.
185
- - `.cache/known_methods_snapshot.json` → `unmapped_observed_methods` is empty.
186
- - The Methods page (`📘 Methods`) shows the method with correct citation/summary.
187
-
188
- If data files haven't been LFS-pulled the app will be blank — run `git lfs pull` first.
189
-
190
- ---
191
-
192
- ## Final step
193
-
194
- After completing the task, offer the user a chance to improve the **`leaderboard-manager`**
195
- skill itself using skill-creator. For example:
196
-
197
- > "Would you like to improve the **leaderboard-manager** skill itself using skill-creator?
198
- > You can share suggestions, or I can analyse patterns from this session — edge cases that
199
- > came up, steps that were unclear, anything that felt awkward — to refine the skill for
200
- > next time."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/references/add-method.md DELETED
@@ -1,141 +0,0 @@
1
- # Add a Method (+ its Data)
2
-
3
- Use this checklist whenever a new calibration algorithm needs to appear on the
4
- Optimization leaderboard. For the UQ leaderboard, complete this checklist first, then
5
- follow `move-between-boards.md`.
6
-
7
- ---
8
-
9
- ## Pre-flight: understand the incoming NetCDF
10
-
11
- Open the collaborator's file (e.g. from `From_Rob_6-10/`) and verify its structure:
12
-
13
- ```python
14
- import xarray as xr
15
- ds = xr.open_dataset("path/to/file.nc")
16
- print(ds) # check dimensions and variables
17
- print(ds["metric"])
18
- ```
19
-
20
- The file must have:
21
- - A `metric` variable (float; shape over the dims below).
22
- - Dimensions: `algorithm_type`, `rmse_target`, `ensemble_size`, `random_seed`.
23
- - `algorithm_type` is a string coordinate that names the algorithm.
24
- - All dim names must match exactly (case-sensitive).
25
-
26
- **If `algorithm_type` is missing from the NetCDF:**
27
- The loader (`src/data_store.py:129–137`) only infers it from the filename for `"abc"` and
28
- `"hm"` files. For any other method you must either:
29
- (a) add the `algorithm_type` dim to the NetCDF before placing it in `data/`, or
30
- (b) add a new `elif` branch in `_load_store()` analogous to the existing `abc`/`hm` block.
31
-
32
- **If `ensemble_size` is missing:**
33
- Same situation — currently only inferred for `"abc"` files (hardcoded to 1). Fix or add a
34
- branch.
35
-
36
- ---
37
-
38
- ## Step 1 — Register the method in `src/common/method_registry.py`
39
-
40
- Open `src/common/method_registry.py` and add an entry to `KNOWN_METHODS` (the dict starts
41
- at line 7). Follow the existing pattern exactly:
42
-
43
- ```python
44
- "new-key": {
45
- "abbreviation": "NEWABBR", # Short uppercase display name
46
- "Method": "Full Method Name", # Human-readable name for tables
47
- "parallelism": "parallel-interacting", # serial | parallel-independent | parallel-interacting
48
- "update_type": "kalman", # gradient | kalman | general
49
- "method_goal": "optimization", # optimization | uq
50
- "emulator_use": "none", # none | within-optimize | after-optimize
51
- "aliases": ["new-key", "alias2"], # All raw strings that may appear in NetCDF algorithm_type
52
- },
53
- ```
54
-
55
- **Canonical key** (`"new-key"`): lowercase, hyphen-separated. Must match what
56
- `canonicalize_method_name()` will resolve to from the NetCDF's `algorithm_type` strings.
57
-
58
- **Aliases**: include every spelling variant that could appear in the raw NetCDF, both
59
- lowercase and the exact case used. The `build_alias_lookup()` function (line 53) also
60
- registers `canonical.upper()` automatically, but doesn't handle mixed-case; be thorough.
61
-
62
- After saving, `ALIAS_TO_CANONICAL` is rebuilt at import time — no extra step needed.
63
-
64
- ---
65
-
66
- ## Step 2 — Add display metadata in `src/pages/MethodDetails.py`
67
-
68
- Open `src/pages/MethodDetails.py`. Find the `method_meta` dict (line 40) and add an entry
69
- keyed by the **abbreviation** (not the canonical key):
70
-
71
- ```python
72
- "NEWABBR": {
73
- "citation": "Author et al., Journal, Year",
74
- "url": "https://doi.org/...",
75
- "summary": "One-sentence description of what this algorithm does.",
76
- },
77
- ```
78
-
79
- If you don't have the citation/URL yet, use placeholder strings and follow up with
80
- `populate-metadata.md`. The app will show "Citation pending" / a broken link if omitted,
81
- but won't crash.
82
-
83
- ---
84
-
85
- ## Step 3 — Place the NetCDF and register it in `src/data_store.py`
86
-
87
- 1. **Copy the file** into `data/` (or a subdirectory like `data/bayesian/`). Use
88
- `git lfs track "data/**/*.nc"` if it isn't already tracked by LFS.
89
-
90
- 2. **Add the path** to `DATASET_FILES` in `src/data_store.py` (starts at line 29):
91
-
92
- ```python
93
- DATASET_FILES = {
94
- "L63": [
95
- "l63_ensemble_results.nc",
96
- ...
97
- "my_subdir/new_method_l63.nc", # ← add here
98
- ],
99
- ...
100
- }
101
- ```
102
-
103
- Keys (`"L63"`, `"L96"`, etc.) are the benchmark labels. Add the file to every benchmark
104
- key it covers. Paths are relative to the `data/` directory.
105
-
106
- ---
107
-
108
- ## Step 4 — Verify
109
-
110
- Run the smoke-test from `SKILL.md`:
111
-
112
- ```bash
113
- streamlit run src/streamlit_app.py
114
- ```
115
-
116
- Then check:
117
-
118
- 1. **Leaderboard table** (Home page): the new method's abbreviation appears as a row.
119
- 2. **Methods page** (`📘 Methods`): the method is in the dropdown; citation/summary display.
120
- 3. **Snapshot file**: open `.cache/known_methods_snapshot.json` after the app loads and
121
- confirm:
122
- - The new canonical key is in `known_methods`.
123
- - It does **not** appear in `unmapped_observed_methods` (which lists any `algorithm_type`
124
- strings from the data that didn't match any known alias).
125
-
126
- If the method appears in `unmapped_observed_methods`, the raw `algorithm_type` in the
127
- NetCDF doesn't match any alias you registered. Add the exact string (check
128
- `observed_methods` in the snapshot for the raw value) to the `aliases` list in
129
- `method_registry.py` and reload.
130
-
131
- ---
132
-
133
- ## Common failure modes
134
-
135
- | Symptom | Likely cause |
136
- |---|---|
137
- | Method missing from leaderboard table | `algorithm_type` in NetCDF doesn't match any alias in `KNOWN_METHODS` — check `unmapped_observed_methods` |
138
- | Method shown as uppercase canonical key (e.g. `NEW-KEY`) | `method_registry.py` entry added but `abbreviation` not picked up — check `get_method_meta()` path |
139
- | "Skipping … missing expected columns" warning | NetCDF lacks `algorithm_type` or `ensemble_size` dims and no special-case branch exists in `_load_store` |
140
- | Old data still shown after file change | Streamlit cache — clear it (top-right menu or restart) |
141
- | `data/*.nc` is 130 bytes | `git lfs pull` not run |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/references/add-uq-data.md DELETED
@@ -1,149 +0,0 @@
1
- # Add UQ Ensemble-Results Data
2
-
3
- Use this reference when a collaborator provides new ensemble-results NetCDF files that
4
- should appear on the UQ leaderboard. This pipeline is entirely separate from the
5
- optimization leaderboard's `DATASET_FILES` / `_load_store()` path.
6
-
7
- ---
8
-
9
- ## How the UQ pipeline works
10
-
11
- The UQ leaderboard does not read a pre-computed `metric` variable. Instead it:
12
-
13
- 1. Reads `output_coverage` from the NC file — the empirical marginal coverage fraction
14
- `S(q)` at each iteration, ensemble size, and random seed.
15
- 2. In `load_uq_budget_store()`, finds the minimum iteration k where
16
- `|S(q) − q| ≤ c·√(q(1−q)/N_y)` holds for **all three** quantiles
17
- `q ∈ [0.15, 0.5, 0.85]` simultaneously.
18
- 3. Budget = N_ens · k_iter; NaN when the condition is never satisfied.
19
- 4. Aggregates over seeds: `mean_budget`, `mean_iters`, `failure_count`, `failure_rate`.
20
- 5. `load_uq_store()` calls `load_uq_budget_store()` and exposes `mean_budget` as the
21
- leaderboard `metric`.
22
-
23
- ---
24
-
25
- ## Required NC file schema
26
-
27
- The file must carry:
28
-
29
- | Variable / coordinate | Dims / shape | Notes |
30
- |---|---|---|
31
- | `output_coverage` | `(coverage_quantile, k_iter, ensemble_size, random_seed)` | Empirical coverage fractions in [0, 1] |
32
- | `coverage_quantile` | `(n_cov_q,)` | Must include 0.15, 0.5, and 0.85 (exactly) |
33
- | `k_iter` | `(n_k,)` | Iteration indices (1-indexed integers) |
34
- | `ensemble_size` | `(n_ens,)` | Ensemble sizes (integers) |
35
- | `random_seed` | `(n_seeds,)` | Seed indices |
36
- | `target_scaling` | `(n_ts,)` | The c values, e.g. [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5] |
37
- | `output_dim` | scalar or dimension | N_y — number of output dimensions |
38
-
39
- **Check the file structure before registering:**
40
- ```python
41
- import xarray as xr
42
- ds = xr.open_dataset("data/ces-eki-dmc_results/my_file.nc")
43
- print(ds)
44
- print("coverage_quantile:", ds["coverage_quantile"].values)
45
- print("target_scaling:", ds["target_scaling"].values)
46
- print("output_dim:", ds.sizes.get("output_dim") or ds["output_dim"].values)
47
- ```
48
-
49
- Confirm that 0.15, 0.5, and 0.85 are all present in `coverage_quantile`. If they are
50
- absent, the budget computation will silently use the nearest available quantile — check
51
- via `np.argmin(np.abs(cov_q - 0.15))` etc.
52
-
53
- ---
54
-
55
- ## Step 1 — Place the file in `data/`
56
-
57
- Place the NC file in `data/ces-eki-dmc_results/` (or an appropriate subdirectory).
58
- Ensure it is tracked by Git LFS:
59
-
60
- ```bash
61
- git lfs track "data/**/*.nc" # already set up; just confirm
62
- git add data/ces-eki-dmc_results/my_file.nc
63
- ```
64
-
65
- ---
66
-
67
- ## Step 2 — Register in `UQ_BUDGET_FILES`
68
-
69
- Open `src/data_store.py` and add the file to `UQ_BUDGET_FILES`:
70
-
71
- ```python
72
- UQ_BUDGET_FILES: dict[str, list[tuple[str, str]]] = {
73
- "L63": [
74
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15.nc"),
75
- ],
76
- "L96": [
77
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_ensemble_results_2026-06-15.nc"),
78
- ("new-method", "new-method_results/new_method_l96.nc"), # ← add here
79
- ],
80
- ...
81
- }
82
- ```
83
-
84
- Each entry is a `(algorithm_type, relative_path)` tuple. `algorithm_type` must match an
85
- entry in `KNOWN_METHODS` (or its aliases list) — see `add-method.md` for how to register
86
- a new method.
87
-
88
- Paths are relative to the `data/` directory.
89
-
90
- ---
91
-
92
- ## Step 3 — Ensure the method is registered
93
-
94
- The `algorithm_type` string in the tuple must resolve through the method identity chain
95
- to a known canonical key. If this is a new method:
96
-
97
- 1. Add an entry to `KNOWN_METHODS` in `src/common/method_registry.py` (see `add-method.md`).
98
- 2. Add display metadata in `src/pages/MethodDetails.py` `method_meta` dict.
99
-
100
- If the method is already on the optimization leaderboard, its registry entry exists —
101
- no changes to `method_registry.py` or `MethodDetails.py` are needed.
102
-
103
- ---
104
-
105
- ## Step 4 — Clear cache and verify
106
-
107
- Clear the Streamlit cache (top-right menu → "Clear cache", or restart the app).
108
-
109
- Then check:
110
-
111
- 1. **UQ Leaderboard** (`🎯 UQ Leaderboard`): the method appears in the ranked table at
112
- target levels where coverage was achievable. At very tight tolerances (low c), all
113
- seeds may fail — this is expected due to grid coarseness (N_y output dims means
114
- coverage fractions are multiples of 1/N_y).
115
-
116
- 2. **Iterations chart**: "Mean Iterations for Coverage vs Ensemble Size" should show a
117
- line for the new method.
118
-
119
- 3. **Failure rate chart**: "Failure Rate of Hitting Target ..." should show bars for
120
- the new method. 100% failure at c=1.0 is often expected for small N_y.
121
-
122
- 4. **Snapshot**: `.cache/known_methods_snapshot.json` → `unmapped_observed_methods`
123
- should not include the new method's algorithm_type string.
124
-
125
- ---
126
-
127
- ## Failure-rate expectations by N_y
128
-
129
- With N_y output dimensions, coverage fractions are multiples of 1/N_y. The first c level
130
- that permits convergence depends on N_y:
131
-
132
- | N_y | First achievable c | Reason |
133
- |---|---|---|
134
- | 9 (L63) | ≈ 1.5 | tol(q=0.5) = 0.25; nearest coverage 3/9 = 0.333 satisfies |0.333−0.5| ≤ 0.25 |
135
- | 80 (L96) | ≈ 1.0 | finer grid; easier to satisfy all three quantiles |
136
-
137
- Expect 100% failure at the tightest c levels for small N_y. This is data behavior, not a
138
- pipeline bug.
139
-
140
- ---
141
-
142
- ## What NOT to do
143
-
144
- - **Do not add UQ files to `UQ_DATASET_FILES`** — that dict is intentionally empty. The
145
- UQ leaderboard only reads from `UQ_BUDGET_FILES` via `load_uq_budget_store()`.
146
- - **Do not rename `rmse_target` to `uq_target`** — UQ files do not use a `uq_target`
147
- dimension at all; the target-scaling levels come from the `target_scaling` coordinate.
148
- - **Do not put a `metric` variable in the UQ NC file** — `load_uq_budget_store()` ignores
149
- it; the metric is computed from `output_coverage`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/references/assess.md DELETED
@@ -1,85 +0,0 @@
1
- # Assess for Staleness / Bugs
2
-
3
- Use this checklist when the user asks for an audit, bug check, or staleness review of the
4
- leaderboard dashboard. Read all relevant source files first (in parallel), then work
5
- through each item below and report findings grouped by severity.
6
-
7
- ---
8
-
9
- ## Files to read
10
-
11
- Read these in a single parallel batch before starting the checklist:
12
-
13
- - `src/common/method_registry.py` — `KNOWN_METHODS`, aliases
14
- - `src/common/leaderboard.py` — `render_leaderboard()`
15
- - `src/data_store.py` — `DATASET_FILES`, `UQ_DATASET_FILES`, `_load_store()`
16
- - `src/streamlit_app.py` — home page wiring
17
- - `src/pages/UQLeaderboard.py`
18
- - `src/pages/MethodDetails.py` — `method_meta` dict, target list
19
- - `src/pages/RawData.py`
20
- - `.cache/known_methods_snapshot.json` — observed vs known methods
21
-
22
- ---
23
-
24
- ## Checklist
25
-
26
- ### API correctness
27
-
28
- - [ ] **`width="stretch"` in `st.dataframe` or `st.altair_chart`** — invalid parameter.
29
- Should be `use_container_width=True`. Grep for `width="stretch"` across `src/`.
30
-
31
- ### Data integrity
32
-
33
- - [ ] **Methods in `KNOWN_METHODS` with no entry in `DATASET_FILES`** — registered but
34
- will never appear on any leaderboard (no data). Check each canonical key in
35
- `KNOWN_METHODS` against the file paths listed in `DATASET_FILES`; confirm at least one
36
- registered file would produce rows for that `algorithm_type`.
37
- - [ ] **`.cache/known_methods_snapshot.json` → `unmapped_observed_methods` not empty** —
38
- means a NetCDF carries an `algorithm_type` string that has no entry in `KNOWN_METHODS`
39
- (or its aliases list). Those rows silently fall through with a raw uppercase fallback
40
- abbreviation and `"unmapped"` for all four taxonomy tags (parallelism, update_type,
41
- method_goal, emulator_use).
42
- - [ ] **`DATASET_FILES` references a file path that doesn't exist on disk** — `_load_store`
43
- emits a `warnings.warn` and skips it silently; run
44
- `python -c "from src.data_store import load_metric_store; load_metric_store()"` and
45
- watch for warnings, or just `ls` the registered paths.
46
-
47
- ### Display correctness
48
-
49
- - [ ] **Hardcoded `target_options` in `MethodDetails.py`** — should be derived from data:
50
- `sorted(slice_df["rmse_target"].astype(str).unique().tolist())`. A hardcoded list
51
- silently hides any target present in the data but absent from the list.
52
- - [ ] **`method_meta` entries with placeholder citations or `example.com` URLs** —
53
- grep for `example.com` and `placeholder` / `citation to be added` in `MethodDetails.py`.
54
- See `references/populate-metadata.md` for confirmed citations.
55
-
56
- ### Dead code
57
-
58
- - [ ] **Unused columns written to `merged` in `_load_store`** — e.g. any column created
59
- but never read downstream. Check columns added after the `dropna` step in `data_store.py`.
60
- - [ ] **Session-state guards that are always `True`** — e.g.
61
- `if st.session_state.get("show_home", True):` where nothing ever sets the key to `False`.
62
- These wrap live code in an unreachable conditional.
63
- - [ ] **Dead functions** — any function in `data_store.py` or elsewhere that is defined but
64
- never called. Check for `def build_*` or similar remnants of superseded pipelines.
65
-
66
- ### Sidebar nav consistency
67
-
68
- - [ ] **All four files have identical sidebar `page_link` blocks** — the block is
69
- copy-pasted in `streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`.
70
- Confirm they are identical (same links, same icons, same order).
71
-
72
- ---
73
-
74
- ## Reporting format
75
-
76
- Group findings by severity:
77
-
78
- - **Bug (functional impact)** — something that causes incorrect output, a runtime error,
79
- or silently hides data (e.g. hardcoded targets, invalid Streamlit API).
80
- - **Staleness** — registry entries, metadata, or cached snapshots that no longer match
81
- current code or data (e.g. placeholder citations, phantom method entries).
82
- - **Dead code** — functions, columns, or conditionals that do nothing.
83
- - **Informational** — minor inconsistencies that don't affect correctness.
84
-
85
- For each finding, include: file, line number(s), description, and recommended fix.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/references/manage-display.md DELETED
@@ -1,368 +0,0 @@
1
- # Manage Display — Table, Chart, and Scoring
2
-
3
- This file covers every display surface in the leaderboard dashboard and how to change it.
4
- The key architectural fact: **both the Optimization and UQ leaderboards share a single
5
- renderer** (`render_leaderboard()` in `src/common/leaderboard.py`). Any change to that
6
- function affects both boards simultaneously. Method-specific charts live in
7
- `src/pages/MethodDetails.py`.
8
-
9
- **A note on line numbers:** this file used to cite specific line numbers (e.g. "line 296").
10
- They drift out of date after almost any edit to `leaderboard.py` — a single reordering
11
- task can shift everything below it by 50+ lines. Below, locations are given as
12
- **function/variable/string anchors** you can `grep` for instead — these survive
13
- refactors. If you add a new durable line-number reference anyway, expect to fix it again
14
- next time someone edits the file.
15
-
16
- ---
17
-
18
- ## Page layout (top to bottom)
19
-
20
- `render_leaderboard()` renders, in order:
21
-
22
- 1. Suitability table (`_render_suitability_table()`)
23
- 2. Benchmark selector (`st.selectbox("Benchmark", ...)`) — always a single benchmark;
24
- there is **no "All benchmarks" option**. Every chart and the table below operate on
25
- whatever `selected_benchmark` is currently chosen.
26
- 3. **Controls — always visible**, not collapsed. Look for `st.subheader("Scoring &
27
- Target Controls")`: target-level radio, scoring-mode radio (+ blend-weight slider
28
- when "Custom Blend" is selected), and the "Methods to display in charts" multiselect.
29
- These used to be wrapped in `st.expander(..., expanded=False)`; that was removed so
30
- users don't have to open a dropdown to see or change the target level.
31
- 4. **Charts** — main "Mean Forward Model Runs vs Ensemble Size" chart, then (UQ only)
32
- the "Mean Iterations for Coverage" chart, then (if `show_failure_panel=True`) the
33
- failure-rate bar chart.
34
- 5. **Leaderboard table** — `st.dataframe(leaderboard_df, ...)`.
35
-
36
- Charts render above the table and below the controls. If you need to reorder these
37
- sections again, see the gotcha below before moving the main chart.
38
-
39
- ### Gotcha: the main chart is gated by `leaderboard_df`, not by its own emptiness check
40
-
41
- The main forward-model-runs chart is wrapped in `if not leaderboard_df.empty:` — the
42
- same DataFrame the table renders from — rather than an independent check on its own
43
- `chart_df`/`fail_df`. This is deliberate: `leaderboard_df` (from `build_scored_table()`)
44
- is empty in two distinct cases:
45
-
46
- - No rows at all for the current benchmark/target (nothing was ever run).
47
- - Every seed failed (`metric` is NaN for all rows) — a real "DNF" case.
48
-
49
- In the DNF case, the chart is intentionally suppressed too — the user sees a warning
50
- ("All runs failed...") instead of a near-empty line chart with only fail-crosses at
51
- y=0. If you're adding a new chart section anywhere near the main chart, decide
52
- deliberately whether it should share this gate or have its own (the UQ iters chart and
53
- the failure-rate chart do **not** share it — they have their own internal emptiness
54
- checks, e.g. `if not all_ens_combos_iters.empty:`, so they can render even when the
55
- scored table is empty).
56
-
57
- ---
58
-
59
- ## Leaderboard table
60
-
61
- ### Column visibility and order
62
-
63
- Search for `table_column_order = [` in `src/common/leaderboard.py` — there is a single
64
- list (not one-per-view):
65
- ```python
66
- table_column_order = [
67
- "Placement", "abbreviation", "Method",
68
- "update_type", "parallelism", "method_goal", "emulator_use", "Score",
69
- "Mean Forward Model Runs", "Minimum Forward Model Runs",
70
- "Mean Failure Rate (%)", "Optimal Ensemble Size", "Ensemble Sizes Used",
71
- ]
72
- ```
73
- To **show or hide** a column, add/remove it from this list.
74
- To **reorder** columns, rearrange entries within the list.
75
-
76
- ### Column labels and formatting
77
-
78
- Column display names and number formats are set in the `st.dataframe(... column_config=...)`
79
- call right after `table_column_order` is used. Each column has an entry like:
80
-
81
- ```python
82
- "Mean Forward Model Runs": st.column_config.NumberColumn(
83
- "Mean Forward Model Runs", format="%.4f"
84
- ),
85
- "Score": st.column_config.ProgressColumn(
86
- "Score (0–100)", min_value=0.0, max_value=100.0, format="%.1f"
87
- ),
88
- ```
89
-
90
- To **rename** a displayed column header, change the string argument (second positional arg)
91
- in the `column_config` entry. The dict key must still match the DataFrame column name.
92
-
93
- To **add a new column to the table**, you also need to produce it in `build_scored_table()`
94
- — add it to a `.agg()` call or compute it after the groupby, then add a `column_config`
95
- entry and include it in `table_column_order`.
96
-
97
- ### Adding `failure_rate` to the leaderboard table
98
-
99
- `failure_rate` is available in `metric_store` but not surfaced in the scored table.
100
- To add it:
101
- 1. In `build_scored_table()`, add it to the `.groupby(...).agg(...)` call, e.g.:
102
- `**{"Mean Failure Rate (%)": ("failure_rate", "mean")}`.
103
- 2. Add a `column_config` entry (e.g. `st.column_config.NumberColumn(..., format="%.1f")`).
104
- 3. Add `"Mean Failure Rate (%)"` to `table_column_order` where desired.
105
-
106
- ---
107
-
108
- ## Scoring modes
109
-
110
- The four scoring modes are defined in `scoring_options` (a list literal near the top of
111
- `render_leaderboard()`, right after `target_options` is computed):
112
- ```python
113
- scoring_options = [
114
- "Mean Forward Model Runs",
115
- "Minimum Forward Model Runs",
116
- "Smallest Optimal Ensemble Size",
117
- "Custom Blend",
118
- ]
119
- ```
120
-
121
- Each mode maps to a sort key and a score column assignment inside `build_scored_table()`
122
- — search for the `if scoring_mode == "Mean Forward Model Runs": ... elif ...` chain. To
123
- **add a new scoring mode**, add its name to `scoring_options` and add an `elif` branch in
124
- that chain that sets `scored_df["Score"]`, `sort_columns`, and `ascending`.
125
-
126
- The "Custom Blend" slider (`st.slider("Blend Weight: Forward Runs vs Ensemble Size", ...)`)
127
- is keyed to the session-state key `k_weight` and only rendered when
128
- `st.session_state[k_scoring] == "Custom Blend"` — no change needed when adding a
129
- non-blend mode. It lives in the always-visible controls section, not behind an expander.
130
-
131
- ---
132
-
133
- ## Leaderboard line chart
134
-
135
- The chart that appears above the table ("Mean Forward Model Runs vs Ensemble Size") is
136
- built right after `st.subheader("Mean Forward Model Runs vs Ensemble Size")`, inside the
137
- `if not leaderboard_df.empty:` guard described in the gotcha above:
138
-
139
- ```python
140
- ens_ticks = sorted(all_ens_combos["ensemble_size"].unique().tolist()) if not all_ens_combos.empty else []
141
- main_color = _method_color(all_ens_combos["abbreviation"].unique().tolist())
142
- chart = (
143
- alt.Chart(chart_df)
144
- .mark_line(point=True)
145
- .encode(
146
- x=alt.X(
147
- "ensemble_size:Q",
148
- title="Ensemble Size",
149
- axis=alt.Axis(values=ens_ticks, format="d"), # integer ticks only
150
- ),
151
- y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
152
- color=main_color,
153
- tooltip=["abbreviation", "ensemble_size",
154
- alt.Tooltip("mean_forward_runs:Q", format=".4f")],
155
- )
156
- )
157
- st.altair_chart(chart, use_container_width=True)
158
- ```
159
-
160
- The `axis=alt.Axis(values=ens_ticks, format="d")` pattern is important: without it,
161
- Altair may interpolate non-integer ticks (e.g. 5.5, 10.5) on the ensemble-size axis.
162
- Always derive `ens_ticks` from the actual data rather than hardcoding.
163
-
164
- Common changes:
165
- - **Log scale on Y**: add `scale=alt.Scale(type="log")` inside `alt.Y(...)`.
166
- - **Add failure rate as a second Y-axis or tooltip**: add `failure_rate` to `chart_df`
167
- groupby and include it in `tooltip`.
168
- - **Change mark type** (e.g. bars): replace `mark_line(point=True)` with `mark_bar()`.
169
- - **Restrict the legend to the methods actually plotted**: use `_method_color(...)` — see
170
- "Method color legends" below. Do **not** reach for `.configure_range(category={"scheme":
171
- ...})`; that overrides the whole palette rather than filtering the legend, and breaks
172
- the per-method color stability the app relies on.
173
-
174
- There is no `if selected_benchmark != "All":` guard — a benchmark is always selected, so
175
- this chart renders whenever there's at least one non-DNF row for it (see the gotcha above
176
- for the exact condition).
177
-
178
- ---
179
-
180
- ## Method Details page surfaces
181
-
182
- The per-method page (`src/pages/MethodDetails.py`) has its own controls.
183
-
184
- ### Target level radio
185
-
186
- Search for `target_options = ["1.0", "1.1", "1.2"]` (a hardcoded list). If the benchmark
187
- gains new target levels, update this list. A more robust approach is to derive them from
188
- the data:
189
- ```python
190
- target_options = sorted(metric_store["rmse_target"].astype(str).unique().tolist())
191
- ```
192
-
193
- ### Per-method special-case charts
194
-
195
- The HM (History Matching) failure-analysis chart (search for `if sel == "HM":`) is the
196
- canonical example of a per-method detail chart:
197
-
198
- ```python
199
- if sel == "HM":
200
- st.markdown("### Failure Analysis")
201
- chart = (alt.Chart(best_table_view).mark_bar().encode(...))
202
- st.altair_chart(chart, use_container_width=True)
203
- ```
204
-
205
- To **add a chart for another method**, add an `elif sel == "ABBR":` block immediately
206
- after the `if sel == "HM":` block, following the same pattern.
207
-
208
- To **generalize** (show failure rate for all methods when non-zero), replace the `if sel
209
- == "HM":` guard with a condition on the data, e.g.:
210
- ```python
211
- if best_table_view["failure_rate"].max() > 0:
212
- # render the failure chart for any method that has failures
213
- ```
214
-
215
- ### Scaling chart (ensemble size vs forward-model runs per benchmark)
216
-
217
- The `"Scaling by Benchmark"` chart is already generic across all methods. To modify
218
- encoding, change the `.encode()` arguments analogously to the leaderboard chart above.
219
-
220
- ---
221
-
222
- ## UQ-specific charts (`budget_store` parameter)
223
-
224
- The UQ leaderboard passes a `budget_store` DataFrame to `render_leaderboard()` via the
225
- `budget_store` keyword argument. This enables two additional chart sections rendered in
226
- `src/common/leaderboard.py`, both **after** the main chart and **before** the leaderboard
227
- table:
228
-
229
- 1. **"Mean Iterations for Coverage vs Ensemble Size"** — line chart of `mean_iters` vs
230
- ensemble size (`if budget_store is not None and not budget_store.empty:`). Uses the
231
- same integer-tick pattern as the main chart. This section has its own emptiness check
232
- (`if not all_ens_combos_iters.empty:`) — it is independent of `leaderboard_df`, so it
233
- can render even when the scored table/main chart are suppressed by an all-failed
234
- selection.
235
-
236
- 2. **"Failure Rate of Hitting Target ..."** — grouped bar chart (`if show_failure_panel:`)
237
- with `ensemble_size:O` on the x-axis (ordinal, not quantitative) and
238
- `mean_failure_rate:Q` on y. The title reflects the currently selected target level.
239
- Also independent of `leaderboard_df`.
240
-
241
- To **add or modify a UQ chart**, edit the `if budget_store is not None` block. The
242
- `budget_store` DataFrame has columns: `benchmark`, `algorithm_type`, `abbreviation`,
243
- `parallelism`, `update_type`, `method_goal`, `emulator_use`, `uq_target`, `ensemble_size`,
244
- `mean_budget`, `mean_iters`, `failure_count`, `failure_rate`, `n_seeds`.
245
-
246
- **Bar chart integer x-axis:** Use `ensemble_size:O` (ordinal) with an explicit sort list
247
- and `axis=alt.Axis(labelAngle=0)` to suppress diagonal labels:
248
- ```python
249
- ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
250
- x=alt.X(
251
- "ensemble_size:O",
252
- title="Ensemble Size",
253
- sort=[str(e) for e in ens_ticks_fail],
254
- axis=alt.Axis(labelAngle=0),
255
- )
256
- ```
257
-
258
- ---
259
-
260
- ## Plotting conventions and shared display constants
261
-
262
- ### Where shared constants live
263
-
264
- `src/common/method_registry.py` is the single source of truth for any display property
265
- that must be consistent across multiple pages or charts. Currently it exports:
266
-
267
- - **`METHOD_COLORS: dict[str, str]`** — abbreviation → hex color for every known method,
268
- derived from `KNOWN_METHODS` in declaration order.
269
- - **`_METHOD_PALETTE`** — the 10-slot Vega tableau10 palette that backs `METHOD_COLORS`.
270
-
271
- **Rule:** if a constant is used in more than one file, it belongs in `method_registry.py`,
272
- not defined locally in `leaderboard.py` or a page file.
273
-
274
- When **adding a new method**, its color is assigned automatically — no manual update
275
- needed, as long as it is appended to the end of `KNOWN_METHODS`. The palette cycles every
276
- 10 methods.
277
-
278
- ### Method color legends: scale stability vs. legend filtering
279
-
280
- `src/common/leaderboard.py` defines a `_method_color(present_abbrevs: list[str])` helper
281
- — **use it (or its pattern) for every method-colored chart**, rather than building
282
- `alt.Color(...)` by hand:
283
-
284
- ```python
285
- def _method_color(present_abbrevs: list[str]) -> alt.Color:
286
- return alt.Color(
287
- "abbreviation:N",
288
- title="Method",
289
- scale=alt.Scale(domain=list(METHOD_COLORS.keys()), range=list(METHOD_COLORS.values())),
290
- legend=alt.Legend(values=sorted(present_abbrevs)),
291
- )
292
- ```
293
-
294
- This encodes two *separate* requirements that are easy to conflate:
295
-
296
- - **The `scale` domain/range must always be the full registry** (`METHOD_COLORS.keys()`
297
- / `.values()`), never a filtered subset. This is what keeps a given method's color
298
- stable across every chart, benchmark, and page — if you filtered the scale domain to
299
- "just the methods in this chart," a method could get a different color depending on
300
- which benchmark happens to be selected.
301
- - **The `legend` should be filtered to just the methods present in *this* chart's data**
302
- (`legend=alt.Legend(values=...)`). Without this, Vega-Lite lists every method in the
303
- domain in the legend regardless of whether it appears in the plotted data — on a
304
- benchmark with 3 methods, the legend would still show all 9+ registered methods.
305
-
306
- When calling `_method_color()`, compute `present_abbrevs` from the actual DataFrame(s)
307
- plotted in that chart section (e.g. `all_ens_combos["abbreviation"].unique()`), **not**
308
- from the global "Methods to display in charts" multiselect. A method can be selected in
309
- the multiselect but have no data for the current benchmark/target combination — including
310
- it in the legend anyway would be misleading. Each chart section (main chart, UQ iters
311
- chart, failure-rate chart) computes and passes its own `present_abbrevs`, since the set
312
- of methods with data can differ section to section (e.g. a method might have forward-run
313
- data but no coverage/iters data).
314
-
315
- If a layered chart (`alt.layer(...)`) has multiple encode calls sharing one color field
316
- (e.g. the main chart's success-line layer, fail-cross layer, and single-ensemble rule
317
- layer), pass the **same** `_method_color(...)` result (same `present_abbrevs`) to all of
318
- them — Vega-Lite needs matching legend/scale specs across layers to merge them into one
319
- legend instead of drawing duplicates.
320
-
321
- ### How to import shared colors in a new page/file
322
-
323
- ```python
324
- try:
325
- from common.method_registry import METHOD_COLORS
326
- except ModuleNotFoundError:
327
- from src.common.method_registry import METHOD_COLORS
328
- ```
329
-
330
- `_method_color()` itself currently lives in `leaderboard.py` (not `method_registry.py`)
331
- and is private (`_`-prefixed). If a future page outside `leaderboard.py` needs
332
- method-colored charts with the same scale-stable/legend-filtered behavior, promote
333
- `_method_color()` to `method_registry.py` (dropping the underscore) rather than
334
- duplicating the function — don't hand-roll a second copy.
335
-
336
- ### General plotting conventions
337
-
338
- Unless the task explicitly asks for something different, follow these defaults for every
339
- new Altair chart in the app:
340
-
341
- | Concern | Convention |
342
- |---|---|
343
- | Method color | `_method_color(present_abbrevs)` (see above) — never a local palette or an unfiltered legend |
344
- | Benchmark color | `alt.Color("benchmark:N")` — Altair assigns automatically; no custom palette needed |
345
- | Full-width charts | `st.altair_chart(chart, use_container_width=True)` — never `width="stretch"` |
346
- | Ensemble-size x-axis (quantitative) | `alt.Axis(values=ens_ticks, format="d")` — derive ticks from data, integer format |
347
- | Ensemble-size x-axis (ordinal bar) | `ensemble_size:O` with `sort=[str(e) for e in ens_ticks]` and `axis=alt.Axis(labelAngle=0)` |
348
- | Y-axis scale | Linear by default; add `scale=alt.Scale(type="log")` only when explicitly requested |
349
- | Tooltips | Always include `abbreviation`, `ensemble_size`, and the primary metric with `format=".4f"` |
350
- | 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 |
351
-
352
- ---
353
-
354
- ## Display change checklist
355
-
356
- 1. Edit the relevant file (`leaderboard.py` for both leaderboards; `MethodDetails.py` for
357
- per-method surfaces).
358
- 2. If adding a column: produce it in the aggregation AND add a `column_config` entry AND
359
- add it to `table_column_order`.
360
- 3. If adding or reordering a chart section: check whether it should share the
361
- `leaderboard_df`-emptiness gate (main chart) or have its own independent emptiness
362
- check (UQ iters chart, failure panel) — see the gotcha under "Page layout" above.
363
- 4. If the chart colors by `abbreviation`: use `_method_color(present_abbrevs)` computed
364
- from that section's actual plotted data, not the global multiselect.
365
- 5. Restart or clear Streamlit cache to pick up Python changes (code changes take effect on
366
- restart; cache only matters for data changes).
367
- 6. Smoke-test with a benchmark/target combination where every run fails (all-DNF) to
368
- confirm the warning/chart-suppression behavior still matches what you intended.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/references/move-between-boards.md DELETED
@@ -1,116 +0,0 @@
1
- # Move or Duplicate a Method Between Leaderboards
2
-
3
- This file covers wiring a method's results onto the UQ leaderboard, or removing it from
4
- one board while keeping it on the other. The key insight is that **the method registry
5
- and the MethodDetails metadata are shared** — you never need to touch `method_registry.py`
6
- or `MethodDetails.py` for this task. It is purely a **data registration** operation.
7
-
8
- ---
9
-
10
- ## How the two boards relate
11
-
12
- ```
13
- method_registry.py ← shared; controls identity on BOTH boards
14
- pages/MethodDetails.py ← shared; controls the Methods page for BOTH boards
15
- data_store.py
16
- DATASET_FILES ← Optimization board only (rmse_target, metric variable)
17
- UQ_BUDGET_FILES ← UQ board only (output_coverage variable, budget pipeline)
18
- ```
19
-
20
- A method is "on" a leaderboard if and only if a NetCDF containing its `algorithm_type`
21
- is registered in the corresponding data dict.
22
-
23
- **Critical difference:** the two boards use entirely different NetCDF schemas and loading
24
- pipelines. Do not try to use the same NC file for both boards.
25
-
26
- ---
27
-
28
- ## UQ vs optimization NC schemas
29
-
30
- | Aspect | Optimization (`DATASET_FILES`) | UQ (`UQ_BUDGET_FILES`) |
31
- |---|---|---|
32
- | Key variable | `metric` (pre-computed runs) | `output_coverage` (coverage fractions) |
33
- | Target coord | `rmse_target` dimension | `target_scaling` coordinate (not a dim) |
34
- | Failure encoding | `metric == -1` | NaN (never reached) |
35
- | Loader | `_load_store()` | `load_uq_budget_store()` computes budget on the fly |
36
-
37
- For the full UQ NC schema, see `references/add-uq-data.md`.
38
-
39
- ---
40
-
41
- ## Duplicate a method (appear on both boards)
42
-
43
- "Duplicate" means the method shows up on the Optimization **and** UQ leaderboards.
44
-
45
- 1. Obtain (or produce) a UQ-schema NC file for the method (must have `output_coverage`,
46
- `coverage_quantile`, `k_iter`, `ensemble_size`, `random_seed`, `target_scaling`,
47
- `output_dim` — see `references/add-uq-data.md` for the full spec).
48
- 2. Place it in `data/` (e.g. `data/method_results/method_l63_ensemble_results.nc`).
49
- 3. Add the path to `UQ_BUDGET_FILES` in `src/data_store.py`:
50
-
51
- ```python
52
- UQ_BUDGET_FILES: dict[str, list[tuple[str, str]]] = {
53
- "L63": [
54
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15.nc"),
55
- ("new-method", "new-method_results/new_method_l63.nc"), # ← add here
56
- ],
57
- ...
58
- }
59
- ```
60
-
61
- The method's optimization results in `DATASET_FILES` remain untouched.
62
- No changes to `method_registry.py`, `MethodDetails.py`, or `UQLeaderboard.py` are needed.
63
-
64
- **Clear the Streamlit cache** after editing `data_store.py` — all three loaders are cached.
65
-
66
- ---
67
-
68
- ## Move a method (UQ only, remove from optimization)
69
-
70
- "Move" means the method should appear on the UQ leaderboard **and not** the Optimization
71
- leaderboard.
72
-
73
- 1. Complete the duplicate steps above (register UQ NC in `UQ_BUDGET_FILES`).
74
- 2. Remove the method's path(s) from `DATASET_FILES` in `src/data_store.py`.
75
-
76
- The registry entry and `method_meta` in `MethodDetails.py` stay — the Methods page still
77
- shows the method, but performance data comes only from `load_uq_store()`. The method won't
78
- appear in the optimization leaderboard table.
79
-
80
- ---
81
-
82
- ## Populate the UQ leaderboard for a new benchmark (first time)
83
-
84
- When a benchmark key is absent from `UQ_BUDGET_FILES`, that benchmark has no UQ data.
85
- Add the key and the NC file tuple — see the "Duplicate" section above. The UQ page calls
86
- `render_leaderboard(load_uq_store(), ...)` which automatically includes all registered
87
- benchmarks; no page-level changes needed.
88
-
89
- ---
90
-
91
- ## Verify
92
-
93
- After editing `UQ_BUDGET_FILES` and clearing the cache:
94
-
95
- 1. Visit the **UQ Leaderboard** page (`🎯 UQ Leaderboard` in the sidebar).
96
- 2. Confirm the method appears in the ranked table for the benchmark you registered.
97
- 3. Check `.cache/known_methods_snapshot.json` → `unmapped_observed_methods` is empty for
98
- the new method.
99
- 4. Expect 100% failure at tight c values (e.g. c=1.0) for benchmarks with small N_y —
100
- see `references/add-uq-data.md` for the coverage-coarseness explanation.
101
-
102
- If the UQ leaderboard still shows "No metric data found" after clearing cache, confirm:
103
- - The NC path in `UQ_BUDGET_FILES` is correct relative to `data/`.
104
- - The file has been `git lfs pull`-ed (not a 130-byte pointer stub).
105
- - `output_coverage` is present in the file and the `coverage_quantile` coordinate
106
- includes 0.15, 0.5, and 0.85.
107
-
108
- ---
109
-
110
- ## What does NOT need changing
111
-
112
- - `src/common/leaderboard.py` — fully generic; handles both leaderboards via parameters.
113
- - `src/pages/UQLeaderboard.py` — passes `budget_store=load_uq_budget_store()` already.
114
- - `src/common/method_registry.py` — no change if the method is already registered.
115
- - `src/pages/MethodDetails.py` — no change; the Methods page is shared.
116
- - The five sidebar nav blocks — no new pages are being added.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.claude/skills/leaderboard-manager/references/populate-metadata.md DELETED
@@ -1,150 +0,0 @@
1
- # Populate Method and Data Metadata
2
-
3
- This file covers filling in or correcting text that appears about a method in the
4
- dashboard — citations, reference URLs, and summary descriptions — as well as understanding
5
- where benchmark (dataset) labels come from.
6
-
7
- ---
8
-
9
- ## EKP documentation — fetch warning
10
-
11
- The canonical citation source for Kalman-family methods is:
12
- `https://clima.github.io/EnsembleKalmanProcesses.jl/dev/`
13
-
14
- **This URL frequently times out in WebFetch.** If it does, fall back to the confirmed
15
- citations below rather than retrying repeatedly.
16
-
17
- ### Confirmed citations (verified in prior sessions)
18
-
19
- | Abbreviation | Citation | DOI / URL |
20
- |---|---|---|
21
- | TEKI | Chada et al., SIAM/ASA J. UQ, 2020 | https://doi.org/10.1137/17M114402X |
22
- | IEKF | Iglesias, Inverse Problems, 2016 | https://doi.org/10.1088/0266-5611/32/2/025002 |
23
- | CES-EKI-DMC | Cleary et al., J. Comput. Phys., 2021 | https://doi.org/10.1016/j.jcp.2020.109716 |
24
- | ETKI | Schillings & Stuart, Numer. Math., 2017 | link to EKP docs (sub-page DOI unconfirmed) |
25
- | UKI | Huang, Huang & Stuart, Physica D, 2022 | link to EKP docs (sub-page DOI unconfirmed) |
26
- | HM | Williamson et al. 2013; King et al. 2025 | — |
27
-
28
- Use `https://clima.github.io/EnsembleKalmanProcesses.jl/dev/` as the URL for ETKI and UKI
29
- until their sub-page DOIs are confirmed.
30
-
31
- ---
32
-
33
- ---
34
-
35
- ## The metadata split
36
-
37
- Method information lives in **two separate places** that serve different purposes:
38
-
39
- | File | Key used | What it stores |
40
- |---|---|---|
41
- | `src/common/method_registry.py` | canonical key (e.g. `"uki"`) | Abbreviation, full method name, taxonomy tags (parallelism, update_type, method_goal, emulator_use), aliases — used for **data processing and identity** |
42
- | `src/pages/MethodDetails.py` `method_meta` dict | abbreviation (e.g. `"UKI"`) | Citation, URL, summary — used for **display copy only** |
43
-
44
- Both must be kept in sync. When you edit one, check the other. The most common mistake is
45
- adding a method to `method_registry.py` without adding a corresponding `method_meta`
46
- entry — the app won't crash, but the Methods page will show "Citation pending" and a
47
- broken link.
48
-
49
- ---
50
-
51
- ## Editing method display copy
52
-
53
- Open `src/pages/MethodDetails.py`. The `method_meta` dict starts at line 40:
54
-
55
- ```python
56
- 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
- }
64
- ```
65
-
66
- **To update a citation or URL**, find the entry by abbreviation and edit the values.
67
- **To add a missing entry**, add a new key matching the `abbreviation` in `method_registry.py`.
68
-
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
-
79
- ## Remaining placeholder backlog
80
-
81
- Most entries have been filled in. The following still need real values:
82
-
83
- | Abbreviation | Field | Status |
84
- |---|---|---|
85
- | `ABC` | url | `"https://example.com/abc"` — no canonical URL identified yet |
86
- | `HM` | url | `"https://example.com/hm"` — no canonical URL identified yet |
87
- | `ETKI` | url | Points to EKP docs index — sub-page DOI not yet confirmed |
88
- | `UKI` | url | Points to EKP docs index — sub-page DOI not yet confirmed |
89
-
90
- When filling in `ABC` or `HM` URLs, check with the collaborator for the preferred
91
- reference (History Matching has many implementations; the right URL depends on which
92
- variant is used here).
93
-
94
- ---
95
-
96
- ## Editing method identity / naming
97
-
98
- If you need to correct the full method name, taxonomy tags, or abbreviation (the
99
- short label that appears in table columns), edit `KNOWN_METHODS` in
100
- `src/common/method_registry.py`:
101
-
102
- ```python
103
- "uki": {
104
- "abbreviation": "UKI", # Short label in table columns
105
- "Method": "Unscented Kalman Inversion", # Full name in Method column
106
- "parallelism": "parallel-interacting", # serial | parallel-independent | parallel-interacting
107
- "update_type": "kalman", # gradient | kalman | general
108
- "method_goal": "optimization", # optimization | uq
109
- "emulator_use": "none", # none | within-optimize | after-optimize
110
- "aliases": ["uki"], # Raw algorithm_type strings from NetCDF
111
- },
112
- ```
113
-
114
- **If you change `abbreviation`**: also update the corresponding key in `method_meta` in
115
- `MethodDetails.py` (the keys must match). The app looks up `method_meta[abbreviation]`.
116
-
117
- **If you change any taxonomy tag**: the corresponding column in the leaderboard table
118
- updates automatically — no other change needed. Changing `update_type` also shifts that
119
- method's group in the sort order (see `_UPDATE_TYPE_ORDER` in `manage-display.md`).
120
-
121
- **If you add an alias**: new alias strings start matching immediately after reload. Use
122
- this when a collaborator's NetCDF uses a variant spelling not currently in the alias list.
123
-
124
- ---
125
-
126
- ## Benchmark / dataset labels
127
-
128
- Benchmark names (`L63`, `L96`, `L96_NN_FORCING`, `L96_SPATIAL_FORCING`) come from the
129
- keys of `DATASET_FILES` in `src/data_store.py`. They appear:
130
- - In the "Benchmark" selectbox on both leaderboards.
131
- - In the `benchmark` column of every row in `metric_store`.
132
- - As chart axis labels on the Methods detail page ("Scaling by Benchmark").
133
-
134
- To **rename a benchmark label**, change its key in `DATASET_FILES` (and `UQ_DATASET_FILES`
135
- if present). The label propagates everywhere automatically because it's read from the data
136
- at runtime. There is no separate label registry.
137
-
138
- To **add a new benchmark**, see `add-method.md` → Step 3 (the same pattern applies even
139
- without a new method — just add a new benchmark key with its file list).
140
-
141
- ---
142
-
143
- ## Verify
144
-
145
- After editing metadata:
146
- 1. Restart the app (code changes don't require cache clearing, only data changes do).
147
- 2. Open the `📘 Methods` page and select the edited method.
148
- 3. Confirm the citation text, URL button target, and summary box show the new values.
149
- 4. Check no `method_meta` key is misspelled (the abbreviation in `method_meta` must match
150
- `KNOWN_METHODS[...]["abbreviation"]` exactly, including capitalisation).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitattributes CHANGED
@@ -34,4 +34,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.nc filter=lfs diff=lfs merge=lfs -text
37
- *.png filter=lfs diff=lfs merge=lfs -text
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.nc filter=lfs diff=lfs merge=lfs -text
 
.gitignore CHANGED
@@ -1,34 +1,3 @@
1
- # Emacs
2
- *~
3
- \#*\#
4
- .\#*
5
- *.elc
6
-
7
- # Python
8
- *.py[cod]
9
- *.so
10
- __pycache__/
11
- *.egg-info/
12
- .eggs/
13
- dist/
14
- build/
15
- .pytest_cache/
16
- .mypy_cache/
17
- .ruff_cache/
18
-
19
- # Virtual environments
20
- .venv/
21
- venv/
22
- env/
23
-
24
- # Streamlit
25
- .streamlit/secrets.toml
26
-
27
- # App cache
28
- .cache/
29
-
30
- # Jupyter
31
- .ipynb_checkpoints/
32
-
33
- # OS
34
- .DS_Store
 
1
+ *.pyc
2
+ .venv
3
+ __pycache__
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CLAUDE.md DELETED
@@ -1,254 +0,0 @@
1
- # Calibration Benchmark
2
-
3
- ## Project
4
- Streamlit dashboard hosting a **calibration / UQ leaderboard**. Compares parameter-calibration
5
- methods on Lorenz-63/96 dynamical-system benchmarks by ranking them on *forward-model run*
6
- efficiency (cost to hit a target across ensemble sizes). Two leaderboards:
7
-
8
- - **Optimization leaderboard** (home page) — race to reach an RMSE target.
9
- - **UQ leaderboard** (`pages/UQLeaderboard.py`) — budget-for-coverage metric: N_ens·k_iter to
10
- satisfy a marginal-coverage condition at quantiles [0.15, 0.5, 0.85] for a given tolerance
11
- scaling c. Data comes from ensemble-results NetCDF files registered in `UQ_BUDGET_FILES`.
12
-
13
- Deployed as a Hugging Face Space via Docker (port 8501).
14
-
15
- **Key metric (optimization):** `metric` = mean forward-model runs to reach an RMSE target
16
- (lower is better). `metric == -1` encodes a failed run.
17
-
18
- **Key metric (UQ):** `metric` = mean budget N_ens·k_iter to reach marginal-coverage
19
- tolerance `c·√(q(1−q)/N_y)` for all q ∈ [0.15, 0.5, 0.85] simultaneously (lower = better).
20
- `uq_target` = the scaling c ∈ {1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5}. NaN encodes failure
21
- (target never reached within the k_iter range).
22
-
23
- ## Quick start
24
-
25
- ```sh
26
- # First-time setup
27
- python3.11 -m venv .venv
28
- source .venv/bin/activate
29
- pip install -r requirements.txt
30
- git lfs pull # required — data/*.nc are LFS pointers; app shows nothing without this
31
-
32
- # Run
33
- streamlit run src/streamlit_app.py
34
- ```
35
-
36
- Docker:
37
- ```sh
38
- docker build -t calibration-benchmark .
39
- docker run -p 8501:8501 calibration-benchmark # → http://localhost:8501
40
- ```
41
-
42
- ## File map
43
-
44
- ```
45
- calibration_benchmark/
46
- ├── src/
47
- │ ├── streamlit_app.py Entrypoint + Home/"Optimization Leaderboard"
48
- │ ├── data_store.py load_metric_store() / load_uq_store() / load_uq_budget_store()
49
- │ │ → @st.cache_data DataFrames; DATASET_FILES, UQ_BUDGET_FILES
50
- │ │ → BENCHMARK_DIMS — physical (param, state, output) dims per benchmark
51
- │ ├── common/
52
- │ │ ├── leaderboard.py render_leaderboard() — shared scored-table + chart pipeline
53
- │ │ │ _render_suitability_table() — method × benchmark suitability grid
54
- │ │ │ optional budget_store param adds budget/iters chart section
55
- │ │ └── method_registry.py KNOWN_METHODS registry + name canonicalization
56
- │ └── pages/
57
- │ ├── UQLeaderboard.py 🎯 UQ Leaderboard — calls render_leaderboard(load_uq_store())
58
- │ ├── MethodDetails.py 📘 Methods — per-method charts + metadata
59
- │ └── RawData.py 🧾 Get Data — raw table + CSV export
60
- ├── data/ NetCDF result files (git-LFS — run `git lfs pull`)
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
- │ │ filenames follow `leaderboard_adam_<benchmark-tag>_<date>.nc`;
66
- │ │ benchmark tags: `l63` → L63, `l96_const-force` → L96,
67
- │ │ `l96_vec-force` → L96_SPATIAL_FORCING (no L96_NN_FORCING yet)
68
- │ ├── levenberg_marquardt_results/ LM gradient optimizer results
69
- │ │ filenames follow `leaderboard_lm_<benchmark-tag>_<date>.nc`;
70
- │ │ same benchmark tags as adam_results/ (no L96_NN_FORCING yet)
71
- │ ├── ces-eki-dmc_results/ CES-EKI-DMC ensemble-results files (UQ source)
72
- │ │ filenames follow `ces-eki-dmc_<benchmark-tag>_ensemble_results_<date>[_minimal].nc`;
73
- │ │ benchmark tags: `l63` → L63, `l96` (no suffix) → L96,
74
- │ │ `l96_nn_forcing` → L96_NN_FORCING, `l96_spatial_forcing` → L96_SPATIAL_FORCING
75
- │ └── gnki-uq_results/ IEKF (a.k.a GNKI) ensemble-results files (UQ source)
76
- │ filenames follow `leaderboard_gnki_<benchmark-tag>_<date>[_minimal].nc`;
77
- │ benchmark tags: `l63` → L63, `l96_const-force` → L96,
78
- │ `l96_flux-force` → L96_NN_FORCING, `l96_vec-force` → L96_SPATIAL_FORCING
79
- ├── examples/ lorenz_demo.ipynb + lorenz.py (reference, not app code)
80
- ├── From_Rob_6-10/ Raw collaborator data drop — NOT read by the app
81
- ├── .cache/ known_methods_snapshot.json (auto-generated, safe to delete)
82
- ├── .streamlit/config.toml showSidebarNavigation = false
83
- ├── Dockerfile python:3.13.5-slim; ENTRYPOINT streamlit run src/...
84
- ├── requirements.txt altair, pandas, streamlit, xarray, netcdf4, h5netcdf, jax, gpjax
85
- ├── test_parse.py Ad-hoc NetCDF parse check (not a test suite)
86
- └── test_failures.py Ad-hoc failure-count check (not a test suite)
87
- ```
88
-
89
- ## Data model
90
-
91
- ### Optimization store (`load_metric_store()`)
92
-
93
- | Field | Meaning |
94
- |---|---|
95
- | `benchmark` | `L63`, `L96`, `L96_NN_FORCING`, `L96_SPATIAL_FORCING` |
96
- | `algorithm_type` | Canonical method key (e.g. `teki`) |
97
- | `algorithm_alias` | Normalized raw name from NetCDF |
98
- | `abbreviation` | Display name (e.g. `TEKI`) |
99
- | `parallelism` / `update_type` / `method_goal` / `emulator_use` | Taxonomy tags — see **Method taxonomy** below |
100
- | `rmse_target` | Target RMSE level |
101
- | `ensemble_size` | Ensemble / particle count |
102
- | `metric` | Mean forward-model runs over `random_seed` (lower = better; −1 = failed) |
103
- | `failure_rate` | % seeds that failed (`metric == -1`) |
104
-
105
- `EXPECTED_DIMS = ("algorithm_type", "rmse_target", "ensemble_size", "random_seed")`
106
-
107
- ### UQ store (`load_uq_store()`)
108
-
109
- Derived from `load_uq_budget_store()` — same schema as the optimization store but with
110
- `uq_target` in place of `rmse_target`, and `metric` = mean budget (N_ens·k_iter). Failure
111
- rate = % of seeds that never satisfied the coverage condition (NaN metric, not −1).
112
-
113
- ### Budget store (`load_uq_budget_store()`)
114
-
115
- | Field | Meaning |
116
- |---|---|
117
- | `benchmark` | `L63`, `L96`, `L96_NN_FORCING`, `L96_SPATIAL_FORCING` |
118
- | `algorithm_type` | Canonical method key |
119
- | `abbreviation` / `Method` / `parallelism` / `update_type` / `method_goal` / `emulator_use` | From method registry |
120
- | `uq_target` | Target-scaling level c |
121
- | `ensemble_size` | Ensemble size N |
122
- | `mean_budget` | Mean N·k_iter over converged seeds (NaN if all failed) |
123
- | `mean_iters` | Mean k_iter over converged seeds |
124
- | `failure_count` | # seeds that never reached the target |
125
- | `failure_rate` | % seeds that never reached the target |
126
- | `n_seeds` | Total seeds (20) |
127
-
128
- Source NC files are registered in `UQ_BUDGET_FILES` in `src/data_store.py`. Each file must
129
- carry `output_coverage (coverage_quantile, k_iter, ensemble_size, random_seed)`,
130
- `target_scaling`, and `output_dim`.
131
-
132
- `UQ_COVERAGE_QUANTILES = [0.15, 0.5, 0.85]` — the three quantile levels used for the
133
- all-quantiles-satisfied condition.
134
-
135
- All three stores are `@st.cache_data` — clear Streamlit cache to pick up new data files.
136
-
137
- ### Benchmark dimensions (`BENCHMARK_DIMS`)
138
-
139
- Physical dimensions of each benchmark exported from `src/data_store.py` as
140
- `BENCHMARK_DIMS: dict[str, tuple[int, int, int]]` — `(param_dim, state_dim, output_dim)`:
141
-
142
- | Benchmark | param | state | output |
143
- |---|---|---|---|
144
- | `L63` | 2 | 3 | 9 |
145
- | `L96` | 1 | 40 | 80 |
146
- | `L96_NN_FORCING` | 61 | 100 | 200 |
147
- | `L96_SPATIAL_FORCING` | 40 | 40 | 80 |
148
-
149
- Passed as `benchmark_dims=BENCHMARK_DIMS` to `render_leaderboard()` for column-header
150
- annotations in the suitability table. Update if a new benchmark is added.
151
-
152
- ## Method taxonomy
153
-
154
- Four independent tags on every `KNOWN_METHODS` entry, each scoped to its own axis:
155
-
156
- - **`parallelism`** — how the search explores parameter space:
157
- `serial` (single point estimate advanced step by step, e.g. `ADAM`/`LM`) ·
158
- `parallel-independent` (population updated with no coupling between members,
159
- e.g. `ABC`'s accepted samples / `HM`'s per-wave resampling) ·
160
- `parallel-interacting` (ensemble members coupled through a shared update each
161
- iteration — any Kalman-based method).
162
- - **`update_type`** — the mechanism driving each update step:
163
- `gradient` (follows the loss gradient or a Gauss-Newton approximation) ·
164
- `kalman` (a possibly linearized/unscented Kalman-style ensemble update) ·
165
- `general` (anything else, e.g. `ABC`'s rejection sampling, `HM`'s implausibility cuts).
166
- - **`method_goal`** — what the method is built to report:
167
- `optimization` (single best-fit parameter estimate) or `uq` (full posterior /
168
- parameter uncertainty). UQ methods *can* still be scored on the Optimization
169
- leaderboard — they're just usually less competitive there.
170
- - **`emulator_use`** — when/whether a surrogate model of the forward model is used:
171
- `none` (samples/evaluates the true forward model throughout) ·
172
- `within-optimize` (refits a surrogate at each search iteration, e.g. `HM` waves) ·
173
- `after-optimize` (fits a surrogate once, after calibration finishes, e.g. `CES-EKI-DMC`).
174
-
175
- `update_type` deliberately avoids a "Bayesian" label — Kalman methods are approximate
176
- Bayesian updates too, so the split is about mechanism (gradient vs. Kalman vs. general),
177
- not philosophy. `_UPDATE_TYPE_ORDER` in `src/common/leaderboard.py` controls
178
- table/suitability sort order and must stay in sync with `update_type` values.
179
- A quick taxonomy tree is rendered on the home page (`src/streamlit_app.py`).
180
-
181
- ## Method registry (`src/common/method_registry.py`)
182
-
183
- | Canonical | Abbreviation | parallelism | update_type | method_goal | emulator_use | Aliases |
184
- |---|---|---|---|---|---|---|
185
- | `teki` | TEKI | parallel-interacting | kalman | optimization | none | — |
186
- | `etki` | ETKI | parallel-interacting | kalman | optimization | none | — |
187
- | `iekf` | IEKF | parallel-interacting | kalman | uq | none | gnsl, gnki |
188
- | `uki` | UKI | parallel-interacting | kalman | optimization | none | — |
189
- | `abc` | ABC | parallel-independent | general | uq | none | — |
190
- | `hm` | HM | parallel-independent | general | uq | within-optimize | — |
191
- | `ces-eki-dmc` | CES-EKI-DMC | parallel-interacting | kalman | uq | after-optimize | — |
192
- | `adam` | ADAM | serial | gradient | optimization | none | — |
193
- | `lm` | LM | serial | gradient | optimization | none | gradient_descent |
194
-
195
- ## How to extend
196
-
197
- ### Add a method
198
- 1. Add entry to `KNOWN_METHODS` in `src/common/method_registry.py`
199
- (keys: `abbreviation`, `Method`, `parallelism`, `update_type`, `method_goal`,
200
- `emulator_use`, `aliases` — see **Method taxonomy** above for valid tag values).
201
- 2. Add detail-page metadata to the inline `method_meta` dict in `src/pages/MethodDetails.py`
202
- keyed by **abbreviation** (e.g. `"TEKI"`). ⚠️ Metadata is split across both files.
203
- 3. Place result NetCDF(s) in `data/` and add paths to `DATASET_FILES` in `src/data_store.py`.
204
-
205
- ### Add an optimization benchmark dataset
206
- 1. Add a `"BENCHMARK_NAME": [list_of_nc_paths]` entry to `DATASET_FILES` in `src/data_store.py`.
207
- Paths are relative to the `data/` directory.
208
- 2. Add a `"BENCHMARK_NAME": (param_dim, state_dim, output_dim)` entry to `BENCHMARK_DIMS`
209
- in `src/data_store.py` for the suitability table column header.
210
-
211
- ### Add UQ data
212
- Add an `(algorithm_type, nc_path)` tuple to the appropriate benchmark key in `UQ_BUDGET_FILES`
213
- in `src/data_store.py`. The NC file must carry:
214
- - `output_coverage` with dims `(coverage_quantile, k_iter, ensemble_size, random_seed)`
215
- - coordinates `coverage_quantile`, `k_iter`, `ensemble_size`, `target_scaling`
216
- - dimension `output_dim`
217
-
218
- `load_uq_budget_store()` and `load_uq_store()` will pick it up automatically after a
219
- Streamlit cache clear. No changes to `UQ_DATASET_FILES` (kept empty) are needed.
220
-
221
- The budget/iters chart and coverage-failure bar chart in the UQ leaderboard are rendered via
222
- the `budget_store` parameter of `render_leaderboard()` in `src/common/leaderboard.py`.
223
-
224
- ### Add a page
225
- 1. Create `src/pages/NewPage.py` — copy the sidebar nav block + `data_store` import shim
226
- from an existing page (e.g. `RawData.py`).
227
- 2. Add `st.sidebar.page_link("pages/NewPage.py", ...)` to **all four** existing files
228
- (`streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`). Sidebar nav
229
- is duplicated — there is no central nav module.
230
-
231
- ## Known gotchas
232
- - **`git lfs pull` is mandatory** before first run — without it `data/*.nc` are 130-byte stubs.
233
- - **Sidebar nav is not centralized** — adding/renaming pages requires editing four files
234
- (`streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`).
235
- - **Method metadata is split** — `method_registry.py` (scoring) and `pages/MethodDetails.py`
236
- `method_meta` dict (display copy) must both be updated when adding a new method.
237
- - **`build_leaderboard()` in `data_store.py` is dead code** — both leaderboards are rendered
238
- via `render_leaderboard()` in `src/common/leaderboard.py`.
239
- - **No CI / no formal test suite** — `test_parse.py` and `test_failures.py` are manual scripts.
240
- - **Cross-page linking** uses `st.query_params["method"]` (URL param = abbreviation).
241
- - **`UQ_DATASET_FILES` is intentionally empty** — UQ data is loaded via `UQ_BUDGET_FILES`
242
- and the `load_uq_budget_store()` pipeline, not via `_load_store()`.
243
- - **UQ failure ≠ optimization failure** — in the UQ store, NaN metric means coverage target
244
- never reached (no `metric == -1` sentinel); `failure_rate` is computed differently.
245
- - **`render_leaderboard` budget_store param** — must be passed explicitly from
246
- `UQLeaderboard.py`; `leaderboard.py` is not aware of UQ-specific data paths.
247
- - **`render_leaderboard` requires `default_target`** — callers must pass the fixed target level
248
- shown on first load (`1.1` for optimization, `1.5` for UQ). There is no "All targets" option.
249
- - **Suitability table is fixed at `default_target`** — `_render_suitability_table()` always
250
- evaluates suitability at the `default_target` passed to `render_leaderboard()`, independent
251
- of what the user selects in the target radio control below it.
252
- - **Suitability criteria** — a method is green for a benchmark if there exists an ensemble size
253
- with `failure_rate < 20%` and `mean_budget ≤ 3× global_best` at `default_target`. Gray means
254
- no data for that benchmark; the method may be suitable on other benchmarks.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -17,123 +17,3 @@ Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :hear
17
 
18
  If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
  forums](https://discuss.streamlit.io).
20
-
21
- ## Path to running the app locally (without docker)
22
- Install the huggingface authentication (via `hf`) add security key
23
-
24
- Use `python (e.g., 3.11)`
25
-
26
- Then call
27
- ```
28
- python3.11 -m venv .venv
29
- pip install -r requirements.txt
30
- ```
31
-
32
- As the data is just a pointer to the repository, install, and pull the large files
33
- ```
34
- sudo apt install git-lfs
35
- git lfs pull
36
- ```
37
- then run the streamlit app
38
- ```
39
- streamlit run src/streamlit_app.py
40
- ```
41
-
42
- ## Via docker (untested)
43
-
44
- install docker
45
- ```
46
- sudo snap install docker
47
- docker build -t calibration-benchmark .
48
- docker run -p 8501:8501 calibration-benchmark
49
- ```
50
- Open
51
- ```
52
- http://localhost:8501
53
- ```
54
-
55
- ## Adding new data files (git LFS)
56
-
57
- All `*.nc` files in `data/` are tracked via git LFS (configured in `.gitattributes`).
58
- To add a new result file:
59
-
60
- **Step 1 — Ensure git LFS is installed and initialised**
61
- ```
62
- sudo apt install git-lfs # or: brew install git-lfs
63
- git lfs install
64
- ```
65
-
66
- **Step 2 — Copy the file into `data/`**
67
- ```
68
- cp /path/to/your_results.nc data/your_results.nc
69
- ```
70
- Sub-directories are fine (`data/UKI_results/`, `data/bayesian/`, etc.).
71
-
72
- **Step 3 — Stage and commit — LFS handles the rest**
73
- ```
74
- git add data/your_results.nc
75
- git commit -m "Add <method> results for <benchmark>"
76
- ```
77
- Git LFS intercepts the add automatically because `.gitattributes` already contains
78
- `*.nc filter=lfs diff=lfs merge=lfs -text`. You can verify the file is tracked with:
79
- ```
80
- git lfs ls-files | grep your_results.nc
81
- ```
82
-
83
- **Step 4 — Register the file in the app**
84
-
85
- Open `src/data_store.py` and add the path to `DATASET_FILES` (optimization) or
86
- `UQ_DATASET_FILES` (UQ leaderboard):
87
- ```python
88
- DATASET_FILES = {
89
- "L96": [
90
- "existing_file.nc",
91
- "your_results.nc", # <-- add here
92
- ],
93
- ...
94
- }
95
- ```
96
- Paths are relative to the `data/` directory.
97
-
98
- **Step 5 — Push**
99
- ```
100
- git push
101
- ```
102
- LFS objects are pushed to the LFS store automatically alongside the pointer commit.
103
- Collaborators get the data with `git lfs pull` after cloning or fetching.
104
-
105
- ---
106
-
107
- ## Contributing via a Hugging Face pull request
108
-
109
- Hugging Face Spaces uses a non-standard PR workflow: you create the PR online first, then push to the branch it creates.
110
-
111
- **Step 1 — Clone the repo (if you haven't already)**
112
- ```
113
- git clone https://huggingface.co/spaces/<owner>/<space-name>
114
- cd <space-name>
115
- ```
116
-
117
- **Step 2 — Create and work on a local branch**
118
- ```
119
- git checkout -b <local-branch-name>
120
- ```
121
- Make your changes, then commit them:
122
- ```
123
- git add <changed-files>
124
- git commit -m "Your commit message"
125
- ```
126
-
127
- **Step 3 — Open the pull request online**
128
-
129
- Go to the Space on huggingface.co → **Community** tab → **New Pull Request**.
130
- Fill in the title and description and submit. Hugging Face will create a remote PR branch
131
- (shown on the PR page, e.g. `refs/pr/1` or a named branch).
132
-
133
- **Step 4 — Push your local branch to the PR branch**
134
- ```
135
- git push --set-upstream origin <local-branch-name>:<pr-branch-name>
136
- ```
137
- Replace `<pr-branch-name>` with the branch name shown on the HF PR page (often `refs/pr/N`).
138
-
139
- The PR page will update automatically once the push lands.
 
17
 
18
  If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
  forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:1c8999ffa5c70d8e891443a24929622e203e0e55bb263be52add1b0585b8a6c7
3
- size 12343
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dad0b7dfa06ba4e56bedd0173a4c9312674e9ab97845244578b27abb0cef997a
3
+ size 12408
data/adam_results/leaderboard_adam_l63_2026-06-26.nc DELETED
@@ -1,3 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:539eb03a55d4ffe0a51b3b45db3b5dc0d0e159c8132e71639ba2eb5c0f72af95
3
- size 11440
 
 
 
 
data/ces-eki-const_results/ces-eki-const_l63_ensemble_results_2026-07-09_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d0fa6d71f8392a285333cbfdb7c9e8f166a45fa4daaff573d89e4c254371bfdd
3
- size 672937
 
 
 
 
data/ces-eki-const_results/ces-eki-const_l96_ensemble_results_2026-07-09_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c32fcb778c650815415574c4de51ba5569bc65c2773fbd1aa1e0e2be52a603ff
3
- size 809737
 
 
 
 
data/ces-eki-const_results/ces-eki-const_l96_nn_forcing_ensemble_results_2026-07-09_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:90bcb0da3058b7adf3f3c93f3d196b3f94360269a4ec7a7f3fbca4989a7528a3
3
- size 809737
 
 
 
 
data/ces-eki-const_results/ces-eki-const_l96_spatial_forcing_ensemble_results_2026-07-09_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4665e8a50bb32de9f6d51b9332d105e3430d66c6545ff3bcebf2d39b28fc48e7
3
- size 809737
 
 
 
 
data/ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ea0c583aa3b7ffd39d8b00ab476b4fe7fcf3b15b54e0e15377cbc2245385d8e7
3
- size 287190
 
 
 
 
data/ces-eki-dmc_results/ces-eki-dmc_l96_ensemble_results_2026-06-15_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:eda2163d7dfc7e4a851aea5d6055b1f6cc1e1c4ec8496cf4072679739101fd39
3
- size 123030
 
 
 
 
data/ces-eki-dmc_results/ces-eki-dmc_l96_nn_forcing_ensemble_results_2026-06-15_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d36761e0de6abb50ddd3054d2adf6100a6e730ba9ea8e2240ed7dd6a2c970892
3
- size 150390
 
 
 
 
data/ces-eki-dmc_results/ces-eki-dmc_l96_spatial_forcing_ensemble_results_2026-06-15_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4a402f8dc6d39b34bb1243e878bb34505846fe176a7e21a1024a8c1727d55dc1
3
- size 177750
 
 
 
 
data/ces-iekf-const_results/ces-iekf-const_l63_ensemble_results_2026-07-17_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:34cbaa8b1fca2cce61f1c1b1b47ae7e175a282b64f5725f4c6ff5fecb376c8cf
3
- size 672937
 
 
 
 
data/ces-iekf-const_results/ces-iekf-const_l96_ensemble_results_2026-07-17_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c530515a97613472903b1f47fa4d2e1a5cd15955222553b73ccd5db74fa51c21
3
- size 809737
 
 
 
 
data/ces-iekf-const_results/ces-iekf-const_l96_nn_forcing_ensemble_results_2026-07-17_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c25dad11f5b277584938dd0cbb400c8fe4a416d597c6156821059abd39129d1a
3
- size 809737
 
 
 
 
data/ces-iekf-const_results/ces-iekf-const_l96_spatial_forcing_ensemble_results_2026-07-17_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:dfc68524a29a1bf75b91d4124dbb01f7011bdb084e9706ec079560b701504228
3
- size 809737
 
 
 
 
data/gnki-uq_results/leaderboard_gnki_l63_2026-07-07_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a6f8bcb81074d1c26eb1fca23f8f23b1f9962d157a6515a0269d0f9c68f2e2e1
3
- size 673072
 
 
 
 
data/gnki-uq_results/leaderboard_gnki_l96_const-force_2026-07-07_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f01ca145dca3bb97378e7ad5b2cf5c4a8fbc5db45576ac3036600e0d41caf124
3
- size 809872
 
 
 
 
data/gnki-uq_results/leaderboard_gnki_l96_flux-force_2026-07-07_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:44d18f3e3d79324e53f2cee5cf38137a53bc1f343e756c6efbdad5c84e00222e
3
- size 809872
 
 
 
 
data/gnki-uq_results/leaderboard_gnki_l96_vec-force_2026-07-07_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ec7d3760d1a8cf35b043e44130085c2c16b87b0d8d5c6eb11120e7ef83f5edea
3
- size 809872
 
 
 
 
data/history-matching-uq_results/history-matching_l63_ensemble_results_2026-07-20_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8a656f0ff90226742e5be62946e4bef99a8762884f8113d3a24c458e9996b982
3
- size 672930
 
 
 
 
data/history-matching-uq_results/history-matching_l96_const-force_2026-07-20_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:38b60ef99cfeb4baae97d8a429d84d9a8143799d405d6ee79a2c01833bfe6409
3
- size 809730
 
 
 
 
data/history-matching-uq_results/history-matching_l96_flux-force_2026-07-21_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8b76f4016ea38382595960750081bb9d1797b3df69f212abea7fd322d9582308
3
- size 809730
 
 
 
 
data/history-matching-uq_results/history-matching_l96_vec-force_2026-07-21_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:9040eb783a225266d63cdfafd31b7a8443ef41c949ab9ecaf059fe5e7cabf7f7
3
- size 809730
 
 
 
 
data/history-matching-uq_results/history-matching_l96_vec-force_2026-07-22_minimal.nc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7b257c962e1c1e3172d31c2a19cd73d0f2ec8e8ebd62d679faf0b53d86be6002
3
- size 809730
 
 
 
 
data/levenberg_marquardt_results/leaderboard_lm_l63_2026-06-29.nc DELETED
@@ -1,3 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7a647aeac2cc740665d27164aef3295bf7f07f8d461013dcb396ec843624c47e
3
- size 11440
 
 
 
 
requirements.txt CHANGED
@@ -1,4 +1,3 @@
1
- hf
2
  altair
3
  pandas
4
  streamlit
@@ -6,6 +5,4 @@ jax
6
  gpjax
7
  matplotlib
8
  xarray
9
- netcdf4
10
- h5netcdf
11
- h5py
 
 
1
  altair
2
  pandas
3
  streamlit
 
5
  gpjax
6
  matplotlib
7
  xarray
8
+ netCDF4
 
 
src/common/leaderboard.py DELETED
@@ -1,768 +0,0 @@
1
- """Shared leaderboard renderer for the Calibration Benchmark dashboard.
2
-
3
- Extracts the scored-table pipeline from the home page so both the Optimization
4
- leaderboard (home) and the UQ leaderboard (pages/UQLeaderboard.py) can reuse
5
- it without duplicating code.
6
-
7
- Usage::
8
-
9
- from common.leaderboard import render_leaderboard
10
-
11
- render_leaderboard(
12
- metric_store,
13
- target_col="rmse_target",
14
- target_label="RMSE Target Level",
15
- title="Optimization Leaderboard",
16
- state_prefix="opt",
17
- default_target=1.1,
18
- raw_page="pages/RawData.py",
19
- )
20
- """
21
- from __future__ import annotations
22
-
23
- import altair as alt
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
-
37
- def _method_color(present_abbrevs: list[str]) -> alt.Color:
38
- """Color encoding for the ``abbreviation`` field.
39
-
40
- The scale domain/range is always the full method registry so a given
41
- method keeps the same color across every chart and page. The legend,
42
- however, is restricted to *present_abbrevs* so it only lists methods
43
- actually plotted in this chart section rather than every known method.
44
- """
45
- return alt.Color(
46
- "abbreviation:N",
47
- title="Method",
48
- scale=alt.Scale(domain=list(METHOD_COLORS.keys()), range=list(METHOD_COLORS.values())),
49
- legend=alt.Legend(values=sorted(present_abbrevs)),
50
- )
51
-
52
- # update_type display order in the leaderboard table (lower = earlier).
53
- # Unknown/unmapped update types fall back to 99 and appear at the end.
54
- # See method_registry.py for the full four-axis taxonomy.
55
- _UPDATE_TYPE_ORDER: dict[str, int] = {
56
- "kalman": 0,
57
- "gradient": 1,
58
- "general": 2,
59
- }
60
-
61
-
62
- def _render_suitability_table(
63
- store: pd.DataFrame,
64
- target_col: str,
65
- suitability_target: float,
66
- benchmark_dims: dict[str, tuple[int, int, int]] | None = None,
67
- failure_threshold: float = 20.0,
68
- ratio_threshold: float = 3.0,
69
- ) -> None:
70
- """Render the method × benchmark suitability grid above the leaderboard controls."""
71
- benchmarks = sorted(
72
- store["benchmark"].unique().tolist(),
73
- key=lambda bm: benchmark_dims[bm][0] if (benchmark_dims and bm in benchmark_dims) else bm,
74
- )
75
- _abbr_update_type = (
76
- store[["abbreviation", "update_type"]].dropna()
77
- .drop_duplicates("abbreviation")
78
- .set_index("abbreviation")["update_type"]
79
- .to_dict()
80
- )
81
- methods = sorted(
82
- store["abbreviation"].dropna().unique().tolist(),
83
- key=lambda a: (_UPDATE_TYPE_ORDER.get(_abbr_update_type.get(a, ""), 99), a),
84
- )
85
-
86
- target_str = str(float(suitability_target))
87
- target_str_col = f"{target_col}_str"
88
- target_df = store.copy()
89
- target_df[target_str_col] = target_df[target_col].astype(str)
90
- target_df = target_df[target_df[target_str_col] == target_str]
91
-
92
- # Global best per benchmark: min metric across all methods with failure_rate < threshold
93
- global_best: dict[str, float | None] = {}
94
- for bm in benchmarks:
95
- bm_df = target_df[target_df["benchmark"] == bm]
96
- qualifying = bm_df[bm_df["failure_rate"] < failure_threshold].dropna(subset=["metric"])
97
- global_best[bm] = float(qualifying["metric"].min()) if not qualifying.empty else None
98
-
99
- cell_text: dict[str, dict[str, str]] = {}
100
- cell_color: dict[str, dict[str, str]] = {}
101
-
102
- for method in methods:
103
- cell_text[method] = {}
104
- cell_color[method] = {}
105
- for bm in benchmarks:
106
- sub = target_df[
107
- (target_df["abbreviation"] == method) & (target_df["benchmark"] == bm)
108
- ]
109
- if sub.empty:
110
- cell_text[method][bm] = "—"
111
- cell_color[method][bm] = _UNTESTED
112
- continue
113
- qualifying = sub[sub["failure_rate"] < failure_threshold].dropna(subset=["metric"])
114
- if qualifying.empty:
115
- cell_text[method][bm] = "failed"
116
- cell_color[method][bm] = _UNSUITABLE
117
- continue
118
- method_best = float(qualifying["metric"].min())
119
- gb = global_best.get(bm)
120
- ratio = (method_best / gb) if (gb is not None and gb > 0) else 1.0
121
- cell_text[method][bm] = f"{ratio:.1f}×"
122
- cell_color[method][bm] = _SUITABLE if ratio <= ratio_threshold else _UNSUITABLE
123
-
124
- col_labels: dict[str, str] = {}
125
- for bm in benchmarks:
126
- if benchmark_dims and bm in benchmark_dims:
127
- p, s, o = benchmark_dims[bm]
128
- col_labels[bm] = f"{bm} (p={p}, s={s}, o={o})"
129
- else:
130
- col_labels[bm] = bm
131
-
132
- display_df = pd.DataFrame(cell_text).T.rename(columns=col_labels)
133
- color_df = pd.DataFrame(cell_color).T.rename(columns=col_labels)
134
- display_df.index.name = "Method"
135
-
136
- def _style(df: pd.DataFrame) -> pd.DataFrame:
137
- result = pd.DataFrame("", index=df.index, columns=df.columns)
138
- for row in df.index:
139
- for col in df.columns:
140
- bg = color_df.loc[row, col]
141
- fg = "#212529" if bg == _UNTESTED else "white"
142
- result.loc[row, col] = (
143
- f"background-color: {bg}; color: {fg}; "
144
- "text-align: center; font-weight: bold"
145
- )
146
- return result
147
-
148
- st.subheader("Method Suitability Overview")
149
- st.caption(
150
- f"Evaluated at target = {suitability_target}. "
151
- f"**Green**: at some ensemble size, failure rate < {failure_threshold:.0f}% "
152
- f"and mean budget ≤ {ratio_threshold:.0f}× the best method (ratio shown). "
153
- "**Red** (failed): data present but all runs failed to reach the target. "
154
- "**Gray (—)**: no data for this benchmark."
155
- )
156
- st.dataframe(display_df.style.apply(_style, axis=None), use_container_width=True)
157
-
158
-
159
- def render_leaderboard(
160
- metric_store: pd.DataFrame,
161
- *,
162
- target_col: str,
163
- target_label: str,
164
- title: str,
165
- state_prefix: str,
166
- default_target: float,
167
- raw_page: str | None = None,
168
- show_failure_panel: bool = False,
169
- show_scoring_modes: bool = True,
170
- canonical_target_levels: list[float] | None = None,
171
- budget_store: pd.DataFrame | None = None,
172
- benchmark_dims: dict[str, tuple[int, int, int]] | None = None,
173
- ) -> None:
174
- """Render a scored leaderboard backed by *metric_store*.
175
-
176
- Parameters
177
- ----------
178
- metric_store:
179
- DataFrame produced by ``load_metric_store()`` or ``load_uq_store()``.
180
- Must contain at least the columns ``benchmark``, ``algorithm_type``,
181
- ``abbreviation``, ``Method``, the four taxonomy tag columns
182
- (``parallelism``, ``update_type``, ``method_goal``, ``emulator_use``),
183
- ``ensemble_size``, ``metric``, ``failure_rate``, and *target_col*.
184
- target_col:
185
- Name of the target-coordinate column, e.g. ``"rmse_target"`` or
186
- ``"uq_target"``.
187
- target_label:
188
- Human-readable label for the target-level radio control,
189
- e.g. ``"RMSE Target Level"`` or ``"UQ Target Level"``.
190
- title:
191
- Leaderboard section header text.
192
- state_prefix:
193
- Short string used to namespace ``st.session_state`` keys so multiple
194
- leaderboard pages keep independent control state. Use ``"opt"`` for
195
- the Optimization leaderboard and ``"uq"`` for the UQ leaderboard.
196
- default_target:
197
- Target level shown on first load (e.g. ``1.1`` for optimization,
198
- ``1.5`` for UQ).
199
- raw_page:
200
- Optional Streamlit page path for an "Open Raw Data" link shown at the
201
- bottom. Pass ``None`` to suppress the link.
202
- show_failure_panel:
203
- If ``True``, render a grouped-bar failure-rate chart below the main
204
- performance chart.
205
- show_scoring_modes:
206
- If ``True``, show scoring-mode radio controls.
207
- canonical_target_levels:
208
- If provided, the target-level selector always offers exactly these
209
- values (as strings) regardless of what is present in the data. Use
210
- this to pin the UQ leaderboard to its fixed set of target-scaling
211
- levels even when some have 100 % failure.
212
- budget_store:
213
- Optional DataFrame produced by ``load_uq_budget_store()``. When
214
- provided, an additional "Mean Iterations for Coverage" section is
215
- rendered below the main performance chart.
216
- benchmark_dims:
217
- Optional mapping of benchmark name → (param_dim, state_dim, output_dim)
218
- used to annotate column headers in the suitability table.
219
- """
220
- st.header(title)
221
-
222
- if metric_store.empty:
223
- st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
224
- return
225
-
226
- _render_suitability_table(
227
- metric_store, target_col, default_target, benchmark_dims=benchmark_dims
228
- )
229
-
230
- st.divider()
231
-
232
- # Derived column name for the string version of the target coordinate
233
- target_str_col = f"{target_col}_str"
234
-
235
- benchmark_values = sorted(metric_store["benchmark"].unique().tolist())
236
- selected_benchmark = st.selectbox("Benchmark", options=benchmark_values, index=0)
237
-
238
- filtered = metric_store[metric_store["benchmark"] == selected_benchmark].copy()
239
- filtered[target_str_col] = filtered[target_col].astype(str)
240
-
241
- if canonical_target_levels is not None:
242
- target_options = [str(float(t)) for t in canonical_target_levels]
243
- else:
244
- target_options = sorted(
245
- metric_store[target_col].astype(str).unique().tolist()
246
- )
247
-
248
- scoring_options = [
249
- "Mean Forward Model Runs",
250
- "Minimum Forward Model Runs",
251
- "Smallest Optimal Ensemble Size",
252
- "Custom Blend",
253
- ]
254
-
255
- # Session-state keys namespaced by state_prefix so two leaderboard pages
256
- # don't share control state.
257
- k_target = f"{state_prefix}_selected_target"
258
- k_scoring = f"{state_prefix}_scoring_mode"
259
- k_weight = f"{state_prefix}_fwdruns_weight_percent"
260
- k_methods = f"{state_prefix}_selected_methods"
261
-
262
- default_target_str = str(float(default_target))
263
-
264
- # Pre-populate session state so the radio widget and the filter agree on first load.
265
- if k_target not in st.session_state or st.session_state[k_target] not in target_options:
266
- st.session_state[k_target] = (
267
- default_target_str if default_target_str in target_options else target_options[0]
268
- )
269
- selected_target = st.session_state[k_target]
270
-
271
- current_scoring_mode = st.session_state.get(k_scoring, "Mean Forward Model Runs")
272
- if current_scoring_mode not in scoring_options:
273
- current_scoring_mode = scoring_options[0]
274
-
275
- current_fwdruns_weight_percent = int(st.session_state.get(k_weight, 80))
276
- current_fwdruns_weight_percent = max(0, min(100, current_fwdruns_weight_percent))
277
-
278
- scoring_mode = current_scoring_mode
279
- fwdruns_weight = current_fwdruns_weight_percent / 100.0
280
- ensemble_weight = 1.0 - fwdruns_weight
281
-
282
- # Available methods for the current benchmark selection; used to populate the
283
- # multiselect and to prune any stale saved selections when the benchmark changes.
284
- available_methods = sorted(filtered["abbreviation"].dropna().unique().tolist())
285
- saved_methods = st.session_state.get(k_methods, available_methods)
286
- valid_saved = [m for m in saved_methods if m in available_methods]
287
- st.session_state[k_methods] = valid_saved if valid_saved else available_methods
288
-
289
- def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
290
- ranking_source = input_df[input_df[target_str_col] == selected_target]
291
- if ranking_source.empty:
292
- return ranking_source
293
-
294
- # Failure rate from every row (NaN metric rows carry failure_rate=100)
295
- failure_agg = ranking_source.groupby(
296
- ["algorithm_type", "abbreviation", "Method"], as_index=False
297
- ).agg(**{"Mean Failure Rate (%)": ("failure_rate", "mean")})
298
-
299
- # Metric stats only from runs that reached the target (non-NaN metric)
300
- valid_rows = ranking_source.dropna(subset=["metric"])
301
- if valid_rows.empty:
302
- return pd.DataFrame()
303
-
304
- scored_df = valid_rows.groupby(
305
- ["algorithm_type", "abbreviation", "Method"], as_index=False
306
- ).agg(
307
- **{"Mean Forward Model Runs": ("metric", "mean")},
308
- **{"Minimum Forward Model Runs": ("metric", "min")},
309
- **{"Ensemble Sizes Used": ("ensemble_size", "nunique")},
310
- )
311
-
312
- best_per_target = (
313
- valid_rows.sort_values(["algorithm_type", target_str_col, "metric", "ensemble_size"])
314
- .groupby(["algorithm_type", "abbreviation", "Method", target_str_col], as_index=False)
315
- .first()[
316
- [
317
- "algorithm_type",
318
- "abbreviation",
319
- "Method",
320
- target_str_col,
321
- "ensemble_size",
322
- ]
323
- ]
324
- )
325
-
326
- optimal_ensemble = best_per_target.groupby(
327
- ["algorithm_type", "abbreviation", "Method"], as_index=False
328
- ).agg(**{"Optimal Ensemble Size": ("ensemble_size", "mean")})
329
-
330
- scored_df = scored_df.merge(
331
- optimal_ensemble,
332
- on=["algorithm_type", "abbreviation", "Method"],
333
- how="left",
334
- )
335
- scored_df = scored_df.merge(
336
- failure_agg,
337
- on=["algorithm_type", "abbreviation", "Method"],
338
- how="left",
339
- )
340
-
341
- scored_df["Optimal Ensemble Size"] = scored_df["Optimal Ensemble Size"].round(2)
342
- scored_df["Mean Forward Model Runs"] = scored_df["Mean Forward Model Runs"].round(4)
343
- scored_df["Minimum Forward Model Runs"] = scored_df["Minimum Forward Model Runs"].round(4)
344
- scored_df["Mean Failure Rate (%)"] = scored_df["Mean Failure Rate (%)"].round(1)
345
-
346
- mean_runs_min = scored_df["Mean Forward Model Runs"].min()
347
- mean_runs_max = scored_df["Mean Forward Model Runs"].max()
348
- if mean_runs_max > mean_runs_min:
349
- scored_df["mean_runs_score"] = (
350
- 100.0 * (mean_runs_max - scored_df["Mean Forward Model Runs"]) / (mean_runs_max - mean_runs_min)
351
- )
352
- else:
353
- scored_df["mean_runs_score"] = 100.0
354
-
355
- minimum_runs_min = scored_df["Minimum Forward Model Runs"].min()
356
- minimum_runs_max = scored_df["Minimum Forward Model Runs"].max()
357
- if minimum_runs_max > minimum_runs_min:
358
- scored_df["minimum_runs_score"] = (
359
- 100.0 * (minimum_runs_max - scored_df["Minimum Forward Model Runs"]) / (minimum_runs_max - minimum_runs_min)
360
- )
361
- else:
362
- scored_df["minimum_runs_score"] = 100.0
363
-
364
- ens_min = scored_df["Optimal Ensemble Size"].min()
365
- ens_max = scored_df["Optimal Ensemble Size"].max()
366
- if ens_max > ens_min:
367
- scored_df["ensemble_score"] = (
368
- 100.0 * (ens_max - scored_df["Optimal Ensemble Size"]) / (ens_max - ens_min)
369
- )
370
- else:
371
- scored_df["ensemble_score"] = 100.0
372
-
373
- if scoring_mode == "Mean Forward Model Runs":
374
- scored_df["Score"] = scored_df["mean_runs_score"]
375
- sort_columns = ["Mean Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
376
- ascending = [True, True, True]
377
- elif scoring_mode == "Minimum Forward Model Runs":
378
- scored_df["Score"] = scored_df["minimum_runs_score"]
379
- sort_columns = ["Minimum Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
380
- ascending = [True, True, True]
381
- elif scoring_mode == "Smallest Optimal Ensemble Size":
382
- scored_df["Score"] = scored_df["ensemble_score"]
383
- sort_columns = ["Optimal Ensemble Size", "Mean Forward Model Runs", "abbreviation"]
384
- ascending = [True, True, True]
385
- else:
386
- scored_df["Score"] = (
387
- fwdruns_weight * scored_df["mean_runs_score"]
388
- + ensemble_weight * scored_df["ensemble_score"]
389
- )
390
- sort_columns = ["Score", "Mean Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
391
- ascending = [False, True, True, True]
392
-
393
- scored_df = scored_df.sort_values(sort_columns, ascending=ascending).reset_index(drop=True)
394
-
395
- if add_rank:
396
- scored_df["Rank"] = scored_df.index + 1
397
- scored_df["Placement"] = scored_df["Rank"].apply(
398
- lambda rank: f"{ {1: '🥇', 2: '🥈', 3: '🥉'}.get(rank, '')} #{rank}".strip()
399
- )
400
-
401
- # Append methods that had data for this target but every seed failed (all-NaN metric).
402
- # They appear at the bottom of the table as "DNF" so users can distinguish
403
- # "tried and failed" from "not tested on this benchmark".
404
- tried_abbrevs = set(ranking_source["abbreviation"].dropna().unique())
405
- ranked_abbrevs = set(scored_df["abbreviation"].dropna().unique())
406
- dnf_abbrevs = tried_abbrevs - ranked_abbrevs
407
- if dnf_abbrevs:
408
- dnf_rows = failure_agg[failure_agg["abbreviation"].isin(dnf_abbrevs)].copy()
409
- for col in [
410
- "Score", "mean_runs_score", "minimum_runs_score", "ensemble_score",
411
- "Mean Forward Model Runs", "Minimum Forward Model Runs",
412
- "Optimal Ensemble Size", "Ensemble Sizes Used",
413
- ]:
414
- dnf_rows[col] = float("nan")
415
- if add_rank:
416
- dnf_rows["Rank"] = float("nan")
417
- dnf_rows["Placement"] = "DNF"
418
- scored_df = pd.concat([scored_df, dnf_rows], ignore_index=True)
419
-
420
- return scored_df
421
-
422
- leaderboard_df = build_scored_table(filtered, add_rank=True)
423
-
424
- # Attach the four taxonomy tags (constant per abbreviation, so this is a
425
- # plain lookup merge rather than a groupby key threaded through every
426
- # aggregation above).
427
- if not leaderboard_df.empty:
428
- tag_cols = ["abbreviation", "parallelism", "update_type", "method_goal", "emulator_use"]
429
- tag_lookup = filtered[tag_cols].drop_duplicates("abbreviation")
430
- leaderboard_df = leaderboard_df.merge(tag_lookup, on="abbreviation", how="left")
431
-
432
- # Re-sort by update_type group, then by performance within each group.
433
- # Placement numbers still reflect overall performance rank.
434
- if not leaderboard_df.empty:
435
- leaderboard_df = leaderboard_df.assign(
436
- _update_type_sort=leaderboard_df["update_type"].map(_UPDATE_TYPE_ORDER).fillna(99),
437
- _is_dnf=leaderboard_df["Score"].isna(),
438
- ).sort_values(
439
- ["_update_type_sort", "_is_dnf", "Score"],
440
- ascending=[True, True, False],
441
- na_position="last",
442
- ).drop(columns=["_update_type_sort", "_is_dnf"]).reset_index(drop=True)
443
-
444
- if scoring_mode == "Mean Forward Model Runs":
445
- score_basis = "mean forward-model runs at the selected target level (lower is better)"
446
- elif scoring_mode == "Minimum Forward Model Runs":
447
- score_basis = "minimum forward-model runs at the selected target level (lower is better)"
448
- elif scoring_mode == "Smallest Optimal Ensemble Size":
449
- score_basis = "mean optimal ensemble size at the selected target level (lower is better)"
450
- else:
451
- score_basis = (
452
- f"weighted blend of normalized forward-model-runs score ({fwdruns_weight:.0%}) "
453
- f"and normalized ensemble-size score ({ensemble_weight:.0%})"
454
- )
455
-
456
- # Controls — always visible (not collapsed behind a dropdown) so users can
457
- # change target/scoring/methods even when the current selection yields all
458
- # failures.
459
- st.subheader("Scoring & Target Controls")
460
- st.radio(
461
- target_label,
462
- options=target_options,
463
- horizontal=True,
464
- key=k_target,
465
- )
466
-
467
- if show_scoring_modes:
468
- st.radio(
469
- "Scoring Method",
470
- options=scoring_options,
471
- horizontal=True,
472
- key=k_scoring,
473
- )
474
-
475
- if st.session_state.get(k_scoring, "Mean Forward Model Runs") == "Custom Blend":
476
- st.slider(
477
- "Blend Weight: Forward Runs vs Ensemble Size",
478
- min_value=0,
479
- max_value=100,
480
- step=5,
481
- key=k_weight,
482
- help=(
483
- "Higher forward-runs weight prioritizes fewer model evaluations; "
484
- "higher ensemble-size weight prioritizes smaller ensembles."
485
- ),
486
- )
487
-
488
- st.multiselect(
489
- "Methods to display in charts",
490
- options=available_methods,
491
- key=k_methods,
492
- )
493
-
494
- selected_methods = st.session_state.get(k_methods, available_methods)
495
- if not selected_methods:
496
- selected_methods = available_methods
497
-
498
- st.divider()
499
-
500
- # --- Charts (rendered above the leaderboard table, below the controls) ---
501
- if not leaderboard_df.empty:
502
- st.subheader("Mean Forward Model Runs vs Ensemble Size")
503
- chart_source = filtered[filtered[target_str_col] == selected_target]
504
- chart_source = chart_source[chart_source["abbreviation"].isin(selected_methods)]
505
- chart_df = chart_source.dropna(subset=["metric"]).groupby(
506
- ["abbreviation", "ensemble_size"], as_index=False
507
- ).agg(mean_forward_runs=("metric", "mean"))
508
-
509
- all_ens_combos = chart_source[["abbreviation", "ensemble_size"]].drop_duplicates()
510
- ens_ticks = sorted(all_ens_combos["ensemble_size"].unique().tolist()) if not all_ens_combos.empty else []
511
- main_color = _method_color(all_ens_combos["abbreviation"].unique().tolist())
512
-
513
- if not chart_df.empty:
514
- _ok = chart_df[["abbreviation", "ensemble_size"]].assign(_ok=True)
515
- fail_df = all_ens_combos.merge(_ok, on=["abbreviation", "ensemble_size"], how="left")
516
- fail_df = fail_df[fail_df["_ok"].isna()].drop(columns="_ok").assign(mean_forward_runs=0.0)
517
- else:
518
- fail_df = all_ens_combos.assign(mean_forward_runs=0.0)
519
-
520
- all_failed = chart_df.empty
521
- chart_layers = []
522
- if not chart_df.empty:
523
- chart_layers.append(
524
- alt.Chart(chart_df)
525
- .mark_line(point=True)
526
- .encode(
527
- x=alt.X(
528
- "ensemble_size:Q",
529
- title="Ensemble Size",
530
- axis=alt.Axis(values=ens_ticks, format="d"),
531
- ),
532
- y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
533
- color=main_color,
534
- tooltip=["abbreviation", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
535
- )
536
- )
537
- if not fail_df.empty:
538
- y_fwd = (
539
- alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs", scale=alt.Scale(domain=[0, 1]))
540
- if all_failed
541
- else alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs")
542
- )
543
- chart_layers.append(
544
- alt.Chart(fail_df)
545
- .mark_point(shape="cross", angle=45, size=200, filled=True, opacity=1.0)
546
- .encode(
547
- x=alt.X(
548
- "ensemble_size:Q",
549
- title="Ensemble Size",
550
- axis=alt.Axis(values=ens_ticks, format="d"),
551
- ),
552
- y=y_fwd,
553
- color=main_color,
554
- tooltip=[
555
- alt.Tooltip("abbreviation:N", title="Method"),
556
- alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
557
- alt.Tooltip("mean_forward_runs:Q", title="Value (all failed)"),
558
- ],
559
- )
560
- )
561
- # Dashed vertical rule for methods with only one ensemble size so they
562
- # remain easy to spot when multi-ensemble methods dominate the x-axis.
563
- if not all_ens_combos.empty:
564
- single_ens_abbrevs = (
565
- all_ens_combos.groupby("abbreviation")["ensemble_size"]
566
- .nunique()
567
- .pipe(lambda s: s[s == 1].index.tolist())
568
- )
569
- if single_ens_abbrevs:
570
- rule_df = (
571
- all_ens_combos[all_ens_combos["abbreviation"].isin(single_ens_abbrevs)]
572
- .drop_duplicates()
573
- )
574
- chart_layers.append(
575
- alt.Chart(rule_df)
576
- .mark_rule(strokeDash=[4, 4], opacity=0.5)
577
- .encode(
578
- x=alt.X(
579
- "ensemble_size:Q",
580
- axis=alt.Axis(values=ens_ticks, format="d"),
581
- ),
582
- color=main_color,
583
- tooltip=[
584
- alt.Tooltip("abbreviation:N", title="Method"),
585
- alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
586
- ],
587
- )
588
- )
589
-
590
- if chart_layers:
591
- st.altair_chart(alt.layer(*chart_layers), use_container_width=True)
592
-
593
- # Mean-iterations-for-coverage section (UQ only, when budget_store provided)
594
- if budget_store is not None and not budget_store.empty:
595
- bf = budget_store[budget_store["benchmark"] == selected_benchmark].copy()
596
- bf[target_str_col] = bf[target_col].astype(str)
597
- bf = bf[bf[target_str_col] == selected_target]
598
- bf = bf[bf["abbreviation"].isin(selected_methods)]
599
-
600
- iters_df = (
601
- bf[["abbreviation", "ensemble_size", "mean_iters"]]
602
- .dropna(subset=["mean_iters"])
603
- .groupby(["abbreviation", "ensemble_size"], as_index=False)
604
- .agg(mean_iters=("mean_iters", "mean"))
605
- )
606
- all_ens_combos_iters = bf[["abbreviation", "ensemble_size"]].drop_duplicates()
607
- if not all_ens_combos_iters.empty:
608
- iters_ticks = sorted(all_ens_combos_iters["ensemble_size"].unique().tolist())
609
- iters_color = _method_color(all_ens_combos_iters["abbreviation"].unique().tolist())
610
-
611
- if not iters_df.empty:
612
- _ok_iters = iters_df[["abbreviation", "ensemble_size"]].assign(_ok=True)
613
- fail_df_iters = all_ens_combos_iters.merge(_ok_iters, on=["abbreviation", "ensemble_size"], how="left")
614
- fail_df_iters = fail_df_iters[fail_df_iters["_ok"].isna()].drop(columns="_ok").assign(mean_iters=0.0)
615
- else:
616
- fail_df_iters = all_ens_combos_iters.assign(mean_iters=0.0)
617
-
618
- all_failed_iters = iters_df.empty
619
- st.subheader("Mean Iterations for Coverage vs Ensemble Size")
620
- iters_layers = []
621
- if not iters_df.empty:
622
- iters_layers.append(
623
- alt.Chart(iters_df)
624
- .mark_line(point=True)
625
- .encode(
626
- x=alt.X(
627
- "ensemble_size:Q",
628
- title="Ensemble Size",
629
- axis=alt.Axis(values=iters_ticks, format="d"),
630
- ),
631
- y=alt.Y("mean_iters:Q", title="Mean Iterations"),
632
- color=iters_color,
633
- tooltip=[
634
- alt.Tooltip("abbreviation:N", title="Method"),
635
- alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
636
- alt.Tooltip("mean_iters:Q", format=".2f", title="Mean Iterations"),
637
- ],
638
- )
639
- )
640
- if not fail_df_iters.empty:
641
- y_iters = (
642
- alt.Y("mean_iters:Q", title="Mean Iterations", scale=alt.Scale(domain=[0, 1]))
643
- if all_failed_iters
644
- else alt.Y("mean_iters:Q", title="Mean Iterations")
645
- )
646
- iters_layers.append(
647
- alt.Chart(fail_df_iters)
648
- .mark_point(shape="cross", angle=45, size=200, filled=True, opacity=1.0)
649
- .encode(
650
- x=alt.X(
651
- "ensemble_size:Q",
652
- title="Ensemble Size",
653
- axis=alt.Axis(values=iters_ticks, format="d"),
654
- ),
655
- y=y_iters,
656
- color=iters_color,
657
- tooltip=[
658
- alt.Tooltip("abbreviation:N", title="Method"),
659
- alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
660
- alt.Tooltip("mean_iters:Q", title="Value (all failed)"),
661
- ],
662
- )
663
- )
664
- if iters_layers:
665
- st.altair_chart(alt.layer(*iters_layers), use_container_width=True)
666
-
667
- # Failure panel — rendered regardless of whether the scored table has rows
668
- if show_failure_panel:
669
- failure_source = filtered[filtered[target_str_col] == selected_target]
670
- failure_source = failure_source[failure_source["abbreviation"].isin(selected_methods)]
671
- if not failure_source.empty:
672
- failure_df = failure_source.groupby(
673
- ["abbreviation", "ensemble_size"], as_index=False
674
- ).agg(mean_failure_rate=("failure_rate", "mean"))
675
- failure_df = failure_df.sort_values("ensemble_size")
676
-
677
- st.subheader(f"Failure Rate of Hitting Target {selected_target}")
678
- ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
679
- failure_chart = (
680
- alt.Chart(failure_df)
681
- .mark_bar()
682
- .encode(
683
- x=alt.X(
684
- "ensemble_size:O",
685
- title="Ensemble Size",
686
- sort=[str(e) for e in ens_ticks_fail],
687
- axis=alt.Axis(labelAngle=0),
688
- ),
689
- xOffset=alt.XOffset("abbreviation:N"),
690
- y=alt.Y(
691
- "mean_failure_rate:Q",
692
- title="Failure Rate (%)",
693
- scale=alt.Scale(domain=[0, 100]),
694
- ),
695
- color=_method_color(failure_df["abbreviation"].unique().tolist()),
696
- tooltip=[
697
- alt.Tooltip("abbreviation:N", title="Method"),
698
- alt.Tooltip("ensemble_size:O", title="Ensemble Size"),
699
- alt.Tooltip("mean_failure_rate:Q", format=".1f", title="Failure Rate (%)"),
700
- ],
701
- )
702
- )
703
- ceiling_line = (
704
- alt.Chart(alt.Data(values=[{}]))
705
- .mark_rule(color="grey", strokeDash=[4, 4])
706
- .encode(y=alt.datum(100))
707
- )
708
- st.altair_chart(failure_chart + ceiling_line, use_container_width=True)
709
-
710
- st.divider()
711
-
712
- # --- Leaderboard table (rendered below the charts) ---
713
- table_column_order = [
714
- "Placement",
715
- "abbreviation",
716
- "Method",
717
- "update_type",
718
- "parallelism",
719
- "method_goal",
720
- "emulator_use",
721
- "Score",
722
- "Mean Forward Model Runs",
723
- "Minimum Forward Model Runs",
724
- "Mean Failure Rate (%)",
725
- "Optimal Ensemble Size",
726
- "Ensemble Sizes Used",
727
- ]
728
-
729
- if leaderboard_df.empty:
730
- st.warning(
731
- "All runs failed to reach the target at this selection. "
732
- "See the failure rate chart above."
733
- if show_failure_panel
734
- else "No rows available for the current benchmark/target selection."
735
- )
736
- else:
737
- st.subheader(f"Ranked Leaderboard — {selected_benchmark}")
738
- st.dataframe(
739
- leaderboard_df,
740
- hide_index=True,
741
- use_container_width=True,
742
- column_config={
743
- "Placement": st.column_config.TextColumn("Placement"),
744
- "update_type": st.column_config.TextColumn("Update Type"),
745
- "parallelism": st.column_config.TextColumn("Parallelism"),
746
- "method_goal": st.column_config.TextColumn("Method Goal"),
747
- "emulator_use": st.column_config.TextColumn("Emulator Use"),
748
- "Method": st.column_config.TextColumn("Method"),
749
- "abbreviation": st.column_config.TextColumn("Abbrev."),
750
- "Mean Forward Model Runs": st.column_config.NumberColumn("Mean Forward Model Runs", format="%.4f"),
751
- "Minimum Forward Model Runs": st.column_config.NumberColumn("Minimum Forward Model Runs", format="%.4f"),
752
- "Score": st.column_config.ProgressColumn("Score (0-100)", min_value=0.0, max_value=100.0, format="%.1f"),
753
- "Optimal Ensemble Size": st.column_config.NumberColumn("Mean Optimal Ensemble Size", format="%.2f"),
754
- "Mean Failure Rate (%)": st.column_config.NumberColumn("Mean Failure Rate (%)", format="%.1f"),
755
- "Ensemble Sizes Used": st.column_config.NumberColumn("Ensemble Sizes Used", format="%d"),
756
- },
757
- column_order=table_column_order,
758
- )
759
-
760
- st.info(
761
- f"Score is a normalized 0–100 ranking based on **{score_basis}**. "
762
- "Values are computed from all ensemble sizes after averaging over random seeds."
763
- )
764
-
765
- st.caption("Top 3 are shown as podium spots; remaining methods are directly comparable via normalized score.")
766
-
767
- if raw_page is not None:
768
- st.page_link(raw_page, label="Open Raw Data & CSV Export", icon="🧾")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/common/method_registry.py CHANGED
@@ -4,151 +4,43 @@ import json
4
  from pathlib import Path
5
 
6
 
7
- # Taxonomy
8
- # --------
9
- # Every method carries four independent tags, each scoped to its own axis:
10
- #
11
- # ``parallelism`` — how the search explores parameter space:
12
- # "serial" — a single point estimate advanced step by step (ADAM, LM).
13
- # "parallel-independent" — a population of candidates updated with no coupling
14
- # between members (ABC's accepted samples, HM's per-wave
15
- # resampling from the non-implausible region).
16
- # "parallel-interacting" — an ensemble whose members are coupled through a shared
17
- # update each iteration (any Kalman-based method).
18
- #
19
- # ``update_type`` — the mechanism driving each update step:
20
- # "gradient" — follows the loss gradient or a Gauss-Newton approximation of it.
21
- # "kalman" — a (possibly linearized/unscented) Kalman-style ensemble update.
22
- # "general" — anything else (e.g. ABC's rejection sampling, HM's implausibility cuts).
23
- #
24
- # ``method_goal`` — what the method is built to report:
25
- # "optimization" — a single best-fit parameter estimate.
26
- # "uq" — the full posterior / parameter uncertainty. UQ methods can still
27
- # be scored on the Optimization leaderboard (usually less
28
- # competitive there, since they're not optimizing for speed-to-target).
29
- #
30
- # ``emulator_use`` — when/whether a surrogate model of the forward model is used:
31
- # "none" — samples/evaluates the true forward model throughout.
32
- # "within-optimize" — refits a surrogate at each iteration of the search (HM waves).
33
- # "after-optimize" — fits a surrogate once, after calibration finishes (CES).
34
- #
35
- # Note: Kalman methods are Bayesian in spirit too (approximate Gaussian posterior
36
- # updates) — ``update_type`` is about mechanism, not a "Bayesian vs not" philosophy.
37
  KNOWN_METHODS = {
38
  "teki": {
39
  "abbreviation": "TEKI",
40
  "Method": "Tikhonov Regularized Ensemble Kalman Inversion",
41
- "parallelism": "parallel-interacting",
42
- "update_type": "kalman",
43
- "method_goal": "optimization",
44
- "emulator_use": "none",
45
  "aliases": ["teki"],
46
  },
47
  "etki": {
48
  "abbreviation": "ETKI",
49
  "Method": "Ensemble Transform Kalman Inversion",
50
- "parallelism": "parallel-interacting",
51
- "update_type": "kalman",
52
- "method_goal": "optimization",
53
- "emulator_use": "none",
54
  "aliases": ["etki"],
55
  },
56
  "iekf": {
57
  "abbreviation": "IEKF",
58
  "Method": "Iterative Ensemble Kalman Filter",
59
- "parallelism": "parallel-interacting",
60
- "update_type": "kalman",
61
- "method_goal": "uq",
62
- "emulator_use": "none",
63
- "aliases": ["iekf", "gnsl", "gnki"],
64
  },
65
  "uki": {
66
  "abbreviation": "UKI",
67
  "Method": "Unscented Kalman Inversion",
68
- "parallelism": "parallel-interacting",
69
- "update_type": "kalman",
70
- "method_goal": "optimization",
71
- "emulator_use": "none",
72
  "aliases": ["uki"],
73
  },
74
  "abc": {
75
  "abbreviation": "ABC",
76
  "Method": "Approximate Bayesian Calibration",
77
- "parallelism": "parallel-independent",
78
- "update_type": "general",
79
- "method_goal": "uq",
80
- "emulator_use": "none",
81
  "aliases": ["abc"],
82
  },
83
  "hm": {
84
  "abbreviation": "HM",
85
  "Method": "History Matching",
86
- "parallelism": "parallel-independent",
87
- "update_type": "general",
88
- "method_goal": "uq",
89
- "emulator_use": "within-optimize",
90
  "aliases": ["hm"],
91
  },
92
- "ces-eki-dmc": {
93
- "abbreviation": "CES-EKI-DMC",
94
- "Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
95
- "parallelism": "parallel-interacting",
96
- "update_type": "kalman",
97
- "method_goal": "uq",
98
- "emulator_use": "after-optimize",
99
- "aliases": ["ces-eki-dmc"]
100
- },
101
- "ces-eki-const": {
102
- "abbreviation": "CES-EKI-CONST",
103
- "Method": "Calibrate Emulate Sample (EKI-Constant Scheduler)",
104
- "parallelism": "parallel-interacting",
105
- "update_type": "kalman",
106
- "method_goal": "uq",
107
- "emulator_use": "after-optimize",
108
- "aliases": ["ces-eki-const"]
109
- },
110
- "ces-iekf-const": {
111
- "abbreviation": "CES-IEKF-CONST",
112
- "Method": "Calibrate Emulate Sample (IEKF-Constant Scheduler)",
113
- "parallelism": "parallel-interacting",
114
- "update_type": "kalman",
115
- "method_goal": "uq",
116
- "emulator_use": "after-optimize",
117
- "aliases": ["ces-iekf-const"]
118
- },
119
- "adam": {
120
- "abbreviation": "ADAM",
121
- "Method": "Adaptive Moment Estimation",
122
- "parallelism": "serial",
123
- "update_type": "gradient",
124
- "method_goal": "optimization",
125
- "emulator_use": "none",
126
- "aliases": ["adam"],
127
- },
128
- "lm": {
129
- "abbreviation": "LM",
130
- "Method": "Levenberg-Marquardt",
131
- "parallelism": "serial",
132
- "update_type": "gradient",
133
- "method_goal": "optimization",
134
- "emulator_use": "none",
135
- "aliases": ["lm", "levenberg_marquardt", "levenberg-marquardt", "gradient_descent"],
136
- },
137
- }
138
-
139
-
140
- # Vega tableau10 palette — one slot per method in KNOWN_METHODS declaration order.
141
- # New methods appended to KNOWN_METHODS get the next slot; existing colors never shift.
142
- _METHOD_PALETTE = [
143
- "#4c78a8", "#f58518", "#e45756", "#72b7b2", "#54a24b",
144
- "#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
145
- ]
146
-
147
- # Stable abbreviation → hex color mapping. Import this wherever Altair charts are built
148
- # so every plot in the app assigns the same color to each method.
149
- METHOD_COLORS: dict[str, str] = {
150
- meta["abbreviation"]: _METHOD_PALETTE[i % len(_METHOD_PALETTE)]
151
- for i, meta in enumerate(KNOWN_METHODS.values())
152
  }
153
 
154
 
 
4
  from pathlib import Path
5
 
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  KNOWN_METHODS = {
8
  "teki": {
9
  "abbreviation": "TEKI",
10
  "Method": "Tikhonov Regularized Ensemble Kalman Inversion",
11
+ "family": "Kalman",
 
 
 
12
  "aliases": ["teki"],
13
  },
14
  "etki": {
15
  "abbreviation": "ETKI",
16
  "Method": "Ensemble Transform Kalman Inversion",
17
+ "family": "Kalman",
 
 
 
18
  "aliases": ["etki"],
19
  },
20
  "iekf": {
21
  "abbreviation": "IEKF",
22
  "Method": "Iterative Ensemble Kalman Filter",
23
+ "family": "Kalman",
24
+ "aliases": ["iekf", "gnsl"],
 
 
 
25
  },
26
  "uki": {
27
  "abbreviation": "UKI",
28
  "Method": "Unscented Kalman Inversion",
29
+ "family": "Kalman",
 
 
 
30
  "aliases": ["uki"],
31
  },
32
  "abc": {
33
  "abbreviation": "ABC",
34
  "Method": "Approximate Bayesian Calibration",
35
+ "family": "Bayesian",
 
 
 
36
  "aliases": ["abc"],
37
  },
38
  "hm": {
39
  "abbreviation": "HM",
40
  "Method": "History Matching",
41
+ "family": "Bayesian",
 
 
 
42
  "aliases": ["hm"],
43
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
 
src/data_store.py CHANGED
@@ -2,8 +2,7 @@ from __future__ import annotations
2
 
3
  import importlib
4
  from pathlib import Path
5
- import warnings
6
- import numpy as np
7
  import pandas as pd
8
  import streamlit as st
9
 
@@ -23,136 +22,36 @@ except ModuleNotFoundError:
23
  )
24
 
25
 
26
- # ---------------------------------------------------------------------------
27
- # Optimization (RMSE) datasets
28
- # ---------------------------------------------------------------------------
29
-
30
  DATASET_FILES = {
31
  "L63": [
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
-
59
- EXPECTED_DIMS_OPT = ("algorithm_type", "rmse_target", "ensemble_size", "random_seed")
60
- EXPECTED_DIMS_UQ = ("random_seed", "ensemble_size", "k_iter", "coverage_quantile")
61
-
62
- # Method-taxonomy tag columns — one value per method, sourced from KNOWN_METHODS.
63
- # See src/common/method_registry.py for the axis definitions.
64
- TAXONOMY_TAG_COLUMNS = ["parallelism", "update_type", "method_goal", "emulator_use"]
65
-
66
-
67
- # ---------------------------------------------------------------------------
68
- # Uncertainty Quantification datasets
69
- # ---------------------------------------------------------------------------
70
- # Add entries here when UQ result NetCDF files are available. Each file must
71
- # carry a ``metric`` variable and a ``uq_target`` coordinate (in place of the
72
- # ``rmse_target`` used by the optimization files). The rest of the schema
73
- # (algorithm_type, ensemble_size, random_seed) is identical.
74
- #
75
- # Example:
76
- # UQ_DATASET_FILES = {
77
- # "L63": ["uq/l63_uq_results.nc"],
78
- # "L96": ["uq/l96_uq_results.nc"],
79
- # }
80
-
81
- UQ_DATASET_FILES: dict[str, list[str]] = {}
82
-
83
- # Canonical UQ target-scaling levels always offered in the UQ leaderboard selector.
84
- UQ_TARGET_LEVELS: list[float] = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5]
85
-
86
- # Budget-for-coverage source files: maps benchmark → list of (algorithm_type, nc_path) pairs.
87
- # Each NC must carry output_coverage (coverage_quantile, k_iter, ensemble_size, random_seed),
88
- # target_scaling, and output_dim.
89
- UQ_BUDGET_FILES: dict[str, list[tuple[str, str]]] = {
90
- "L63": [
91
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15_minimal.nc"),
92
- ("iekf", "gnki-uq_results/leaderboard_gnki_l63_2026-07-07_minimal.nc"),
93
- ("ces-eki-const", "ces-eki-const_results/ces-eki-const_l63_ensemble_results_2026-07-09_minimal.nc"),
94
- ("ces-iekf-const", "ces-iekf-const_results/ces-iekf-const_l63_ensemble_results_2026-07-17_minimal.nc"),
95
- ("hm", "history-matching-uq_results/history-matching_l63_ensemble_results_2026-07-20_minimal.nc"),
96
  ],
97
- "L96": [
98
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_ensemble_results_2026-06-15_minimal.nc"),
99
- ("iekf", "gnki-uq_results/leaderboard_gnki_l96_const-force_2026-07-07_minimal.nc"),
100
- ("ces-eki-const", "ces-eki-const_results/ces-eki-const_l96_ensemble_results_2026-07-09_minimal.nc"),
101
- ("ces-iekf-const", "ces-iekf-const_results/ces-iekf-const_l96_ensemble_results_2026-07-17_minimal.nc"),
102
- ("hm", "history-matching-uq_results/history-matching_l96_const-force_2026-07-20_minimal.nc"),
103
- ],
104
- "L96_NN_FORCING": [
105
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_nn_forcing_ensemble_results_2026-06-15_minimal.nc"),
106
- ("iekf", "gnki-uq_results/leaderboard_gnki_l96_flux-force_2026-07-07_minimal.nc"),
107
- ("ces-eki-const", "ces-eki-const_results/ces-eki-const_l96_nn_forcing_ensemble_results_2026-07-09_minimal.nc"),
108
- ("ces-iekf-const", "ces-iekf-const_results/ces-iekf-const_l96_nn_forcing_ensemble_results_2026-07-17_minimal.nc"),
109
- ("hm", "history-matching-uq_results/history-matching_l96_flux-force_2026-07-21_minimal.nc"),
110
- ],
111
- "L96_SPATIAL_FORCING": [
112
- ("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_spatial_forcing_ensemble_results_2026-06-15_minimal.nc"),
113
- ("iekf", "gnki-uq_results/leaderboard_gnki_l96_vec-force_2026-07-07_minimal.nc"),
114
- ("ces-eki-const", "ces-eki-const_results/ces-eki-const_l96_spatial_forcing_ensemble_results_2026-07-09_minimal.nc"),
115
- ("ces-iekf-const", "ces-iekf-const_results/ces-iekf-const_l96_spatial_forcing_ensemble_results_2026-07-17_minimal.nc"),
116
- ("hm", "history-matching-uq_results/history-matching_l96_vec-force_2026-07-22_minimal.nc"),
117
- ],
118
- }
119
-
120
- # Quantile levels used for the all-quantiles-satisfied budget-for-coverage condition.
121
- # Budget = N_ens · k_iter where first k s.t. |S(q)−q| ≤ c·√(q(1−q)/N_y) for ALL q below.
122
- UQ_COVERAGE_QUANTILES: list[float] = [0.15, 0.5, 0.85]
123
-
124
- # Physical dimensions of each benchmark: (param_dim, state_dim, output_dim).
125
- # Used only for column-header annotations in the suitability table.
126
- BENCHMARK_DIMS: dict[str, tuple[int, int, int]] = {
127
- "L63": (2, 3, 9),
128
- "L96": (1, 40, 80),
129
- "L96_NN_FORCING": (61, 100, 200),
130
- "L96_SPATIAL_FORCING": (40, 40, 80),
131
  }
132
 
133
 
134
 
135
- # ---------------------------------------------------------------------------
136
- # Private loader — shared by both public load_* functions
137
- # ---------------------------------------------------------------------------
138
-
139
- def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_nan_metric: bool = True) -> pd.DataFrame:
140
- """Load and merge NetCDF metric files into a long-format DataFrame.
141
 
142
- Parameters
143
- ----------
144
- dataset_files:
145
- Mapping of benchmark name → list of relative NetCDF paths under
146
- ``data/``.
147
- target_col:
148
- Name of the target-coordinate dimension in the NetCDF files, e.g.
149
- ``"rmse_target"`` or ``"uq_target"``.
150
- """
151
- if not dataset_files:
152
- return pd.DataFrame(
153
- columns=["benchmark", "algorithm_type", target_col, "ensemble_size", "metric", "failure_rate"]
154
- )
155
 
 
 
156
  xr = importlib.import_module("xarray")
157
 
158
  project_root = Path(__file__).resolve().parent.parent
@@ -160,7 +59,7 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
160
 
161
  records: list[pd.DataFrame] = []
162
 
163
- for benchmark_name, filenames in dataset_files.items():
164
  for filename in filenames:
165
  file_path = data_dir / filename
166
  if not file_path.exists():
@@ -173,58 +72,44 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
173
  continue
174
 
175
  metric = dataset["metric"]
176
- if not set(metric.dims).issubset(EXPECTED_DIMS_OPT):
177
- warnings.warn(
178
- f"{file_path.name}: metric dims {metric.dims} contain unexpected "
179
- f"dimensions not in EXPECTED_DIMS_OPT {EXPECTED_DIMS_OPT} — skipping"
180
- )
181
- continue
182
-
183
- # Track failures: -1 sentinel OR NaN (some methods use NaN instead of -1)
184
- failures_count = ((metric == -1) | metric.isnull()).sum(dim="random_seed")
185
 
186
  failure_rate = (failures_count / dataset.sizes["random_seed"]) * 100
187
-
188
- # Filter out failed runs (metric == -1)
189
  metric = metric.where(metric != -1)
190
-
191
  metric_mean = metric.mean(dim="random_seed", skipna=True)
192
  df = metric_mean.to_dataframe(name="metric").reset_index()
193
-
194
  df_failures = failure_rate.to_dataframe(name="failure_rate").reset_index()
195
  df["failure_rate"] = df_failures["failure_rate"]
196
 
197
  df["benchmark"] = benchmark_name
198
-
199
  if "algorithm_type" not in df.columns:
200
  if "abc" in file_path.name:
201
  df["algorithm_type"] = "abc"
202
  elif "hm" in file_path.name:
203
  df["algorithm_type"] = "hm"
204
-
205
  if "ensemble_size" not in df.columns:
206
  if "abc" in file_path.name:
207
  df["ensemble_size"] = 1
208
-
209
- required_cols = ["benchmark", "algorithm_type", target_col, "ensemble_size", "metric", "failure_rate"]
210
- missing_cols = [c for c in required_cols if c not in df.columns]
211
  if missing_cols:
212
  st.warning(
213
  f"Skipping {file_path.name}: metric dataframe is missing expected columns {missing_cols}"
214
  )
215
  continue
216
 
217
- # Tag rows from files with exactly one ensemble size. Only those rows
218
- # are retained when metric is NaN — they represent a method that was
219
- # genuinely attempted at that size but every seed failed. NaN rows from
220
- # multi-size files are placeholder entries for sizes never actually run.
221
- df["_single_ens"] = dataset.sizes.get("ensemble_size", 1) == 1
222
- records.append(df[required_cols + ["_single_ens"]])
223
 
224
  if not records:
225
- return pd.DataFrame(
226
- columns=["benchmark", "algorithm_type", target_col, "ensemble_size", "metric", "failure_rate"]
227
- )
228
 
229
  merged = pd.concat(records, ignore_index=True)
230
  merged["algorithm_alias"] = merged["algorithm_type"].map(normalize_method_name)
@@ -232,24 +117,16 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
232
  merged["ensemble_size"] = pd.to_numeric(merged["ensemble_size"], errors="coerce")
233
  merged["metric"] = pd.to_numeric(merged["metric"], errors="coerce")
234
  merged["failure_rate"] = pd.to_numeric(merged["failure_rate"], errors="coerce").fillna(0.0)
235
- merged = merged.dropna(subset=["ensemble_size"])
236
- if drop_nan_metric:
237
- # Keep NaN-metric rows only when they came from a single-ensemble-size file
238
- # AND every seed failed (failure_rate ≈ 100 %). Those represent a method that
239
- # was genuinely attempted but never converged. NaN rows from multi-size files
240
- # are placeholders for ensemble sizes that were never actually run.
241
- single_ens = merged.pop("_single_ens").fillna(False)
242
- merged = merged[merged["metric"].notna() | (single_ens & (merged["failure_rate"] >= 99.9))]
243
- else:
244
- merged = merged.drop(columns=["_single_ens"], errors="ignore")
245
  merged["ensemble_size"] = merged["ensemble_size"].astype(int)
 
 
246
  merged["abbreviation"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("abbreviation"))
247
  merged["Method"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("Method"))
248
- for tag_col in TAXONOMY_TAG_COLUMNS:
249
- merged[tag_col] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get(tag_col))
250
- merged[tag_col] = merged[tag_col].fillna("unmapped")
251
  merged["abbreviation"] = merged["abbreviation"].fillna(merged["algorithm_type"].str.upper())
252
  merged["Method"] = merged["Method"].fillna(merged["abbreviation"])
 
253
 
254
  try:
255
  dump_method_registry_snapshot(project_root=project_root, observed_methods=set(merged["algorithm_type"].unique()))
@@ -259,161 +136,23 @@ def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_na
259
  return merged
260
 
261
 
262
- # ---------------------------------------------------------------------------
263
- # Public cached loaders
264
- # ---------------------------------------------------------------------------
265
-
266
- @st.cache_data(show_spinner=False)
267
- def load_metric_store() -> pd.DataFrame:
268
- """Load the optimization (RMSE) metric store from ``DATASET_FILES``."""
269
- return _load_store(DATASET_FILES, "rmse_target")
270
-
271
-
272
- @st.cache_data(show_spinner=False)
273
- def load_uq_store() -> pd.DataFrame:
274
- """Load the UQ metric store derived from budget-for-coverage computations.
275
-
276
- Returns a DataFrame compatible with ``render_leaderboard`` where ``metric``
277
- is the mean budget (N_ens·k_iter) to satisfy the coverage condition across
278
- all three quantiles in ``UQ_COVERAGE_QUANTILES``. ``uq_target`` is the
279
- target-scaling level c. Failure means the coverage target was never
280
- reached within the k_iter range.
281
- """
282
- budget_df = load_uq_budget_store()
283
- if budget_df.empty:
284
  return pd.DataFrame(
285
  columns=[
286
- "benchmark", "algorithm_type", "algorithm_alias", "abbreviation",
287
- "Method", *TAXONOMY_TAG_COLUMNS, "uq_target", "ensemble_size", "metric", "failure_rate",
 
 
 
 
 
288
  ]
289
  )
290
- df = budget_df.copy()
291
- df["metric"] = df["mean_budget"]
292
- df["algorithm_alias"] = df["algorithm_type"].map(normalize_method_name)
293
- df["Method"] = df["algorithm_type"].map(
294
- lambda m: get_method_meta(canonicalize_method_name(normalize_method_name(m))).get(
295
- "Method", m.upper()
296
- )
297
- )
298
- return df[
299
- [
300
- "benchmark", "algorithm_type", "algorithm_alias", "abbreviation",
301
- "Method", *TAXONOMY_TAG_COLUMNS, "uq_target", "ensemble_size", "metric", "failure_rate",
302
- ]
303
- ]
304
-
305
-
306
- @st.cache_data(show_spinner=False)
307
- def load_uq_budget_store() -> pd.DataFrame:
308
- """Compute budget-for-coverage and iterations-for-coverage from ensemble result NC files.
309
-
310
- For each (target_scaling c, ensemble_size N, random_seed):
311
- Find the minimum k_iter s.t. |S(q) − q| ≤ c·√(q(1−q)/N_y) for ALL q in
312
- UQ_COVERAGE_QUANTILES. Budget = N · k_iter; NaN when target never reached.
313
-
314
- Returns a DataFrame with columns:
315
- benchmark, algorithm_type, abbreviation, parallelism, update_type,
316
- method_goal, emulator_use, uq_target, ensemble_size, mean_budget,
317
- mean_iters, failure_count, failure_rate, n_seeds
318
- """
319
- xr = importlib.import_module("xarray")
320
-
321
- project_root = Path(__file__).resolve().parent.parent
322
- data_dir = project_root / "data"
323
-
324
- bq_vals = np.array(UQ_COVERAGE_QUANTILES)
325
- records: list[dict] = []
326
-
327
- for benchmark_name, method_files in UQ_BUDGET_FILES.items():
328
- for algorithm_type, filename in method_files:
329
- file_path = data_dir / filename
330
- if not file_path.exists():
331
- warnings.warn(f"UQ budget file {file_path} does not exist")
332
- continue
333
-
334
- with xr.open_dataset(file_path) as ds:
335
- if "output_coverage" not in ds:
336
- warnings.warn(f"{file_path.name}: missing output_coverage")
337
- continue
338
 
339
- if set(ds["output_coverage"].dims) != set(EXPECTED_DIMS_UQ):
340
- warnings.warn(
341
- f"{file_path.name}: output_coverage dims {ds['output_coverage'].dims} "
342
- f"do not match EXPECTED_DIMS_UQ {EXPECTED_DIMS_UQ} skipping"
343
- )
344
- continue
345
-
346
- cov_q = ds["coverage_quantile"].values # (n_cov_q,)
347
- k_vals = ds["k_iter"].values # (n_k,) 1-indexed
348
- ens_vals = ds["ensemble_size"].values # (n_ens,)
349
- ts_vals = ds["target_scaling"].values # (n_ts,)
350
- # xarray drops dimensions that no variable uses; read output_dim
351
- # directly from the underlying netCDF4 file to handle minimal files.
352
- nc4 = importlib.import_module("netCDF4")
353
- with nc4.Dataset(file_path) as _nc:
354
- if "output_dim" not in _nc.dimensions:
355
- warnings.warn(f"{file_path.name}: missing output_dim dimension")
356
- continue
357
- n_y = len(_nc.dimensions["output_dim"])
358
- # dims: (coverage_quantile, k_iter, ensemble_size, random_seed)
359
- cov_np = ds["output_coverage"].values
360
-
361
- n_rng = cov_np.shape[3]
362
-
363
- # Indices into coverage_quantile for [0.15, 0.5, 0.85]
364
- bq_idx = [int(np.argmin(np.abs(cov_q - q))) for q in bq_vals]
365
-
366
- for si, c in enumerate(ts_vals):
367
- tol = c * np.sqrt(bq_vals * (1.0 - bq_vals) / n_y)
368
-
369
- for ei, ens_f in enumerate(ens_vals):
370
- N_ens = int(ens_f)
371
- budgets = np.full(n_rng, np.nan)
372
- kiters = np.full(n_rng, np.nan)
373
-
374
- for ri in range(n_rng):
375
- for ki, kv in enumerate(k_vals):
376
- s_q = cov_np[bq_idx, ki, ei, ri]
377
- if np.any(np.isnan(s_q)):
378
- continue
379
- if np.all(np.abs(s_q - bq_vals) <= tol):
380
- budgets[ri] = N_ens * float(kv)
381
- kiters[ri] = float(kv)
382
- break
383
-
384
- valid_b = budgets[~np.isnan(budgets)]
385
- valid_k = kiters[~np.isnan(kiters)]
386
- n_fail = n_rng - len(valid_b)
387
- records.append(
388
- {
389
- "benchmark": benchmark_name,
390
- "algorithm_type": algorithm_type,
391
- "uq_target": float(c),
392
- "ensemble_size": N_ens,
393
- "mean_budget": float(np.mean(valid_b)) if len(valid_b) > 0 else np.nan,
394
- "mean_iters": float(np.mean(valid_k)) if len(valid_k) > 0 else np.nan,
395
- "failure_count": n_fail,
396
- "failure_rate": 100.0 * n_fail / n_rng,
397
- "n_seeds": n_rng,
398
- }
399
- )
400
-
401
- if not records:
402
- return pd.DataFrame(
403
- columns=[
404
- "benchmark", "algorithm_type", "abbreviation", *TAXONOMY_TAG_COLUMNS,
405
- "uq_target", "ensemble_size", "mean_budget", "mean_iters",
406
- "failure_count", "failure_rate", "n_seeds",
407
- ]
408
- )
409
-
410
- df = pd.DataFrame(records)
411
-
412
- def _meta(m: str, key: str, fallback: str) -> str:
413
- return get_method_meta(canonicalize_method_name(normalize_method_name(m))).get(key, fallback)
414
-
415
- df["abbreviation"] = df["algorithm_type"].map(lambda m: _meta(m, "abbreviation", m.upper()))
416
- for tag_col in TAXONOMY_TAG_COLUMNS:
417
- df[tag_col] = df["algorithm_type"].map(lambda m, col=tag_col: _meta(m, col, "unmapped"))
418
- df["Method"] = df["algorithm_type"].map(lambda m: _meta(m, "Method", m.upper()))
419
- return df
 
2
 
3
  import importlib
4
  from pathlib import Path
5
+ import warnings
 
6
  import pandas as pd
7
  import streamlit as st
8
 
 
22
  )
23
 
24
 
 
 
 
 
25
  DATASET_FILES = {
26
  "L63": [
27
  "l63_ensemble_results.nc",
28
  "UKI_results/uki_l63_ensemble_results.nc",
29
  "bayesian/l63_abc.nc",
30
+ "bayesian/l63_hm.nc"
 
 
31
  ],
32
  "L96": [
33
  "l96_ensemble_results.nc",
34
  "UKI_results/uki_l96_ensemble_results.nc",
35
+ "bayesian/l96_abc.nc"
 
 
36
  ],
37
  "L96_NN_FORCING": [
38
  "l96_nn_forcing_ensemble_results.nc",
39
+ "UKI_results/uki_l96_nn_forcing_ensemble_results.nc"
40
  ],
41
  "L96_SPATIAL_FORCING": [
42
  "l96_spatial_forcing_ensemble_results.nc",
43
  "UKI_results/uki_l96_spatial_forcing_ensemble_results.nc",
44
+ "bayesian/l96_varying_abc.nc"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
 
48
 
49
 
50
+ EXPECTED_DIMS = ("algorithm_type", "rmse_target", "ensemble_size", "random_seed")
 
 
 
 
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ @st.cache_data(show_spinner=False)
54
+ def load_metric_store() -> pd.DataFrame:
55
  xr = importlib.import_module("xarray")
56
 
57
  project_root = Path(__file__).resolve().parent.parent
 
59
 
60
  records: list[pd.DataFrame] = []
61
 
62
+ for benchmark_name, filenames in DATASET_FILES.items():
63
  for filename in filenames:
64
  file_path = data_dir / filename
65
  if not file_path.exists():
 
72
  continue
73
 
74
  metric = dataset["metric"]
75
+
76
+ # Track failures (metric == -1) as a percentage
77
+ failures_count = (metric == -1).sum(dim="random_seed")
 
 
 
 
 
 
78
 
79
  failure_rate = (failures_count / dataset.sizes["random_seed"]) * 100
80
+
81
+ # Filter out failed calibrations (metric == -1)
82
  metric = metric.where(metric != -1)
83
+
84
  metric_mean = metric.mean(dim="random_seed", skipna=True)
85
  df = metric_mean.to_dataframe(name="metric").reset_index()
86
+
87
  df_failures = failure_rate.to_dataframe(name="failure_rate").reset_index()
88
  df["failure_rate"] = df_failures["failure_rate"]
89
 
90
  df["benchmark"] = benchmark_name
91
+
92
  if "algorithm_type" not in df.columns:
93
  if "abc" in file_path.name:
94
  df["algorithm_type"] = "abc"
95
  elif "hm" in file_path.name:
96
  df["algorithm_type"] = "hm"
97
+
98
  if "ensemble_size" not in df.columns:
99
  if "abc" in file_path.name:
100
  df["ensemble_size"] = 1
101
+
102
+ missing_cols = [c for c in ["benchmark", "algorithm_type", "rmse_target", "ensemble_size", "metric", "failure_rate"] if c not in df.columns]
 
103
  if missing_cols:
104
  st.warning(
105
  f"Skipping {file_path.name}: metric dataframe is missing expected columns {missing_cols}"
106
  )
107
  continue
108
 
109
+ records.append(df[["benchmark", "algorithm_type", "rmse_target", "ensemble_size", "metric", "failure_rate"]])
 
 
 
 
 
110
 
111
  if not records:
112
+ return pd.DataFrame(columns=["benchmark", "algorithm_type", "rmse_target", "ensemble_size", "metric", "failure_rate"])
 
 
113
 
114
  merged = pd.concat(records, ignore_index=True)
115
  merged["algorithm_alias"] = merged["algorithm_type"].map(normalize_method_name)
 
117
  merged["ensemble_size"] = pd.to_numeric(merged["ensemble_size"], errors="coerce")
118
  merged["metric"] = pd.to_numeric(merged["metric"], errors="coerce")
119
  merged["failure_rate"] = pd.to_numeric(merged["failure_rate"], errors="coerce").fillna(0.0)
120
+ merged = merged.dropna(subset=["ensemble_size", "metric"])
 
 
 
 
 
 
 
 
 
121
  merged["ensemble_size"] = merged["ensemble_size"].astype(int)
122
+ merged["forward_model_runs"] = merged["metric"]
123
+
124
  merged["abbreviation"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("abbreviation"))
125
  merged["Method"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("Method"))
126
+ merged["family"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("family"))
 
 
127
  merged["abbreviation"] = merged["abbreviation"].fillna(merged["algorithm_type"].str.upper())
128
  merged["Method"] = merged["Method"].fillna(merged["abbreviation"])
129
+ merged["family"] = merged["family"].fillna("Kalman")
130
 
131
  try:
132
  dump_method_registry_snapshot(project_root=project_root, observed_methods=set(merged["algorithm_type"].unique()))
 
136
  return merged
137
 
138
 
139
+ def build_leaderboard(metric_store: pd.DataFrame) -> pd.DataFrame:
140
+ if metric_store.empty:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  return pd.DataFrame(
142
  columns=[
143
+ "family",
144
+ "Method",
145
+ "abbreviation",
146
+ "benchmark",
147
+ "Forward Model Runs",
148
+ "Optimal Ensemble Size",
149
+ "Target Level",
150
  ]
151
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ best_idx = metric_store.groupby(["benchmark", "algorithm_type"])["metric"].idxmin()
154
+ best = metric_store.loc[best_idx].copy()
155
+ best = best.rename(columns={"metric": "Forward Model Runs", "rmse_target": "Target Level", "ensemble_size": "Optimal Ensemble Size"})
156
+ return best[["family", "Method", "abbreviation", "benchmark", "Forward Model Runs", "Optimal Ensemble Size", "Target Level"]].sort_values(
157
+ ["benchmark", "Forward Model Runs"], ascending=[True, True]
158
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/media/posterior_ribbons_20_13_k5.png DELETED

Git LFS Details

  • SHA256: a20370c35c70c07857dc47237fc5f2f83fed117352a908e30a5e51efd60b64b6
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
src/media/posterior_ribbons_const-force_12_1_k3.png DELETED

Git LFS Details

  • SHA256: 4fc26312d47c448a9a8dc1a6ecee59814cc2d7fce77cf0fc04648efdd7e1a67d
  • Pointer size: 131 Bytes
  • Size of remote file: 194 kB
src/media/posterior_ribbons_flux-force_80_1_k3.png DELETED

Git LFS Details

  • SHA256: 10043b9fc650de268ea24614c7beb6056ca5850159f2c70c85eeb434b59d81bf
  • Pointer size: 131 Bytes
  • Size of remote file: 296 kB
src/media/posterior_ribbons_vec-force_65_1_k3.png DELETED

Git LFS Details

  • SHA256: 1f9b165d98e5386d737c57df079f1e9a470caf17ab4e805901b06a3682542afb
  • Pointer size: 131 Bytes
  • Size of remote file: 296 kB
src/pages/MethodDetails.py CHANGED
@@ -15,8 +15,6 @@ st.set_page_config(page_title="Method Details", page_icon="📘", layout="wide")
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
 
@@ -33,31 +31,31 @@ if metric_store.empty:
33
  st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
34
  st.stop()
35
 
36
- methods_df = metric_store[["Method", "abbreviation"]].drop_duplicates().sort_values("abbreviation")
37
 
38
  abbrs = methods_df["abbreviation"].tolist()
39
 
40
  # Metadata dictionary (can be moved to a JSON/YAML later)
41
  method_meta = {
42
  "TEKI": {
43
- "citation": "Chada et al., SIAM/ASA J. UQ, 2020",
44
  "url": "https://doi.org/10.1137/17M114402X",
45
- "summary": "EKI variant with Tikhonov regularization that adds a penalty term to prevent ensemble collapse and improve stability on nonlinear problems.",
46
  },
47
  "ETKI": {
48
- "citation": "Schillings & Stuart, Numer. Math., 2017",
49
- "url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
50
- "summary": "Ensemble Transform Kalman Inversion applies an ensemble-space transform update that preserves the ensemble mean while reducing variance inflation.",
51
  },
52
  "IEKF": {
53
- "citation": "Iglesias, Inverse Problems, 2016",
54
- "url": "https://doi.org/10.1088/0266-5611/32/2/025002",
55
- "summary": "Regularizing iterative ensemble Kalman method; repeatedly refines the ensemble around a regularized Gauss-Newton step for nonlinear inverse problems.",
56
  },
57
  "UKI": {
58
- "citation": "Huang, Huang & Stuart, Physica D, 2022",
59
- "url": "https://clima.github.io/EnsembleKalmanProcesses.jl/dev/",
60
- "summary": "Unscented Kalman Inversion — propagates a deterministic set of sigma points through the forward model to estimate mean and covariance without linearization.",
61
  },
62
  "ABC": {
63
  "citation": "Approximate Bayesian Calibration",
@@ -65,35 +63,10 @@ method_meta = {
65
  "summary": "Sample without exact likelihoods until error falls below a target convergence.",
66
  },
67
  "HM": {
68
- "citation": "Williamson et al. 2013; King et al. 2025",
69
  "url": "https://example.com/hm",
70
  "summary": "Iterative constraint of parameter space using wave reductions.",
71
  },
72
- "CES-EKI-DMC": {
73
- "citation": "Cleary et al., J. Comput. Phys., 2021",
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
- "CES-EKI-CONST": {
78
- "citation": "Cleary et al., J. Comput. Phys., 2021",
79
- "url": "https://doi.org/10.1016/j.jcp.2020.109716",
80
- "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.",
81
- },
82
- "CES-IEKF-CONST": {
83
- "citation": "Cleary et al., J. Comput. Phys., 2021; Iglesias, Inverse Problems, 2016",
84
- "url": "https://doi.org/10.1016/j.jcp.2020.109716",
85
- "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.",
86
- },
87
- "ADAM": {
88
- "citation": "Kingma & Ba, ICLR, 2015",
89
- "url": "https://doi.org/10.48550/arXiv.1412.6980",
90
- "summary": "Adaptive Moment Estimation — gradient-based optimizer that adapts per-parameter learning rates using first and second moment estimates of the gradient.",
91
- },
92
- "LM": {
93
- "citation": "Levenberg, 1944; Marquardt, 1963; Fletcher, 1971",
94
- "url": "https://doi.org/10.1090/qam/10666",
95
- "summary": "Levenberg-Marquardt — damped least-squares algorithm that interpolates between gradient descent and Gauss-Newton steps for efficient nonlinear least-squares minimization.",
96
- },
97
  }
98
 
99
  # Selection UI (defaults to query param if valid)
@@ -129,7 +102,7 @@ st.markdown("**Benchmark Slice**")
129
  slice_df = metric_store[metric_store["abbreviation"] == sel].copy()
130
  slice_df = slice_df.sort_values(["benchmark", "rmse_target", "ensemble_size"])
131
 
132
- target_options = sorted(slice_df["rmse_target"].astype(str).unique().tolist())
133
  selected_target = st.radio("RMSE Target Level", options=target_options, horizontal=True)
134
 
135
  best_table_view = slice_df[slice_df["rmse_target"].astype(str) == selected_target]
@@ -138,7 +111,7 @@ best_idx = best_table_view.groupby("benchmark")["metric"].idxmin()
138
  best_ensemble_df = best_table_view.loc[best_idx, ["benchmark", "rmse_target", "ensemble_size", "metric", "failure_rate"]].rename(
139
  columns={"metric": "Mean Forward Model Runs", "failure_rate": "Failure Rate (%)", "ensemble_size": "Optimal Ensemble Size", "rmse_target": "RMSE Target"}
140
  )
141
- st.dataframe(best_ensemble_df, hide_index=True, use_container_width=True)
142
 
143
  if sel == "HM":
144
  st.markdown("### Failure Analysis")
@@ -172,7 +145,7 @@ st.markdown("**Best by Benchmark**")
172
  st.dataframe(
173
  best_by_benchmark,
174
  hide_index=True,
175
- use_container_width=True,
176
  column_config={
177
  "benchmark": st.column_config.TextColumn("Benchmark"),
178
  "Best Mean Forward Model Runs": st.column_config.NumberColumn("Best Mean Forward Model Runs", format="%.4f"),
@@ -196,13 +169,13 @@ chart = (
196
  tooltip=["benchmark", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
197
  )
198
  )
199
- st.altair_chart(chart, use_container_width=True)
200
 
201
  st.markdown("**All Averaged Configurations for Method**")
202
  st.dataframe(
203
  slice_df[["benchmark", "algorithm_alias", "rmse_target", "ensemble_size", "metric"]],
204
  hide_index=True,
205
- use_container_width=True,
206
  column_config={
207
  "benchmark": st.column_config.TextColumn("Benchmark"),
208
  "algorithm_alias": st.column_config.TextColumn("Source Alias"),
@@ -212,4 +185,4 @@ st.dataframe(
212
  },
213
  )
214
 
215
- st.page_link("pages/OptimizationLeaderboard.py", label="⬅ Back to Optimization Leaderboard", icon="↩️")
 
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/MethodDetails.py", label="Methods", icon="📘")
19
  st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="🧾")
20
 
 
31
  st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
32
  st.stop()
33
 
34
+ methods_df = metric_store[["family", "Method", "abbreviation"]].drop_duplicates().sort_values("abbreviation")
35
 
36
  abbrs = methods_df["abbreviation"].tolist()
37
 
38
  # Metadata dictionary (can be moved to a JSON/YAML later)
39
  method_meta = {
40
  "TEKI": {
41
+ "citation": "Chada et al., SIAM/ASA J. UQ, 2018",
42
  "url": "https://doi.org/10.1137/17M114402X",
43
+ "summary": "EKI variant with Tikhonov regularization for stability.",
44
  },
45
  "ETKI": {
46
+ "citation": "Ensemble transform variant (citation to be added)",
47
+ "url": "https://example.com/etki",
48
+ "summary": "Transform-based Kalman inversion using ensemble-space updates.",
49
  },
50
  "IEKF": {
51
+ "citation": "Iterative ensemble Kalman filter (citation to be added)",
52
+ "url": "https://example.com/iekf",
53
+ "summary": "Iterative Kalman updates for nonlinear inverse problems.",
54
  },
55
  "UKI": {
56
+ "citation": "Unscented transform-based inversion (placeholder)",
57
+ "url": "https://example.com/uki",
58
+ "summary": "Uses sigma points to propagate uncertainty without linearization.",
59
  },
60
  "ABC": {
61
  "citation": "Approximate Bayesian Calibration",
 
63
  "summary": "Sample without exact likelihoods until error falls below a target convergence.",
64
  },
65
  "HM": {
66
+ "citation": "History Matching",
67
  "url": "https://example.com/hm",
68
  "summary": "Iterative constraint of parameter space using wave reductions.",
69
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
71
 
72
  # Selection UI (defaults to query param if valid)
 
102
  slice_df = metric_store[metric_store["abbreviation"] == sel].copy()
103
  slice_df = slice_df.sort_values(["benchmark", "rmse_target", "ensemble_size"])
104
 
105
+ target_options = ["1.0", "1.1", "1.2"]
106
  selected_target = st.radio("RMSE Target Level", options=target_options, horizontal=True)
107
 
108
  best_table_view = slice_df[slice_df["rmse_target"].astype(str) == selected_target]
 
111
  best_ensemble_df = best_table_view.loc[best_idx, ["benchmark", "rmse_target", "ensemble_size", "metric", "failure_rate"]].rename(
112
  columns={"metric": "Mean Forward Model Runs", "failure_rate": "Failure Rate (%)", "ensemble_size": "Optimal Ensemble Size", "rmse_target": "RMSE Target"}
113
  )
114
+ st.dataframe(best_ensemble_df, use_container_width=True, hide_index=True)
115
 
116
  if sel == "HM":
117
  st.markdown("### Failure Analysis")
 
145
  st.dataframe(
146
  best_by_benchmark,
147
  hide_index=True,
148
+ width="stretch",
149
  column_config={
150
  "benchmark": st.column_config.TextColumn("Benchmark"),
151
  "Best Mean Forward Model Runs": st.column_config.NumberColumn("Best Mean Forward Model Runs", format="%.4f"),
 
169
  tooltip=["benchmark", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
170
  )
171
  )
172
+ st.altair_chart(chart, width="stretch")
173
 
174
  st.markdown("**All Averaged Configurations for Method**")
175
  st.dataframe(
176
  slice_df[["benchmark", "algorithm_alias", "rmse_target", "ensemble_size", "metric"]],
177
  hide_index=True,
178
+ width="stretch",
179
  column_config={
180
  "benchmark": st.column_config.TextColumn("Benchmark"),
181
  "algorithm_alias": st.column_config.TextColumn("Source Alias"),
 
185
  },
186
  )
187
 
188
+ st.page_link("streamlit_app.py", label="⬅ Back to Leaderboard", icon="↩️")