initial UQ and Claude interface
#2
by odunbar - opened
- .cache/known_methods_snapshot.json +10 -1
- .claude/settings.json +79 -0
- .claude/skills/leaderboard-manager/SKILL.md +200 -0
- .claude/skills/leaderboard-manager/references/add-method.md +138 -0
- .claude/skills/leaderboard-manager/references/add-uq-data.md +149 -0
- .claude/skills/leaderboard-manager/references/assess.md +84 -0
- .claude/skills/leaderboard-manager/references/manage-display.md +218 -0
- .claude/skills/leaderboard-manager/references/move-between-boards.md +116 -0
- .claude/skills/leaderboard-manager/references/populate-metadata.md +146 -0
- .gitattributes +1 -0
- .gitignore +34 -3
- CLAUDE.md +185 -0
- README.md +120 -0
- data/ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15_minimal.nc +3 -0
- data/ces-eki-dmc_results/ces-eki-dmc_l96_ensemble_results_2026-06-15_minimal.nc +3 -0
- data/ces-eki-dmc_results/ces-eki-dmc_l96_nn_forcing_ensemble_results_2026-06-15_minimal.nc +3 -0
- data/ces-eki-dmc_results/ces-eki-dmc_l96_spatial_forcing_ensemble_results_2026-06-15_minimal.nc +3 -0
- requirements.txt +4 -1
- src/common/leaderboard.py +660 -0
- src/common/method_registry.py +7 -1
- src/data_store.py +238 -34
- src/media/posterior_ribbons_20_13_k5.png +3 -0
- src/media/posterior_ribbons_const-force_12_1_k3.png +3 -0
- src/media/posterior_ribbons_flux-force_80_1_k3.png +3 -0
- src/media/posterior_ribbons_vec-force_65_1_k3.png +3 -0
- src/pages/MethodDetails.py +25 -18
- src/pages/OptimizationLeaderboard.py +35 -0
- src/pages/RawData.py +4 -2
- src/pages/UQLeaderboard.py +40 -0
- src/streamlit_app.py +92 -313
.cache/known_methods_snapshot.json
CHANGED
|
@@ -22,7 +22,8 @@
|
|
| 22 |
"family": "Kalman",
|
| 23 |
"aliases": [
|
| 24 |
"iekf",
|
| 25 |
-
"gnsl"
|
|
|
|
| 26 |
]
|
| 27 |
},
|
| 28 |
"uki": {
|
|
@@ -48,6 +49,14 @@
|
|
| 48 |
"aliases": [
|
| 49 |
"hm"
|
| 50 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
},
|
| 53 |
"observed_methods": [
|
|
|
|
| 22 |
"family": "Kalman",
|
| 23 |
"aliases": [
|
| 24 |
"iekf",
|
| 25 |
+
"gnsl",
|
| 26 |
+
"gnki"
|
| 27 |
]
|
| 28 |
},
|
| 29 |
"uki": {
|
|
|
|
| 49 |
"aliases": [
|
| 50 |
"hm"
|
| 51 |
]
|
| 52 |
+
},
|
| 53 |
+
"ces-eki-dmc": {
|
| 54 |
+
"abbreviation": "CES-EKI-DMC",
|
| 55 |
+
"Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
|
| 56 |
+
"family": "calibrate_then_emulate",
|
| 57 |
+
"aliases": [
|
| 58 |
+
"ces-eki-dmc"
|
| 59 |
+
]
|
| 60 |
}
|
| 61 |
},
|
| 62 |
"observed_methods": [
|
.claude/settings.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
| 3 |
+
"permissions": {
|
| 4 |
+
"allow": [
|
| 5 |
+
"Bash(julia --project *)",
|
| 6 |
+
"Read(*)",
|
| 7 |
+
"FileWrite(src/*, test/*, docs/*, examples/*, ai/*)",
|
| 8 |
+
"Edit(src/*)",
|
| 9 |
+
"Write(src/*)",
|
| 10 |
+
"Grep",
|
| 11 |
+
"Glob",
|
| 12 |
+
"Bash(find *)",
|
| 13 |
+
"Bash(ls *)",
|
| 14 |
+
"Bash(tree *)",
|
| 15 |
+
"Bash(wc *)",
|
| 16 |
+
"Bash(head *)",
|
| 17 |
+
"Bash(tail *)",
|
| 18 |
+
"Bash(stat *)",
|
| 19 |
+
"Bash(grep *)",
|
| 20 |
+
"Bash(git log*)",
|
| 21 |
+
"Bash(git diff*)",
|
| 22 |
+
"Bash(git status*)",
|
| 23 |
+
"Bash(git show*)",
|
| 24 |
+
"Bash(git blame*)",
|
| 25 |
+
"Bash(git ls-files*)",
|
| 26 |
+
"Bash(git rev-parse*)",
|
| 27 |
+
"Bash(git describe*)",
|
| 28 |
+
"Bash(git shortlog*)",
|
| 29 |
+
"Bash(git tag -l*)",
|
| 30 |
+
"Bash(git tag --list*)",
|
| 31 |
+
"Bash(git stash list*)",
|
| 32 |
+
"Bash(git stash show*)",
|
| 33 |
+
"Bash(git remote -v*)",
|
| 34 |
+
"Bash(git remote show*)",
|
| 35 |
+
"Bash(git branch -l*)",
|
| 36 |
+
"Bash(git branch --list*)",
|
| 37 |
+
"Bash(git branch -a*)",
|
| 38 |
+
"Bash(git branch -r*)"
|
| 39 |
+
],
|
| 40 |
+
"deny": [
|
| 41 |
+
"Read(./.env)",
|
| 42 |
+
"Bash(git commit*)",
|
| 43 |
+
"Bash(git add*)",
|
| 44 |
+
"Bash(git push*)",
|
| 45 |
+
"Bash(git pull*)",
|
| 46 |
+
"Bash(git merge*)",
|
| 47 |
+
"Bash(git rebase*)",
|
| 48 |
+
"Bash(git checkout*)",
|
| 49 |
+
"Bash(git switch*)",
|
| 50 |
+
"Bash(git restore*)",
|
| 51 |
+
"Bash(git reset*)",
|
| 52 |
+
"Bash(git cherry-pick*)",
|
| 53 |
+
"Bash(git clean*)",
|
| 54 |
+
"Bash(git rm*)"
|
| 55 |
+
]
|
| 56 |
+
},
|
| 57 |
+
"hooks": {
|
| 58 |
+
"UserPromptSubmit": [
|
| 59 |
+
{
|
| 60 |
+
"hooks": [
|
| 61 |
+
{
|
| 62 |
+
"type": "command",
|
| 63 |
+
"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'"
|
| 64 |
+
}
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"Stop": [
|
| 69 |
+
{
|
| 70 |
+
"hooks": [
|
| 71 |
+
{
|
| 72 |
+
"type": "command",
|
| 73 |
+
"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\""
|
| 74 |
+
}
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
]
|
| 78 |
+
}
|
| 79 |
+
}
|
.claude/skills/leaderboard-manager/SKILL.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, family, 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, family, 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
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"family": "Kalman", # One of: Kalman | Bayesian | calibrate_then_emulate
|
| 48 |
+
"aliases": ["new-key", "alias2"], # All raw strings that may appear in NetCDF algorithm_type
|
| 49 |
+
},
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**Canonical key** (`"new-key"`): lowercase, hyphen-separated. Must match what
|
| 53 |
+
`canonicalize_method_name()` will resolve to from the NetCDF's `algorithm_type` strings.
|
| 54 |
+
|
| 55 |
+
**Aliases**: include every spelling variant that could appear in the raw NetCDF, both
|
| 56 |
+
lowercase and the exact case used. The `build_alias_lookup()` function (line 53) also
|
| 57 |
+
registers `canonical.upper()` automatically, but doesn't handle mixed-case; be thorough.
|
| 58 |
+
|
| 59 |
+
After saving, `ALIAS_TO_CANONICAL` is rebuilt at import time β no extra step needed.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Step 2 β Add display metadata in `src/pages/MethodDetails.py`
|
| 64 |
+
|
| 65 |
+
Open `src/pages/MethodDetails.py`. Find the `method_meta` dict (line 40) and add an entry
|
| 66 |
+
keyed by the **abbreviation** (not the canonical key):
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
"NEWABBR": {
|
| 70 |
+
"citation": "Author et al., Journal, Year",
|
| 71 |
+
"url": "https://doi.org/...",
|
| 72 |
+
"summary": "One-sentence description of what this algorithm does.",
|
| 73 |
+
},
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
If you don't have the citation/URL yet, use placeholder strings and follow up with
|
| 77 |
+
`populate-metadata.md`. The app will show "Citation pending" / a broken link if omitted,
|
| 78 |
+
but won't crash.
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## Step 3 β Place the NetCDF and register it in `src/data_store.py`
|
| 83 |
+
|
| 84 |
+
1. **Copy the file** into `data/` (or a subdirectory like `data/bayesian/`). Use
|
| 85 |
+
`git lfs track "data/**/*.nc"` if it isn't already tracked by LFS.
|
| 86 |
+
|
| 87 |
+
2. **Add the path** to `DATASET_FILES` in `src/data_store.py` (starts at line 29):
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
DATASET_FILES = {
|
| 91 |
+
"L63": [
|
| 92 |
+
"l63_ensemble_results.nc",
|
| 93 |
+
...
|
| 94 |
+
"my_subdir/new_method_l63.nc", # β add here
|
| 95 |
+
],
|
| 96 |
+
...
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
Keys (`"L63"`, `"L96"`, etc.) are the benchmark labels. Add the file to every benchmark
|
| 101 |
+
key it covers. Paths are relative to the `data/` directory.
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## Step 4 β Verify
|
| 106 |
+
|
| 107 |
+
Run the smoke-test from `SKILL.md`:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
streamlit run src/streamlit_app.py
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
Then check:
|
| 114 |
+
|
| 115 |
+
1. **Leaderboard table** (Home page): the new method's abbreviation appears as a row.
|
| 116 |
+
2. **Methods page** (`π Methods`): the method is in the dropdown; citation/summary display.
|
| 117 |
+
3. **Snapshot file**: open `.cache/known_methods_snapshot.json` after the app loads and
|
| 118 |
+
confirm:
|
| 119 |
+
- The new canonical key is in `known_methods`.
|
| 120 |
+
- It does **not** appear in `unmapped_observed_methods` (which lists any `algorithm_type`
|
| 121 |
+
strings from the data that didn't match any known alias).
|
| 122 |
+
|
| 123 |
+
If the method appears in `unmapped_observed_methods`, the raw `algorithm_type` in the
|
| 124 |
+
NetCDF doesn't match any alias you registered. Add the exact string (check
|
| 125 |
+
`observed_methods` in the snapshot for the raw value) to the `aliases` list in
|
| 126 |
+
`method_registry.py` and reload.
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## Common failure modes
|
| 131 |
+
|
| 132 |
+
| Symptom | Likely cause |
|
| 133 |
+
|---|---|
|
| 134 |
+
| Method missing from leaderboard table | `algorithm_type` in NetCDF doesn't match any alias in `KNOWN_METHODS` β check `unmapped_observed_methods` |
|
| 135 |
+
| 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 |
|
| 136 |
+
| "Skipping β¦ missing expected columns" warning | NetCDF lacks `algorithm_type` or `ensemble_size` dims and no special-case branch exists in `_load_store` |
|
| 137 |
+
| Old data still shown after file change | Streamlit cache β clear it (top-right menu or restart) |
|
| 138 |
+
| `data/*.nc` is 130 bytes | `git lfs pull` not run |
|
.claude/skills/leaderboard-manager/references/add-uq-data.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 `family = "Kalman"` default.
|
| 41 |
+
- [ ] **`DATASET_FILES` references a file path that doesn't exist on disk** β `_load_store`
|
| 42 |
+
emits a `warnings.warn` and skips it silently; run
|
| 43 |
+
`python -c "from src.data_store import load_metric_store; load_metric_store()"` and
|
| 44 |
+
watch for warnings, or just `ls` the registered paths.
|
| 45 |
+
|
| 46 |
+
### Display correctness
|
| 47 |
+
|
| 48 |
+
- [ ] **Hardcoded `target_options` in `MethodDetails.py`** β should be derived from data:
|
| 49 |
+
`sorted(slice_df["rmse_target"].astype(str).unique().tolist())`. A hardcoded list
|
| 50 |
+
silently hides any target present in the data but absent from the list.
|
| 51 |
+
- [ ] **`method_meta` entries with placeholder citations or `example.com` URLs** β
|
| 52 |
+
grep for `example.com` and `placeholder` / `citation to be added` in `MethodDetails.py`.
|
| 53 |
+
See `references/populate-metadata.md` for confirmed citations.
|
| 54 |
+
|
| 55 |
+
### Dead code
|
| 56 |
+
|
| 57 |
+
- [ ] **Unused columns written to `merged` in `_load_store`** β e.g. any column created
|
| 58 |
+
but never read downstream. Check columns added after the `dropna` step in `data_store.py`.
|
| 59 |
+
- [ ] **Session-state guards that are always `True`** β e.g.
|
| 60 |
+
`if st.session_state.get("show_home", True):` where nothing ever sets the key to `False`.
|
| 61 |
+
These wrap live code in an unreachable conditional.
|
| 62 |
+
- [ ] **Dead functions** β any function in `data_store.py` or elsewhere that is defined but
|
| 63 |
+
never called. Check for `def build_*` or similar remnants of superseded pipelines.
|
| 64 |
+
|
| 65 |
+
### Sidebar nav consistency
|
| 66 |
+
|
| 67 |
+
- [ ] **All four files have identical sidebar `page_link` blocks** β the block is
|
| 68 |
+
copy-pasted in `streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`.
|
| 69 |
+
Confirm they are identical (same links, same icons, same order).
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## Reporting format
|
| 74 |
+
|
| 75 |
+
Group findings by severity:
|
| 76 |
+
|
| 77 |
+
- **Bug (functional impact)** β something that causes incorrect output, a runtime error,
|
| 78 |
+
or silently hides data (e.g. hardcoded targets, invalid Streamlit API).
|
| 79 |
+
- **Staleness** β registry entries, metadata, or cached snapshots that no longer match
|
| 80 |
+
current code or data (e.g. placeholder citations, phantom method entries).
|
| 81 |
+
- **Dead code** β functions, columns, or conditionals that do nothing.
|
| 82 |
+
- **Informational** β minor inconsistencies that don't affect correctness.
|
| 83 |
+
|
| 84 |
+
For each finding, include: file, line number(s), description, and recommended fix.
|
.claude/skills/leaderboard-manager/references/manage-display.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
---
|
| 10 |
+
|
| 11 |
+
## Leaderboard table
|
| 12 |
+
|
| 13 |
+
### Column visibility and order
|
| 14 |
+
|
| 15 |
+
The columns shown depend on whether "All" or a specific benchmark is selected. Both lists
|
| 16 |
+
are defined in `src/common/leaderboard.py`:
|
| 17 |
+
|
| 18 |
+
- **All-benchmarks view** (`table_column_order`, line 296β305):
|
| 19 |
+
```python
|
| 20 |
+
["Placement", "abbreviation", "Method", "family", "Score",
|
| 21 |
+
"Targets Used", "Ensemble Sizes Used", "Benchmarks Used"]
|
| 22 |
+
```
|
| 23 |
+
- **Single-benchmark view** (`table_column_order`, line 307β319):
|
| 24 |
+
```python
|
| 25 |
+
["Placement", "abbreviation", "Method", "family", "Score",
|
| 26 |
+
"Mean Forward Model Runs", "Minimum Forward Model Runs",
|
| 27 |
+
"Optimal Ensemble Size", "Targets Used", "Ensemble Sizes Used", "Benchmarks Used"]
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
To **show or hide** a column, add/remove it from the relevant list.
|
| 31 |
+
To **reorder** columns, rearrange entries within the list.
|
| 32 |
+
|
| 33 |
+
### Column labels and formatting
|
| 34 |
+
|
| 35 |
+
Column display names and number formats are set in `st.dataframe(... column_config=...)`
|
| 36 |
+
at lines 326β339. Each column has an entry like:
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
"Mean Forward Model Runs": st.column_config.NumberColumn(
|
| 40 |
+
"Mean Forward Model Runs", format="%.4f"
|
| 41 |
+
),
|
| 42 |
+
"Score": st.column_config.ProgressColumn(
|
| 43 |
+
"Score (0β100)", min_value=0.0, max_value=100.0, format="%.1f"
|
| 44 |
+
),
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
To **rename** a displayed column header, change the string argument (second positional arg)
|
| 48 |
+
in the `column_config` entry. The dict key must still match the DataFrame column name.
|
| 49 |
+
|
| 50 |
+
To **add a new column to the table**, you also need to produce it in `build_scored_table()`
|
| 51 |
+
(line 111) β add it to the `.agg()` call or compute it after the groupby, then add a
|
| 52 |
+
`column_config` entry and include it in `table_column_order`.
|
| 53 |
+
|
| 54 |
+
### Adding `failure_rate` to the leaderboard table
|
| 55 |
+
|
| 56 |
+
`failure_rate` is available in `metric_store` but not surfaced in the scored table.
|
| 57 |
+
To add it:
|
| 58 |
+
1. In `build_scored_table()`, add it to the `.groupby(...).agg(...)` call, e.g.:
|
| 59 |
+
`**{"Mean Failure Rate (%)": ("failure_rate", "mean")}`.
|
| 60 |
+
2. Add a `column_config` entry (e.g. `st.column_config.NumberColumn(..., format="%.1f")`).
|
| 61 |
+
3. Add `"Mean Failure Rate (%)"` to `table_column_order` where desired.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## Scoring modes
|
| 66 |
+
|
| 67 |
+
The four scoring modes are defined in `scoring_options` (line 82):
|
| 68 |
+
```python
|
| 69 |
+
scoring_options = [
|
| 70 |
+
"Mean Forward Model Runs",
|
| 71 |
+
"Minimum Forward Model Runs",
|
| 72 |
+
"Smallest Optimal Ensemble Size",
|
| 73 |
+
"Custom Blend",
|
| 74 |
+
]
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Each mode maps to a sort key and a score column assignment in `build_scored_table()` at
|
| 78 |
+
lines 185β203. To **add a new scoring mode**, add its name to `scoring_options` and add
|
| 79 |
+
an `elif` branch in the scoring logic block that sets `scored_df["Score"]`,
|
| 80 |
+
`sort_columns`, and `ascending`.
|
| 81 |
+
|
| 82 |
+
The "Custom Blend" slider weight (lines 357β368) is keyed to the session-state key
|
| 83 |
+
`k_weight` β no change needed when adding a non-blend mode.
|
| 84 |
+
|
| 85 |
+
---
|
| 86 |
+
|
| 87 |
+
## Leaderboard line chart
|
| 88 |
+
|
| 89 |
+
The chart that appears in single-benchmark view ("Mean Forward Model Runs vs Ensemble
|
| 90 |
+
Size") is at lines 387β396 (inside the `if selected_benchmark != "All":` guard at line
|
| 91 |
+
376):
|
| 92 |
+
|
| 93 |
+
```python
|
| 94 |
+
ens_ticks = sorted(chart_df["ensemble_size"].unique().tolist())
|
| 95 |
+
chart = (
|
| 96 |
+
alt.Chart(chart_df)
|
| 97 |
+
.mark_line(point=True)
|
| 98 |
+
.encode(
|
| 99 |
+
x=alt.X(
|
| 100 |
+
"ensemble_size:Q",
|
| 101 |
+
title="Ensemble Size",
|
| 102 |
+
axis=alt.Axis(values=ens_ticks, format="d"), # integer ticks only
|
| 103 |
+
),
|
| 104 |
+
y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
|
| 105 |
+
color=alt.Color("abbreviation:N", title="Method"),
|
| 106 |
+
tooltip=["abbreviation", "ensemble_size",
|
| 107 |
+
alt.Tooltip("mean_forward_runs:Q", format=".4f")],
|
| 108 |
+
)
|
| 109 |
+
)
|
| 110 |
+
st.altair_chart(chart, use_container_width=True)
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
The `axis=alt.Axis(values=ens_ticks, format="d")` pattern is important: without it,
|
| 114 |
+
Altair may interpolate non-integer ticks (e.g. 5.5, 10.5) on the ensemble-size axis.
|
| 115 |
+
Always derive `ens_ticks` from the actual data rather than hardcoding.
|
| 116 |
+
|
| 117 |
+
Common changes:
|
| 118 |
+
- **Log scale on Y**: add `scale=alt.Scale(type="log")` inside `alt.Y(...)`.
|
| 119 |
+
- **Add failure rate as a second Y-axis or tooltip**: add `failure_rate` to `chart_df`
|
| 120 |
+
groupby and include it in `tooltip`.
|
| 121 |
+
- **Change mark type** (e.g. bars): replace `mark_line(point=True)` with `mark_bar()`.
|
| 122 |
+
- **Color palette**: add `.configure_range(category={"scheme": "tableau10"})` after the
|
| 123 |
+
chart construction.
|
| 124 |
+
|
| 125 |
+
The chart only renders when a single benchmark is selected (`if selected_benchmark != "All":`
|
| 126 |
+
guard at line 376).
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## Method Details page surfaces
|
| 131 |
+
|
| 132 |
+
The per-method page (`src/pages/MethodDetails.py`) has its own controls.
|
| 133 |
+
|
| 134 |
+
### Target level radio
|
| 135 |
+
|
| 136 |
+
Line 111:
|
| 137 |
+
```python
|
| 138 |
+
target_options = ["1.0", "1.1", "1.2"]
|
| 139 |
+
selected_target = st.radio("RMSE Target Level", options=target_options, horizontal=True)
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
These are **hardcoded strings**. If the benchmark gains new target levels, update this
|
| 143 |
+
list. A more robust approach is to derive them from the data:
|
| 144 |
+
```python
|
| 145 |
+
target_options = sorted(metric_store["rmse_target"].astype(str).unique().tolist())
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Per-method special-case charts
|
| 149 |
+
|
| 150 |
+
The HM (History Matching) failure-analysis chart (lines 122β138) is the canonical
|
| 151 |
+
example of a per-method detail chart:
|
| 152 |
+
|
| 153 |
+
```python
|
| 154 |
+
if sel == "HM":
|
| 155 |
+
st.markdown("### Failure Analysis")
|
| 156 |
+
chart = (alt.Chart(best_table_view).mark_bar().encode(...))
|
| 157 |
+
st.altair_chart(chart, use_container_width=True)
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
To **add a chart for another method**, add an `elif sel == "ABBR":` block immediately
|
| 161 |
+
after the `if sel == "HM":` block, following the same pattern.
|
| 162 |
+
|
| 163 |
+
To **generalize** (show failure rate for all methods when non-zero), replace the `if sel
|
| 164 |
+
== "HM":` guard with a condition on the data, e.g.:
|
| 165 |
+
```python
|
| 166 |
+
if best_table_view["failure_rate"].max() > 0:
|
| 167 |
+
# render the failure chart for any method that has failures
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
### Scaling chart (ensemble size vs forward-model runs per benchmark)
|
| 171 |
+
|
| 172 |
+
The `"Scaling by Benchmark"` chart is already generic across all methods. To modify
|
| 173 |
+
encoding, change the `.encode()` arguments analogously to the leaderboard chart above.
|
| 174 |
+
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
## UQ-specific charts (`budget_store` parameter)
|
| 178 |
+
|
| 179 |
+
The UQ leaderboard passes a `budget_store` DataFrame to `render_leaderboard()` via the
|
| 180 |
+
`budget_store` keyword argument. This enables two additional chart sections rendered in
|
| 181 |
+
`src/common/leaderboard.py` after the main performance chart:
|
| 182 |
+
|
| 183 |
+
1. **"Mean Iterations for Coverage vs Ensemble Size"** β line chart of `mean_iters` vs
|
| 184 |
+
ensemble size. Uses the same integer-tick pattern as the main chart.
|
| 185 |
+
|
| 186 |
+
2. **"Failure Rate of Hitting Target ..."** β grouped bar chart with `ensemble_size:O`
|
| 187 |
+
on the x-axis (ordinal, not quantitative) and `mean_failure_rate:Q` on y. The title
|
| 188 |
+
reflects the currently selected target level.
|
| 189 |
+
|
| 190 |
+
To **add or modify a UQ chart**, edit the `if budget_store is not None` block in
|
| 191 |
+
`src/common/leaderboard.py`. The `budget_store` DataFrame has columns: `benchmark`,
|
| 192 |
+
`algorithm_type`, `abbreviation`, `family`, `uq_target`, `ensemble_size`, `mean_budget`,
|
| 193 |
+
`mean_iters`, `failure_count`, `failure_rate`, `n_seeds`.
|
| 194 |
+
|
| 195 |
+
**Bar chart integer x-axis:** Use `ensemble_size:O` (ordinal) with an explicit sort list
|
| 196 |
+
and `axis=alt.Axis(labelAngle=0)` to suppress diagonal labels:
|
| 197 |
+
```python
|
| 198 |
+
ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
|
| 199 |
+
x=alt.X(
|
| 200 |
+
"ensemble_size:O",
|
| 201 |
+
title="Ensemble Size",
|
| 202 |
+
sort=[str(e) for e in ens_ticks_fail],
|
| 203 |
+
axis=alt.Axis(labelAngle=0),
|
| 204 |
+
)
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
## Display change checklist
|
| 210 |
+
|
| 211 |
+
1. Edit the relevant file (`leaderboard.py` for both leaderboards; `MethodDetails.py` for
|
| 212 |
+
per-method surfaces).
|
| 213 |
+
2. If adding a column: produce it in the aggregation AND add a `column_config` entry AND
|
| 214 |
+
add it to `table_column_order`.
|
| 215 |
+
3. Restart or clear Streamlit cache to pick up Python changes (code changes take effect on
|
| 216 |
+
restart; cache only matters for data changes).
|
| 217 |
+
4. Smoke-test on both the "All" view and a single-benchmark view to catch the different
|
| 218 |
+
`table_column_order` branches.
|
.claude/skills/leaderboard-manager/references/move-between-boards.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, family, 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, family classification, 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 |
+
"family": "Kalman", # Kalman | Bayesian | calibrate_then_emulate
|
| 107 |
+
"aliases": ["uki"], # Raw algorithm_type strings from NetCDF
|
| 108 |
+
},
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
**If you change `abbreviation`**: also update the corresponding key in `method_meta` in
|
| 112 |
+
`MethodDetails.py` (the keys must match). The app looks up `method_meta[abbreviation]`.
|
| 113 |
+
|
| 114 |
+
**If you change `family`**: the "Family" column in the leaderboard table updates
|
| 115 |
+
automatically β no other change needed.
|
| 116 |
+
|
| 117 |
+
**If you add an alias**: new alias strings start matching immediately after reload. Use
|
| 118 |
+
this when a collaborator's NetCDF uses a variant spelling not currently in the alias list.
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## Benchmark / dataset labels
|
| 123 |
+
|
| 124 |
+
Benchmark names (`L63`, `L96`, `L96_NN_FORCING`, `L96_SPATIAL_FORCING`) come from the
|
| 125 |
+
keys of `DATASET_FILES` in `src/data_store.py`. They appear:
|
| 126 |
+
- In the "Benchmark" selectbox on both leaderboards.
|
| 127 |
+
- In the `benchmark` column of every row in `metric_store`.
|
| 128 |
+
- As chart axis labels on the Methods detail page ("Scaling by Benchmark").
|
| 129 |
+
|
| 130 |
+
To **rename a benchmark label**, change its key in `DATASET_FILES` (and `UQ_DATASET_FILES`
|
| 131 |
+
if present). The label propagates everywhere automatically because it's read from the data
|
| 132 |
+
at runtime. There is no separate label registry.
|
| 133 |
+
|
| 134 |
+
To **add a new benchmark**, see `add-method.md` β Step 3 (the same pattern applies even
|
| 135 |
+
without a new method β just add a new benchmark key with its file list).
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## Verify
|
| 140 |
+
|
| 141 |
+
After editing metadata:
|
| 142 |
+
1. Restart the app (code changes don't require cache clearing, only data changes do).
|
| 143 |
+
2. Open the `π Methods` page and select the edited method.
|
| 144 |
+
3. Confirm the citation text, URL button target, and summary box show the new values.
|
| 145 |
+
4. Check no `method_meta` key is misspelled (the abbreviation in `method_meta` must match
|
| 146 |
+
`KNOWN_METHODS[...]["abbreviation"]` exactly, including capitalisation).
|
.gitattributes
CHANGED
|
@@ -34,3 +34,4 @@ 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
|
|
|
|
|
|
| 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
|
.gitignore
CHANGED
|
@@ -1,3 +1,34 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 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
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
β βββ common/
|
| 51 |
+
β β βββ leaderboard.py render_leaderboard() β shared scored-table + chart pipeline
|
| 52 |
+
β β β optional budget_store param adds budget/iters chart section
|
| 53 |
+
β β βββ method_registry.py KNOWN_METHODS registry + name canonicalization
|
| 54 |
+
β βββ pages/
|
| 55 |
+
β βββ UQLeaderboard.py π― UQ Leaderboard β calls render_leaderboard(load_uq_store())
|
| 56 |
+
β βββ MethodDetails.py π Methods β per-method charts + metadata
|
| 57 |
+
β βββ RawData.py π§Ύ Get Data β raw table + CSV export
|
| 58 |
+
βββ data/ NetCDF result files (git-LFS β run `git lfs pull`)
|
| 59 |
+
β βββ *.nc Kalman results (TEKI, ETKI, IEKF)
|
| 60 |
+
β βββ bayesian/ ABC + HM results
|
| 61 |
+
β βββ UKI_results/ UKI results
|
| 62 |
+
β βββ ces-eki-dmc_results/ CES-EKI-DMC ensemble-results files (UQ source)
|
| 63 |
+
β βββ ces-eki-dmc_l63_ensemble_results_2026-06-15.nc
|
| 64 |
+
β βββ ces-eki-dmc_l96_ensemble_results_2026-06-15.nc
|
| 65 |
+
β βββ ces-eki-dmc_l96_nn_forcing_ensemble_results_2026-06-15.nc
|
| 66 |
+
β βββ ces-eki-dmc_l96_spatial_forcing_ensemble_results_2026-06-15.nc
|
| 67 |
+
βββ examples/ lorenz_demo.ipynb + lorenz.py (reference, not app code)
|
| 68 |
+
βββ From_Rob_6-10/ Raw collaborator data drop β NOT read by the app
|
| 69 |
+
βββ .cache/ known_methods_snapshot.json (auto-generated, safe to delete)
|
| 70 |
+
βββ .streamlit/config.toml showSidebarNavigation = false
|
| 71 |
+
βββ Dockerfile python:3.13.5-slim; ENTRYPOINT streamlit run src/...
|
| 72 |
+
βββ requirements.txt altair, pandas, streamlit, xarray, netcdf4, h5netcdf, jax, gpjax
|
| 73 |
+
βββ test_parse.py Ad-hoc NetCDF parse check (not a test suite)
|
| 74 |
+
βββ test_failures.py Ad-hoc failure-count check (not a test suite)
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Data model
|
| 78 |
+
|
| 79 |
+
### Optimization store (`load_metric_store()`)
|
| 80 |
+
|
| 81 |
+
| Field | Meaning |
|
| 82 |
+
|---|---|
|
| 83 |
+
| `benchmark` | `L63`, `L96`, `L96_NN_FORCING`, `L96_SPATIAL_FORCING` |
|
| 84 |
+
| `algorithm_type` | Canonical method key (e.g. `teki`) |
|
| 85 |
+
| `algorithm_alias` | Normalized raw name from NetCDF |
|
| 86 |
+
| `abbreviation` | Display name (e.g. `TEKI`) |
|
| 87 |
+
| `family` | `Kalman` Β· `Bayesian` Β· `calibrate_then_emulate` |
|
| 88 |
+
| `rmse_target` | Target RMSE level |
|
| 89 |
+
| `ensemble_size` | Ensemble / particle count |
|
| 90 |
+
| `metric` | Mean forward-model runs over `random_seed` (lower = better; β1 = failed) |
|
| 91 |
+
| `failure_rate` | % seeds that failed (`metric == -1`) |
|
| 92 |
+
|
| 93 |
+
`EXPECTED_DIMS = ("algorithm_type", "rmse_target", "ensemble_size", "random_seed")`
|
| 94 |
+
|
| 95 |
+
### UQ store (`load_uq_store()`)
|
| 96 |
+
|
| 97 |
+
Derived from `load_uq_budget_store()` β same schema as the optimization store but with
|
| 98 |
+
`uq_target` in place of `rmse_target`, and `metric` = mean budget (N_ensΒ·k_iter). Failure
|
| 99 |
+
rate = % of seeds that never satisfied the coverage condition (NaN metric, not β1).
|
| 100 |
+
|
| 101 |
+
### Budget store (`load_uq_budget_store()`)
|
| 102 |
+
|
| 103 |
+
| Field | Meaning |
|
| 104 |
+
|---|---|
|
| 105 |
+
| `benchmark` | `L63`, `L96`, `L96_NN_FORCING`, `L96_SPATIAL_FORCING` |
|
| 106 |
+
| `algorithm_type` | Canonical method key |
|
| 107 |
+
| `abbreviation` / `Method` / `family` | From method registry |
|
| 108 |
+
| `uq_target` | Target-scaling level c |
|
| 109 |
+
| `ensemble_size` | Ensemble size N |
|
| 110 |
+
| `mean_budget` | Mean NΒ·k_iter over converged seeds (NaN if all failed) |
|
| 111 |
+
| `mean_iters` | Mean k_iter over converged seeds |
|
| 112 |
+
| `failure_count` | # seeds that never reached the target |
|
| 113 |
+
| `failure_rate` | % seeds that never reached the target |
|
| 114 |
+
| `n_seeds` | Total seeds (20) |
|
| 115 |
+
|
| 116 |
+
Source NC files are registered in `UQ_BUDGET_FILES` in `src/data_store.py`. Each file must
|
| 117 |
+
carry `output_coverage (coverage_quantile, k_iter, ensemble_size, random_seed)`,
|
| 118 |
+
`target_scaling`, and `output_dim`.
|
| 119 |
+
|
| 120 |
+
`UQ_COVERAGE_QUANTILES = [0.15, 0.5, 0.85]` β the three quantile levels used for the
|
| 121 |
+
all-quantiles-satisfied condition.
|
| 122 |
+
|
| 123 |
+
All three stores are `@st.cache_data` β clear Streamlit cache to pick up new data files.
|
| 124 |
+
|
| 125 |
+
## Method registry (`src/common/method_registry.py`)
|
| 126 |
+
|
| 127 |
+
| Canonical | Abbreviation | Family | Aliases |
|
| 128 |
+
|---|---|---|---|
|
| 129 |
+
| `teki` | TEKI | Kalman | β |
|
| 130 |
+
| `etki` | ETKI | Kalman | β |
|
| 131 |
+
| `iekf` | IEKF | Kalman | gnsl, gnki |
|
| 132 |
+
| `uki` | UKI | Kalman | β |
|
| 133 |
+
| `abc` | ABC | Bayesian | β |
|
| 134 |
+
| `hm` | HM | Bayesian | β |
|
| 135 |
+
| `ces-eki-dmc` | CES-EKI-DMC | calibrate_then_emulate | β |
|
| 136 |
+
|
| 137 |
+
## How to extend
|
| 138 |
+
|
| 139 |
+
### Add a method
|
| 140 |
+
1. Add entry to `KNOWN_METHODS` in `src/common/method_registry.py`
|
| 141 |
+
(keys: `abbreviation`, `Method`, `family`, `aliases`).
|
| 142 |
+
2. Add detail-page metadata to the inline `method_meta` dict in `src/pages/MethodDetails.py`
|
| 143 |
+
keyed by **abbreviation** (e.g. `"TEKI"`). β οΈ Metadata is split across both files.
|
| 144 |
+
3. Place result NetCDF(s) in `data/` and add paths to `DATASET_FILES` in `src/data_store.py`.
|
| 145 |
+
|
| 146 |
+
### Add an optimization benchmark dataset
|
| 147 |
+
Add a `"BENCHMARK_NAME": [list_of_nc_paths]` entry to `DATASET_FILES` in `src/data_store.py`.
|
| 148 |
+
Paths are relative to the `data/` directory.
|
| 149 |
+
|
| 150 |
+
### Add UQ data
|
| 151 |
+
Add an `(algorithm_type, nc_path)` tuple to the appropriate benchmark key in `UQ_BUDGET_FILES`
|
| 152 |
+
in `src/data_store.py`. The NC file must carry:
|
| 153 |
+
- `output_coverage` with dims `(coverage_quantile, k_iter, ensemble_size, random_seed)`
|
| 154 |
+
- coordinates `coverage_quantile`, `k_iter`, `ensemble_size`, `target_scaling`
|
| 155 |
+
- dimension `output_dim`
|
| 156 |
+
|
| 157 |
+
`load_uq_budget_store()` and `load_uq_store()` will pick it up automatically after a
|
| 158 |
+
Streamlit cache clear. No changes to `UQ_DATASET_FILES` (kept empty) are needed.
|
| 159 |
+
|
| 160 |
+
The budget/iters chart and coverage-failure bar chart in the UQ leaderboard are rendered via
|
| 161 |
+
the `budget_store` parameter of `render_leaderboard()` in `src/common/leaderboard.py`.
|
| 162 |
+
|
| 163 |
+
### Add a page
|
| 164 |
+
1. Create `src/pages/NewPage.py` β copy the sidebar nav block + `data_store` import shim
|
| 165 |
+
from an existing page (e.g. `RawData.py`).
|
| 166 |
+
2. Add `st.sidebar.page_link("pages/NewPage.py", ...)` to **all four** existing files
|
| 167 |
+
(`streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`). Sidebar nav
|
| 168 |
+
is duplicated β there is no central nav module.
|
| 169 |
+
|
| 170 |
+
## Known gotchas
|
| 171 |
+
- **`git lfs pull` is mandatory** before first run β without it `data/*.nc` are 130-byte stubs.
|
| 172 |
+
- **Sidebar nav is not centralized** β adding/renaming pages requires editing four files
|
| 173 |
+
(`streamlit_app.py`, `UQLeaderboard.py`, `MethodDetails.py`, `RawData.py`).
|
| 174 |
+
- **Method metadata is split** β `method_registry.py` (scoring) and `pages/MethodDetails.py`
|
| 175 |
+
`method_meta` dict (display copy) must both be updated when adding a new method.
|
| 176 |
+
- **`build_leaderboard()` in `data_store.py` is dead code** β both leaderboards are rendered
|
| 177 |
+
via `render_leaderboard()` in `src/common/leaderboard.py`.
|
| 178 |
+
- **No CI / no formal test suite** β `test_parse.py` and `test_failures.py` are manual scripts.
|
| 179 |
+
- **Cross-page linking** uses `st.query_params["method"]` (URL param = abbreviation).
|
| 180 |
+
- **`UQ_DATASET_FILES` is intentionally empty** β UQ data is loaded via `UQ_BUDGET_FILES`
|
| 181 |
+
and the `load_uq_budget_store()` pipeline, not via `_load_store()`.
|
| 182 |
+
- **UQ failure β optimization failure** β in the UQ store, NaN metric means coverage target
|
| 183 |
+
never reached (no `metric == -1` sentinel); `failure_rate` is computed differently.
|
| 184 |
+
- **`render_leaderboard` budget_store param** β must be passed explicitly from
|
| 185 |
+
`UQLeaderboard.py`; `leaderboard.py` is not aware of UQ-specific data paths.
|
README.md
CHANGED
|
@@ -17,3 +17,123 @@ 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).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
data/ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15_minimal.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a402f8dc6d39b34bb1243e878bb34505846fe176a7e21a1024a8c1727d55dc1
|
| 3 |
+
size 177750
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
altair
|
| 2 |
pandas
|
| 3 |
streamlit
|
|
@@ -5,4 +6,6 @@ jax
|
|
| 5 |
gpjax
|
| 6 |
matplotlib
|
| 7 |
xarray
|
| 8 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
hf
|
| 2 |
altair
|
| 3 |
pandas
|
| 4 |
streamlit
|
|
|
|
| 6 |
gpjax
|
| 7 |
matplotlib
|
| 8 |
xarray
|
| 9 |
+
netcdf4
|
| 10 |
+
h5netcdf
|
| 11 |
+
h5py
|
src/common/leaderboard.py
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
raw_page="pages/RawData.py",
|
| 18 |
+
)
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import altair as alt
|
| 23 |
+
import pandas as pd
|
| 24 |
+
import streamlit as st
|
| 25 |
+
|
| 26 |
+
# Stable categorical palette (Vega tableau10). Methods are assigned colors by their
|
| 27 |
+
# sorted position in the full available-method list, so toggling visibility never
|
| 28 |
+
# reassigns colors to other methods.
|
| 29 |
+
_METHOD_PALETTE = [
|
| 30 |
+
"#4c78a8", "#f58518", "#e45756", "#72b7b2", "#54a24b",
|
| 31 |
+
"#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def render_leaderboard(
|
| 36 |
+
metric_store: pd.DataFrame,
|
| 37 |
+
*,
|
| 38 |
+
target_col: str,
|
| 39 |
+
target_label: str,
|
| 40 |
+
title: str,
|
| 41 |
+
state_prefix: str,
|
| 42 |
+
raw_page: str | None = None,
|
| 43 |
+
show_failure_panel: bool = False,
|
| 44 |
+
show_scoring_modes: bool = True,
|
| 45 |
+
canonical_target_levels: list[float] | None = None,
|
| 46 |
+
budget_store: pd.DataFrame | None = None,
|
| 47 |
+
) -> None:
|
| 48 |
+
"""Render a scored leaderboard backed by *metric_store*.
|
| 49 |
+
|
| 50 |
+
Parameters
|
| 51 |
+
----------
|
| 52 |
+
metric_store:
|
| 53 |
+
DataFrame produced by ``load_metric_store()`` or ``load_uq_store()``.
|
| 54 |
+
Must contain at least the columns ``benchmark``, ``algorithm_type``,
|
| 55 |
+
``abbreviation``, ``Method``, ``family``, ``ensemble_size``, ``metric``,
|
| 56 |
+
``failure_rate``, and *target_col*.
|
| 57 |
+
target_col:
|
| 58 |
+
Name of the target-coordinate column, e.g. ``"rmse_target"`` or
|
| 59 |
+
``"uq_target"``.
|
| 60 |
+
target_label:
|
| 61 |
+
Human-readable label for the target-level radio control,
|
| 62 |
+
e.g. ``"RMSE Target Level"`` or ``"UQ Target Level"``.
|
| 63 |
+
title:
|
| 64 |
+
Leaderboard section header text.
|
| 65 |
+
state_prefix:
|
| 66 |
+
Short string used to namespace ``st.session_state`` keys so multiple
|
| 67 |
+
leaderboard pages keep independent control state. Use ``"opt"`` for
|
| 68 |
+
the Optimization leaderboard and ``"uq"`` for the UQ leaderboard.
|
| 69 |
+
raw_page:
|
| 70 |
+
Optional Streamlit page path for an "Open Raw Data" link shown at the
|
| 71 |
+
bottom. Pass ``None`` to suppress the link.
|
| 72 |
+
show_failure_panel:
|
| 73 |
+
If ``True``, render a grouped-bar failure-rate chart below the main
|
| 74 |
+
performance chart.
|
| 75 |
+
canonical_target_levels:
|
| 76 |
+
If provided, the target-level selector always offers exactly these
|
| 77 |
+
values (as strings) regardless of what is present in the data. Use
|
| 78 |
+
this to pin the UQ leaderboard to its fixed set of target-scaling
|
| 79 |
+
levels even when some have 100 % failure.
|
| 80 |
+
budget_store:
|
| 81 |
+
Optional DataFrame produced by ``load_uq_budget_store()``. When
|
| 82 |
+
provided, an additional "Budget and Iterations for Coverage" section is
|
| 83 |
+
rendered below the main performance chart, showing mean budget
|
| 84 |
+
(N_ensΒ·k_iter, solid lines) and mean iterations (k_iter, dashed lines)
|
| 85 |
+
vs ensemble size, followed by a coverage-failure-rate bar chart.
|
| 86 |
+
"""
|
| 87 |
+
st.header(title)
|
| 88 |
+
|
| 89 |
+
if metric_store.empty:
|
| 90 |
+
st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
# Derived column name for the string version of the target coordinate
|
| 94 |
+
target_str_col = f"{target_col}_str"
|
| 95 |
+
|
| 96 |
+
benchmark_values = sorted(metric_store["benchmark"].unique().tolist())
|
| 97 |
+
benchmark_options = ["All"] + benchmark_values
|
| 98 |
+
selected_benchmark = st.selectbox("Benchmark", options=benchmark_options, index=0)
|
| 99 |
+
|
| 100 |
+
filtered = (
|
| 101 |
+
metric_store.copy()
|
| 102 |
+
if selected_benchmark == "All"
|
| 103 |
+
else metric_store[metric_store["benchmark"] == selected_benchmark].copy()
|
| 104 |
+
)
|
| 105 |
+
filtered[target_str_col] = filtered[target_col].astype(str)
|
| 106 |
+
|
| 107 |
+
if canonical_target_levels is not None:
|
| 108 |
+
target_options = ["All targets"] + [str(float(t)) for t in canonical_target_levels]
|
| 109 |
+
else:
|
| 110 |
+
target_options = ["All targets"] + sorted(
|
| 111 |
+
metric_store[target_col].astype(str).unique().tolist()
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
scoring_options = [
|
| 115 |
+
"Mean Forward Model Runs",
|
| 116 |
+
"Minimum Forward Model Runs",
|
| 117 |
+
"Smallest Optimal Ensemble Size",
|
| 118 |
+
"Custom Blend",
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
# Session-state keys namespaced by state_prefix so two leaderboard pages
|
| 122 |
+
# don't share control state.
|
| 123 |
+
k_target = f"{state_prefix}_selected_target"
|
| 124 |
+
k_scoring = f"{state_prefix}_scoring_mode"
|
| 125 |
+
k_weight = f"{state_prefix}_fwdruns_weight_percent"
|
| 126 |
+
k_methods = f"{state_prefix}_selected_methods"
|
| 127 |
+
|
| 128 |
+
current_target = st.session_state.get(k_target, "All targets")
|
| 129 |
+
if current_target not in target_options:
|
| 130 |
+
current_target = target_options[0]
|
| 131 |
+
|
| 132 |
+
current_scoring_mode = st.session_state.get(k_scoring, "Mean Forward Model Runs")
|
| 133 |
+
if current_scoring_mode not in scoring_options:
|
| 134 |
+
current_scoring_mode = scoring_options[0]
|
| 135 |
+
|
| 136 |
+
current_fwdruns_weight_percent = int(st.session_state.get(k_weight, 80))
|
| 137 |
+
current_fwdruns_weight_percent = max(0, min(100, current_fwdruns_weight_percent))
|
| 138 |
+
|
| 139 |
+
selected_target = current_target
|
| 140 |
+
scoring_mode = current_scoring_mode
|
| 141 |
+
fwdruns_weight = current_fwdruns_weight_percent / 100.0
|
| 142 |
+
ensemble_weight = 1.0 - fwdruns_weight
|
| 143 |
+
|
| 144 |
+
# Available methods for the current benchmark selection; used to populate the
|
| 145 |
+
# multiselect and to prune any stale saved selections when the benchmark changes.
|
| 146 |
+
available_methods = sorted(filtered["abbreviation"].dropna().unique().tolist())
|
| 147 |
+
saved_methods = st.session_state.get(k_methods, available_methods)
|
| 148 |
+
valid_saved = [m for m in saved_methods if m in available_methods]
|
| 149 |
+
st.session_state[k_methods] = valid_saved if valid_saved else available_methods
|
| 150 |
+
|
| 151 |
+
# Stable color scale: domain covers ALL methods so colors don't shift when a
|
| 152 |
+
# subset is displayed.
|
| 153 |
+
color_domain = available_methods
|
| 154 |
+
color_range = [_METHOD_PALETTE[i % len(_METHOD_PALETTE)] for i in range(len(available_methods))]
|
| 155 |
+
method_color = alt.Color(
|
| 156 |
+
"abbreviation:N",
|
| 157 |
+
title="Method",
|
| 158 |
+
scale=alt.Scale(domain=color_domain, range=color_range),
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
|
| 162 |
+
ranking_source = (
|
| 163 |
+
input_df
|
| 164 |
+
if selected_target == "All targets"
|
| 165 |
+
else input_df[input_df[target_str_col] == selected_target]
|
| 166 |
+
)
|
| 167 |
+
if ranking_source.empty:
|
| 168 |
+
return ranking_source
|
| 169 |
+
|
| 170 |
+
# Failure rate from every row (NaN metric rows carry failure_rate=100)
|
| 171 |
+
failure_agg = ranking_source.groupby(
|
| 172 |
+
["algorithm_type", "abbreviation", "Method", "family"], as_index=False
|
| 173 |
+
).agg(**{"Mean Failure Rate (%)": ("failure_rate", "mean")})
|
| 174 |
+
|
| 175 |
+
# Metric stats only from runs that reached the target (non-NaN metric)
|
| 176 |
+
valid_rows = ranking_source.dropna(subset=["metric"])
|
| 177 |
+
if valid_rows.empty:
|
| 178 |
+
return pd.DataFrame()
|
| 179 |
+
|
| 180 |
+
scored_df = valid_rows.groupby(
|
| 181 |
+
["algorithm_type", "abbreviation", "Method", "family"], as_index=False
|
| 182 |
+
).agg(
|
| 183 |
+
**{"Mean Forward Model Runs": ("metric", "mean")},
|
| 184 |
+
**{"Minimum Forward Model Runs": ("metric", "min")},
|
| 185 |
+
**{"Targets Used": (target_str_col, "nunique")},
|
| 186 |
+
**{"Ensemble Sizes Used": ("ensemble_size", "nunique")},
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
best_per_target = (
|
| 190 |
+
valid_rows.sort_values(["algorithm_type", target_str_col, "metric", "ensemble_size"])
|
| 191 |
+
.groupby(["algorithm_type", "abbreviation", "Method", "family", target_str_col], as_index=False)
|
| 192 |
+
.first()[
|
| 193 |
+
[
|
| 194 |
+
"algorithm_type",
|
| 195 |
+
"abbreviation",
|
| 196 |
+
"Method",
|
| 197 |
+
"family",
|
| 198 |
+
target_str_col,
|
| 199 |
+
"ensemble_size",
|
| 200 |
+
]
|
| 201 |
+
]
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
optimal_ensemble = best_per_target.groupby(
|
| 205 |
+
["algorithm_type", "abbreviation", "Method", "family"], as_index=False
|
| 206 |
+
).agg(**{"Optimal Ensemble Size": ("ensemble_size", "mean")})
|
| 207 |
+
|
| 208 |
+
scored_df = scored_df.merge(
|
| 209 |
+
optimal_ensemble,
|
| 210 |
+
on=["algorithm_type", "abbreviation", "Method", "family"],
|
| 211 |
+
how="left",
|
| 212 |
+
)
|
| 213 |
+
scored_df = scored_df.merge(
|
| 214 |
+
failure_agg,
|
| 215 |
+
on=["algorithm_type", "abbreviation", "Method", "family"],
|
| 216 |
+
how="left",
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
scored_df["Optimal Ensemble Size"] = scored_df["Optimal Ensemble Size"].round(2)
|
| 220 |
+
scored_df["Mean Forward Model Runs"] = scored_df["Mean Forward Model Runs"].round(4)
|
| 221 |
+
scored_df["Minimum Forward Model Runs"] = scored_df["Minimum Forward Model Runs"].round(4)
|
| 222 |
+
scored_df["Mean Failure Rate (%)"] = scored_df["Mean Failure Rate (%)"].round(1)
|
| 223 |
+
|
| 224 |
+
mean_runs_min = scored_df["Mean Forward Model Runs"].min()
|
| 225 |
+
mean_runs_max = scored_df["Mean Forward Model Runs"].max()
|
| 226 |
+
if mean_runs_max > mean_runs_min:
|
| 227 |
+
scored_df["mean_runs_score"] = (
|
| 228 |
+
100.0 * (mean_runs_max - scored_df["Mean Forward Model Runs"]) / (mean_runs_max - mean_runs_min)
|
| 229 |
+
)
|
| 230 |
+
else:
|
| 231 |
+
scored_df["mean_runs_score"] = 100.0
|
| 232 |
+
|
| 233 |
+
minimum_runs_min = scored_df["Minimum Forward Model Runs"].min()
|
| 234 |
+
minimum_runs_max = scored_df["Minimum Forward Model Runs"].max()
|
| 235 |
+
if minimum_runs_max > minimum_runs_min:
|
| 236 |
+
scored_df["minimum_runs_score"] = (
|
| 237 |
+
100.0 * (minimum_runs_max - scored_df["Minimum Forward Model Runs"]) / (minimum_runs_max - minimum_runs_min)
|
| 238 |
+
)
|
| 239 |
+
else:
|
| 240 |
+
scored_df["minimum_runs_score"] = 100.0
|
| 241 |
+
|
| 242 |
+
ens_min = scored_df["Optimal Ensemble Size"].min()
|
| 243 |
+
ens_max = scored_df["Optimal Ensemble Size"].max()
|
| 244 |
+
if ens_max > ens_min:
|
| 245 |
+
scored_df["ensemble_score"] = (
|
| 246 |
+
100.0 * (ens_max - scored_df["Optimal Ensemble Size"]) / (ens_max - ens_min)
|
| 247 |
+
)
|
| 248 |
+
else:
|
| 249 |
+
scored_df["ensemble_score"] = 100.0
|
| 250 |
+
|
| 251 |
+
if scoring_mode == "Mean Forward Model Runs":
|
| 252 |
+
scored_df["Score"] = scored_df["mean_runs_score"]
|
| 253 |
+
sort_columns = ["Mean Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
|
| 254 |
+
ascending = [True, True, True]
|
| 255 |
+
elif scoring_mode == "Minimum Forward Model Runs":
|
| 256 |
+
scored_df["Score"] = scored_df["minimum_runs_score"]
|
| 257 |
+
sort_columns = ["Minimum Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
|
| 258 |
+
ascending = [True, True, True]
|
| 259 |
+
elif scoring_mode == "Smallest Optimal Ensemble Size":
|
| 260 |
+
scored_df["Score"] = scored_df["ensemble_score"]
|
| 261 |
+
sort_columns = ["Optimal Ensemble Size", "Mean Forward Model Runs", "abbreviation"]
|
| 262 |
+
ascending = [True, True, True]
|
| 263 |
+
else:
|
| 264 |
+
scored_df["Score"] = (
|
| 265 |
+
fwdruns_weight * scored_df["mean_runs_score"]
|
| 266 |
+
+ ensemble_weight * scored_df["ensemble_score"]
|
| 267 |
+
)
|
| 268 |
+
sort_columns = ["Score", "Mean Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
|
| 269 |
+
ascending = [False, True, True, True]
|
| 270 |
+
|
| 271 |
+
scored_df = scored_df.sort_values(sort_columns, ascending=ascending).reset_index(drop=True)
|
| 272 |
+
|
| 273 |
+
if add_rank:
|
| 274 |
+
scored_df["Rank"] = scored_df.index + 1
|
| 275 |
+
scored_df["Placement"] = scored_df["Rank"].apply(
|
| 276 |
+
lambda rank: f"{ {1: 'π₯', 2: 'π₯', 3: 'π₯'}.get(rank, '')} #{rank}".strip()
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
return scored_df
|
| 280 |
+
|
| 281 |
+
if selected_benchmark == "All":
|
| 282 |
+
benchmark_scores = []
|
| 283 |
+
for benchmark_name in benchmark_values:
|
| 284 |
+
benchmark_df = metric_store[metric_store["benchmark"] == benchmark_name].copy()
|
| 285 |
+
benchmark_df[target_str_col] = benchmark_df[target_col].astype(str)
|
| 286 |
+
scored = build_scored_table(benchmark_df, add_rank=False)
|
| 287 |
+
if scored.empty:
|
| 288 |
+
continue
|
| 289 |
+
scored["benchmark"] = benchmark_name
|
| 290 |
+
benchmark_scores.append(scored)
|
| 291 |
+
|
| 292 |
+
if benchmark_scores:
|
| 293 |
+
combined_scores = (
|
| 294 |
+
benchmark_scores[0].copy()
|
| 295 |
+
if len(benchmark_scores) == 1
|
| 296 |
+
else pd.concat(benchmark_scores, ignore_index=True)
|
| 297 |
+
)
|
| 298 |
+
leaderboard_df = combined_scores.groupby(
|
| 299 |
+
["algorithm_type", "abbreviation", "Method", "family"], as_index=False
|
| 300 |
+
).agg(
|
| 301 |
+
Score=("Score", "mean"),
|
| 302 |
+
**{"Mean Forward Model Runs": ("Mean Forward Model Runs", "mean")},
|
| 303 |
+
**{"Minimum Forward Model Runs": ("Minimum Forward Model Runs", "mean")},
|
| 304 |
+
**{"Optimal Ensemble Size": ("Optimal Ensemble Size", "mean")},
|
| 305 |
+
**{"Targets Used": ("Targets Used", "mean")},
|
| 306 |
+
**{"Ensemble Sizes Used": ("Ensemble Sizes Used", "mean")},
|
| 307 |
+
**{"Benchmarks Used": ("benchmark", "nunique")},
|
| 308 |
+
**{"Mean Failure Rate (%)": ("Mean Failure Rate (%)", "mean")},
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
leaderboard_df["Mean Forward Model Runs"] = leaderboard_df["Mean Forward Model Runs"].round(4)
|
| 312 |
+
leaderboard_df["Minimum Forward Model Runs"] = leaderboard_df["Minimum Forward Model Runs"].round(4)
|
| 313 |
+
leaderboard_df["Optimal Ensemble Size"] = leaderboard_df["Optimal Ensemble Size"].round(2)
|
| 314 |
+
leaderboard_df["Targets Used"] = leaderboard_df["Targets Used"].round().astype(int)
|
| 315 |
+
leaderboard_df["Ensemble Sizes Used"] = leaderboard_df["Ensemble Sizes Used"].round().astype(int)
|
| 316 |
+
leaderboard_df["Mean Failure Rate (%)"] = leaderboard_df["Mean Failure Rate (%)"].round(1)
|
| 317 |
+
leaderboard_df = leaderboard_df.sort_values(
|
| 318 |
+
["Score", "Mean Forward Model Runs", "abbreviation"], ascending=[False, True, True]
|
| 319 |
+
).reset_index(drop=True)
|
| 320 |
+
leaderboard_df["Rank"] = leaderboard_df.index + 1
|
| 321 |
+
leaderboard_df["Placement"] = leaderboard_df["Rank"].apply(
|
| 322 |
+
lambda rank: f"{ {1: 'π₯', 2: 'π₯', 3: 'π₯'}.get(rank, '')} #{rank}".strip()
|
| 323 |
+
)
|
| 324 |
+
else:
|
| 325 |
+
leaderboard_df = pd.DataFrame(
|
| 326 |
+
columns=[
|
| 327 |
+
"Placement",
|
| 328 |
+
"abbreviation",
|
| 329 |
+
"Method",
|
| 330 |
+
"family",
|
| 331 |
+
"Mean Forward Model Runs",
|
| 332 |
+
"Minimum Forward Model Runs",
|
| 333 |
+
"Score",
|
| 334 |
+
"Optimal Ensemble Size",
|
| 335 |
+
"Targets Used",
|
| 336 |
+
"Ensemble Sizes Used",
|
| 337 |
+
"Benchmarks Used",
|
| 338 |
+
"Mean Failure Rate (%)",
|
| 339 |
+
]
|
| 340 |
+
)
|
| 341 |
+
else:
|
| 342 |
+
leaderboard_df = build_scored_table(filtered, add_rank=True)
|
| 343 |
+
leaderboard_df["Benchmarks Used"] = 1
|
| 344 |
+
|
| 345 |
+
if scoring_mode == "Mean Forward Model Runs":
|
| 346 |
+
score_basis = "normalized mean of best forward model runs over selected target levels (lower is better)"
|
| 347 |
+
elif scoring_mode == "Minimum Forward Model Runs":
|
| 348 |
+
score_basis = "normalized minimum of forward model runs over selected targets and ensemble sizes (lower is better)"
|
| 349 |
+
elif scoring_mode == "Smallest Optimal Ensemble Size":
|
| 350 |
+
score_basis = "normalized mean optimal ensemble size over selected target levels (lower is better)"
|
| 351 |
+
else:
|
| 352 |
+
score_basis = (
|
| 353 |
+
"weighted blend of normalized forward-model-runs score and normalized ensemble-size score "
|
| 354 |
+
f"(forward-runs weight {fwdruns_weight:.0%}, ensemble-size weight {ensemble_weight:.0%})"
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
if selected_benchmark == "All":
|
| 358 |
+
score_basis = f"{score_basis}; in All mode, each method's final score is the mean of its per-benchmark scores"
|
| 359 |
+
|
| 360 |
+
# Controls expander β always shown so users can change target even when the
|
| 361 |
+
# current selection yields all failures.
|
| 362 |
+
with st.expander("Scoring & Target Controls", expanded=False):
|
| 363 |
+
st.radio(
|
| 364 |
+
target_label,
|
| 365 |
+
options=target_options,
|
| 366 |
+
horizontal=True,
|
| 367 |
+
key=k_target,
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
if show_scoring_modes:
|
| 371 |
+
st.radio(
|
| 372 |
+
"Scoring Method",
|
| 373 |
+
options=scoring_options,
|
| 374 |
+
horizontal=True,
|
| 375 |
+
key=k_scoring,
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
if st.session_state.get(k_scoring, "Mean Forward Model Runs") == "Custom Blend":
|
| 379 |
+
st.slider(
|
| 380 |
+
"Blend Weight: Forward Runs vs Ensemble Size",
|
| 381 |
+
min_value=0,
|
| 382 |
+
max_value=100,
|
| 383 |
+
step=5,
|
| 384 |
+
key=k_weight,
|
| 385 |
+
help=(
|
| 386 |
+
"Higher forward-runs weight prioritizes fewer model evaluations; "
|
| 387 |
+
"higher ensemble-size weight prioritizes smaller ensembles."
|
| 388 |
+
),
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
st.multiselect(
|
| 392 |
+
"Methods to display in charts",
|
| 393 |
+
options=available_methods,
|
| 394 |
+
key=k_methods,
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
selected_methods = st.session_state.get(k_methods, available_methods)
|
| 398 |
+
if not selected_methods:
|
| 399 |
+
selected_methods = available_methods
|
| 400 |
+
|
| 401 |
+
if leaderboard_df.empty:
|
| 402 |
+
st.warning(
|
| 403 |
+
"All runs failed to reach the target at this selection. "
|
| 404 |
+
"See the failure rate chart below."
|
| 405 |
+
if show_failure_panel
|
| 406 |
+
else "No rows available for the current benchmark/target selection."
|
| 407 |
+
)
|
| 408 |
+
else:
|
| 409 |
+
if selected_benchmark == "All":
|
| 410 |
+
table_column_order = [
|
| 411 |
+
"Placement",
|
| 412 |
+
"abbreviation",
|
| 413 |
+
"Method",
|
| 414 |
+
"family",
|
| 415 |
+
"Score",
|
| 416 |
+
"Mean Failure Rate (%)",
|
| 417 |
+
"Targets Used",
|
| 418 |
+
"Ensemble Sizes Used",
|
| 419 |
+
"Benchmarks Used",
|
| 420 |
+
]
|
| 421 |
+
else:
|
| 422 |
+
table_column_order = [
|
| 423 |
+
"Placement",
|
| 424 |
+
"abbreviation",
|
| 425 |
+
"Method",
|
| 426 |
+
"family",
|
| 427 |
+
"Score",
|
| 428 |
+
"Mean Forward Model Runs",
|
| 429 |
+
"Minimum Forward Model Runs",
|
| 430 |
+
"Mean Failure Rate (%)",
|
| 431 |
+
"Optimal Ensemble Size",
|
| 432 |
+
"Targets Used",
|
| 433 |
+
"Ensemble Sizes Used",
|
| 434 |
+
"Benchmarks Used",
|
| 435 |
+
]
|
| 436 |
+
|
| 437 |
+
st.subheader(f"Ranked Leaderboard β {selected_benchmark}")
|
| 438 |
+
st.dataframe(
|
| 439 |
+
leaderboard_df,
|
| 440 |
+
hide_index=True,
|
| 441 |
+
use_container_width=True,
|
| 442 |
+
column_config={
|
| 443 |
+
"Placement": st.column_config.TextColumn("Placement"),
|
| 444 |
+
"family": st.column_config.TextColumn("Family"),
|
| 445 |
+
"Method": st.column_config.TextColumn("Method"),
|
| 446 |
+
"abbreviation": st.column_config.TextColumn("Abbrev."),
|
| 447 |
+
"Mean Forward Model Runs": st.column_config.NumberColumn("Mean Forward Model Runs", format="%.4f"),
|
| 448 |
+
"Minimum Forward Model Runs": st.column_config.NumberColumn("Minimum Forward Model Runs", format="%.4f"),
|
| 449 |
+
"Score": st.column_config.ProgressColumn("Score (0-100)", min_value=0.0, max_value=100.0, format="%.1f"),
|
| 450 |
+
"Optimal Ensemble Size": st.column_config.NumberColumn("Mean Optimal Ensemble Size", format="%.2f"),
|
| 451 |
+
"Mean Failure Rate (%)": st.column_config.NumberColumn("Mean Failure Rate (%)", format="%.1f"),
|
| 452 |
+
"Targets Used": st.column_config.NumberColumn("Targets Used", format="%d"),
|
| 453 |
+
"Ensemble Sizes Used": st.column_config.NumberColumn("Ensemble Sizes Used", format="%d"),
|
| 454 |
+
"Benchmarks Used": st.column_config.NumberColumn("Benchmarks Used", format="%d"),
|
| 455 |
+
},
|
| 456 |
+
column_order=table_column_order,
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
st.info(
|
| 460 |
+
f"Score is a normalized 0β100 ranking based on **{score_basis}**. "
|
| 461 |
+
"For Mean/Minimum forward-runs scoring, values are computed from all selected metric target levels "
|
| 462 |
+
"and all ensemble sizes after averaging over random seeds."
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
if selected_benchmark != "All":
|
| 466 |
+
st.subheader("Mean Forward Model Runs vs Ensemble Size")
|
| 467 |
+
chart_source = (
|
| 468 |
+
filtered
|
| 469 |
+
if selected_target == "All targets"
|
| 470 |
+
else filtered[filtered[target_str_col] == selected_target]
|
| 471 |
+
)
|
| 472 |
+
chart_source = chart_source[chart_source["abbreviation"].isin(selected_methods)]
|
| 473 |
+
chart_df = chart_source.dropna(subset=["metric"]).groupby(
|
| 474 |
+
["abbreviation", "ensemble_size"], as_index=False
|
| 475 |
+
).agg(mean_forward_runs=("metric", "mean"))
|
| 476 |
+
|
| 477 |
+
all_ens_combos = chart_source[["abbreviation", "ensemble_size"]].drop_duplicates()
|
| 478 |
+
ens_ticks = sorted(all_ens_combos["ensemble_size"].unique().tolist()) if not all_ens_combos.empty else []
|
| 479 |
+
|
| 480 |
+
if not chart_df.empty:
|
| 481 |
+
_ok = chart_df[["abbreviation", "ensemble_size"]].assign(_ok=True)
|
| 482 |
+
fail_df = all_ens_combos.merge(_ok, on=["abbreviation", "ensemble_size"], how="left")
|
| 483 |
+
fail_df = fail_df[fail_df["_ok"].isna()].drop(columns="_ok").assign(mean_forward_runs=0.0)
|
| 484 |
+
else:
|
| 485 |
+
fail_df = all_ens_combos.assign(mean_forward_runs=0.0)
|
| 486 |
+
|
| 487 |
+
all_failed = chart_df.empty
|
| 488 |
+
chart_layers = []
|
| 489 |
+
if not chart_df.empty:
|
| 490 |
+
chart_layers.append(
|
| 491 |
+
alt.Chart(chart_df)
|
| 492 |
+
.mark_line(point=True)
|
| 493 |
+
.encode(
|
| 494 |
+
x=alt.X(
|
| 495 |
+
"ensemble_size:Q",
|
| 496 |
+
title="Ensemble Size",
|
| 497 |
+
axis=alt.Axis(values=ens_ticks, format="d"),
|
| 498 |
+
),
|
| 499 |
+
y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
|
| 500 |
+
color=method_color,
|
| 501 |
+
tooltip=["abbreviation", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
|
| 502 |
+
)
|
| 503 |
+
)
|
| 504 |
+
if not fail_df.empty:
|
| 505 |
+
y_fwd = (
|
| 506 |
+
alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs", scale=alt.Scale(domain=[0, 1]))
|
| 507 |
+
if all_failed
|
| 508 |
+
else alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs")
|
| 509 |
+
)
|
| 510 |
+
chart_layers.append(
|
| 511 |
+
alt.Chart(fail_df)
|
| 512 |
+
.mark_point(shape="cross", angle=45, size=200, filled=True, opacity=1.0)
|
| 513 |
+
.encode(
|
| 514 |
+
x=alt.X(
|
| 515 |
+
"ensemble_size:Q",
|
| 516 |
+
title="Ensemble Size",
|
| 517 |
+
axis=alt.Axis(values=ens_ticks, format="d"),
|
| 518 |
+
),
|
| 519 |
+
y=y_fwd,
|
| 520 |
+
color=method_color,
|
| 521 |
+
tooltip=[
|
| 522 |
+
alt.Tooltip("abbreviation:N", title="Method"),
|
| 523 |
+
alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
|
| 524 |
+
alt.Tooltip("mean_forward_runs:Q", title="Value (all failed)"),
|
| 525 |
+
],
|
| 526 |
+
)
|
| 527 |
+
)
|
| 528 |
+
if chart_layers:
|
| 529 |
+
st.altair_chart(alt.layer(*chart_layers), use_container_width=True)
|
| 530 |
+
|
| 531 |
+
# Mean-iterations-for-coverage section (UQ only, when budget_store provided)
|
| 532 |
+
if budget_store is not None and not budget_store.empty and selected_benchmark != "All":
|
| 533 |
+
bf = budget_store[budget_store["benchmark"] == selected_benchmark].copy()
|
| 534 |
+
bf[target_str_col] = bf[target_col].astype(str)
|
| 535 |
+
if selected_target != "All targets":
|
| 536 |
+
bf = bf[bf[target_str_col] == selected_target]
|
| 537 |
+
bf = bf[bf["abbreviation"].isin(selected_methods)]
|
| 538 |
+
|
| 539 |
+
iters_df = (
|
| 540 |
+
bf[["abbreviation", "ensemble_size", "mean_iters"]]
|
| 541 |
+
.dropna(subset=["mean_iters"])
|
| 542 |
+
.groupby(["abbreviation", "ensemble_size"], as_index=False)
|
| 543 |
+
.agg(mean_iters=("mean_iters", "mean"))
|
| 544 |
+
)
|
| 545 |
+
all_ens_combos_iters = bf[["abbreviation", "ensemble_size"]].drop_duplicates()
|
| 546 |
+
if not all_ens_combos_iters.empty:
|
| 547 |
+
iters_ticks = sorted(all_ens_combos_iters["ensemble_size"].unique().tolist())
|
| 548 |
+
|
| 549 |
+
if not iters_df.empty:
|
| 550 |
+
_ok_iters = iters_df[["abbreviation", "ensemble_size"]].assign(_ok=True)
|
| 551 |
+
fail_df_iters = all_ens_combos_iters.merge(_ok_iters, on=["abbreviation", "ensemble_size"], how="left")
|
| 552 |
+
fail_df_iters = fail_df_iters[fail_df_iters["_ok"].isna()].drop(columns="_ok").assign(mean_iters=0.0)
|
| 553 |
+
else:
|
| 554 |
+
fail_df_iters = all_ens_combos_iters.assign(mean_iters=0.0)
|
| 555 |
+
|
| 556 |
+
all_failed_iters = iters_df.empty
|
| 557 |
+
st.subheader("Mean Iterations for Coverage vs Ensemble Size")
|
| 558 |
+
iters_layers = []
|
| 559 |
+
if not iters_df.empty:
|
| 560 |
+
iters_layers.append(
|
| 561 |
+
alt.Chart(iters_df)
|
| 562 |
+
.mark_line(point=True)
|
| 563 |
+
.encode(
|
| 564 |
+
x=alt.X(
|
| 565 |
+
"ensemble_size:Q",
|
| 566 |
+
title="Ensemble Size",
|
| 567 |
+
axis=alt.Axis(values=iters_ticks, format="d"),
|
| 568 |
+
),
|
| 569 |
+
y=alt.Y("mean_iters:Q", title="Mean Iterations"),
|
| 570 |
+
color=method_color,
|
| 571 |
+
tooltip=[
|
| 572 |
+
alt.Tooltip("abbreviation:N", title="Method"),
|
| 573 |
+
alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
|
| 574 |
+
alt.Tooltip("mean_iters:Q", format=".2f", title="Mean Iterations"),
|
| 575 |
+
],
|
| 576 |
+
)
|
| 577 |
+
)
|
| 578 |
+
if not fail_df_iters.empty:
|
| 579 |
+
y_iters = (
|
| 580 |
+
alt.Y("mean_iters:Q", title="Mean Iterations", scale=alt.Scale(domain=[0, 1]))
|
| 581 |
+
if all_failed_iters
|
| 582 |
+
else alt.Y("mean_iters:Q", title="Mean Iterations")
|
| 583 |
+
)
|
| 584 |
+
iters_layers.append(
|
| 585 |
+
alt.Chart(fail_df_iters)
|
| 586 |
+
.mark_point(shape="cross", angle=45, size=200, filled=True, opacity=1.0)
|
| 587 |
+
.encode(
|
| 588 |
+
x=alt.X(
|
| 589 |
+
"ensemble_size:Q",
|
| 590 |
+
title="Ensemble Size",
|
| 591 |
+
axis=alt.Axis(values=iters_ticks, format="d"),
|
| 592 |
+
),
|
| 593 |
+
y=y_iters,
|
| 594 |
+
color=method_color,
|
| 595 |
+
tooltip=[
|
| 596 |
+
alt.Tooltip("abbreviation:N", title="Method"),
|
| 597 |
+
alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
|
| 598 |
+
alt.Tooltip("mean_iters:Q", title="Value (all failed)"),
|
| 599 |
+
],
|
| 600 |
+
)
|
| 601 |
+
)
|
| 602 |
+
if iters_layers:
|
| 603 |
+
st.altair_chart(alt.layer(*iters_layers), use_container_width=True)
|
| 604 |
+
|
| 605 |
+
# Failure panel β rendered regardless of whether the scored table has rows
|
| 606 |
+
if show_failure_panel and selected_benchmark != "All":
|
| 607 |
+
failure_source = (
|
| 608 |
+
filtered
|
| 609 |
+
if selected_target == "All targets"
|
| 610 |
+
else filtered[filtered[target_str_col] == selected_target]
|
| 611 |
+
)
|
| 612 |
+
failure_source = failure_source[failure_source["abbreviation"].isin(selected_methods)]
|
| 613 |
+
if not failure_source.empty:
|
| 614 |
+
failure_df = failure_source.groupby(
|
| 615 |
+
["abbreviation", "ensemble_size"], as_index=False
|
| 616 |
+
).agg(mean_failure_rate=("failure_rate", "mean"))
|
| 617 |
+
failure_df = failure_df.sort_values("ensemble_size")
|
| 618 |
+
|
| 619 |
+
target_str = (
|
| 620 |
+
"All Targets"
|
| 621 |
+
if selected_target == "All targets"
|
| 622 |
+
else f"Target {selected_target}"
|
| 623 |
+
)
|
| 624 |
+
st.subheader(f"Failure Rate of Hitting {target_str}")
|
| 625 |
+
ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
|
| 626 |
+
failure_chart = (
|
| 627 |
+
alt.Chart(failure_df)
|
| 628 |
+
.mark_bar()
|
| 629 |
+
.encode(
|
| 630 |
+
x=alt.X(
|
| 631 |
+
"ensemble_size:O",
|
| 632 |
+
title="Ensemble Size",
|
| 633 |
+
sort=[str(e) for e in ens_ticks_fail],
|
| 634 |
+
axis=alt.Axis(labelAngle=0),
|
| 635 |
+
),
|
| 636 |
+
xOffset=alt.XOffset("abbreviation:N"),
|
| 637 |
+
y=alt.Y(
|
| 638 |
+
"mean_failure_rate:Q",
|
| 639 |
+
title="Failure Rate (%)",
|
| 640 |
+
scale=alt.Scale(domain=[0, 100]),
|
| 641 |
+
),
|
| 642 |
+
color=method_color,
|
| 643 |
+
tooltip=[
|
| 644 |
+
alt.Tooltip("abbreviation:N", title="Method"),
|
| 645 |
+
alt.Tooltip("ensemble_size:O", title="Ensemble Size"),
|
| 646 |
+
alt.Tooltip("mean_failure_rate:Q", format=".1f", title="Failure Rate (%)"),
|
| 647 |
+
],
|
| 648 |
+
)
|
| 649 |
+
)
|
| 650 |
+
ceiling_line = (
|
| 651 |
+
alt.Chart(alt.Data(values=[{}]))
|
| 652 |
+
.mark_rule(color="grey", strokeDash=[4, 4])
|
| 653 |
+
.encode(y=alt.datum(100))
|
| 654 |
+
)
|
| 655 |
+
st.altair_chart(failure_chart + ceiling_line, use_container_width=True)
|
| 656 |
+
|
| 657 |
+
st.caption("Top 3 are shown as podium spots; remaining methods are directly comparable via normalized score.")
|
| 658 |
+
|
| 659 |
+
if raw_page is not None:
|
| 660 |
+
st.page_link(raw_page, label="Open Raw Data & CSV Export", icon="π§Ύ")
|
src/common/method_registry.py
CHANGED
|
@@ -21,7 +21,7 @@ KNOWN_METHODS = {
|
|
| 21 |
"abbreviation": "IEKF",
|
| 22 |
"Method": "Iterative Ensemble Kalman Filter",
|
| 23 |
"family": "Kalman",
|
| 24 |
-
"aliases": ["iekf", "gnsl"],
|
| 25 |
},
|
| 26 |
"uki": {
|
| 27 |
"abbreviation": "UKI",
|
|
@@ -41,6 +41,12 @@ KNOWN_METHODS = {
|
|
| 41 |
"family": "Bayesian",
|
| 42 |
"aliases": ["hm"],
|
| 43 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
|
|
|
|
| 21 |
"abbreviation": "IEKF",
|
| 22 |
"Method": "Iterative Ensemble Kalman Filter",
|
| 23 |
"family": "Kalman",
|
| 24 |
+
"aliases": ["iekf", "gnsl", "gnki"],
|
| 25 |
},
|
| 26 |
"uki": {
|
| 27 |
"abbreviation": "UKI",
|
|
|
|
| 41 |
"family": "Bayesian",
|
| 42 |
"aliases": ["hm"],
|
| 43 |
},
|
| 44 |
+
"ces-eki-dmc": {
|
| 45 |
+
"abbreviation": "CES-EKI-DMC",
|
| 46 |
+
"Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
|
| 47 |
+
"family": "calibrate_then_emulate",
|
| 48 |
+
"aliases": ["ces-eki-dmc"]
|
| 49 |
+
}
|
| 50 |
}
|
| 51 |
|
| 52 |
|
src/data_store.py
CHANGED
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import importlib
|
| 4 |
from pathlib import Path
|
| 5 |
-
import warnings
|
|
|
|
| 6 |
import pandas as pd
|
| 7 |
import streamlit as st
|
| 8 |
|
|
@@ -22,6 +23,10 @@ except ModuleNotFoundError:
|
|
| 22 |
)
|
| 23 |
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
DATASET_FILES = {
|
| 26 |
"L63": [
|
| 27 |
"l63_ensemble_results.nc",
|
|
@@ -45,13 +50,66 @@ DATASET_FILES = {
|
|
| 45 |
],
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,7 +117,7 @@ def load_metric_store() -> pd.DataFrame:
|
|
| 59 |
|
| 60 |
records: list[pd.DataFrame] = []
|
| 61 |
|
| 62 |
-
for benchmark_name, filenames in
|
| 63 |
for filename in filenames:
|
| 64 |
file_path = data_dir / filename
|
| 65 |
if not file_path.exists():
|
|
@@ -72,44 +130,53 @@ def load_metric_store() -> pd.DataFrame:
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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[
|
| 110 |
|
| 111 |
if not records:
|
| 112 |
-
return pd.DataFrame(
|
|
|
|
|
|
|
| 113 |
|
| 114 |
merged = pd.concat(records, ignore_index=True)
|
| 115 |
merged["algorithm_alias"] = merged["algorithm_type"].map(normalize_method_name)
|
|
@@ -117,10 +184,11 @@ def load_metric_store() -> pd.DataFrame:
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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"))
|
|
@@ -136,23 +204,159 @@ def load_metric_store() -> pd.DataFrame:
|
|
| 136 |
return merged
|
| 137 |
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
return pd.DataFrame(
|
| 142 |
columns=[
|
| 143 |
-
"
|
| 144 |
-
"Method",
|
| 145 |
-
"abbreviation",
|
| 146 |
-
"benchmark",
|
| 147 |
-
"Forward Model Runs",
|
| 148 |
-
"Optimal Ensemble Size",
|
| 149 |
-
"Target Level",
|
| 150 |
]
|
| 151 |
)
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
| 158 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
)
|
| 24 |
|
| 25 |
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Optimization (RMSE) datasets
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
|
| 30 |
DATASET_FILES = {
|
| 31 |
"L63": [
|
| 32 |
"l63_ensemble_results.nc",
|
|
|
|
| 50 |
],
|
| 51 |
}
|
| 52 |
|
| 53 |
+
EXPECTED_DIMS_OPT = ("algorithm_type", "rmse_target", "ensemble_size", "random_seed")
|
| 54 |
+
EXPECTED_DIMS_UQ = ("random_seed", "ensemble_size", "k_iter", "coverage_quantile")
|
| 55 |
|
| 56 |
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Uncertainty Quantification datasets
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
# Add entries here when UQ result NetCDF files are available. Each file must
|
| 61 |
+
# carry a ``metric`` variable and a ``uq_target`` coordinate (in place of the
|
| 62 |
+
# ``rmse_target`` used by the optimization files). The rest of the schema
|
| 63 |
+
# (algorithm_type, ensemble_size, random_seed) is identical.
|
| 64 |
+
#
|
| 65 |
+
# Example:
|
| 66 |
+
# UQ_DATASET_FILES = {
|
| 67 |
+
# "L63": ["uq/l63_uq_results.nc"],
|
| 68 |
+
# "L96": ["uq/l96_uq_results.nc"],
|
| 69 |
+
# }
|
| 70 |
|
| 71 |
+
UQ_DATASET_FILES: dict[str, list[str]] = {}
|
| 72 |
+
|
| 73 |
+
# Canonical UQ target-scaling levels always offered in the UQ leaderboard selector.
|
| 74 |
+
UQ_TARGET_LEVELS: list[float] = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5]
|
| 75 |
+
|
| 76 |
+
# Budget-for-coverage source files: maps benchmark β list of (algorithm_type, nc_path) pairs.
|
| 77 |
+
# Each NC must carry output_coverage (coverage_quantile, k_iter, ensemble_size, random_seed),
|
| 78 |
+
# target_scaling, and output_dim.
|
| 79 |
+
UQ_BUDGET_FILES: dict[str, list[tuple[str, str]]] = {
|
| 80 |
+
"L63": [("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l63_ensemble_results_2026-06-15_minimal.nc")],
|
| 81 |
+
"L96": [("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_ensemble_results_2026-06-15_minimal.nc")],
|
| 82 |
+
"L96_NN_FORCING": [("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_nn_forcing_ensemble_results_2026-06-15_minimal.nc")],
|
| 83 |
+
"L96_SPATIAL_FORCING": [("ces-eki-dmc", "ces-eki-dmc_results/ces-eki-dmc_l96_spatial_forcing_ensemble_results_2026-06-15_minimal.nc")],
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Quantile levels used for the all-quantiles-satisfied budget-for-coverage condition.
|
| 87 |
+
# Budget = N_ens Β· k_iter where first k s.t. |S(q)βq| β€ cΒ·β(q(1βq)/N_y) for ALL q below.
|
| 88 |
+
UQ_COVERAGE_QUANTILES: list[float] = [0.15, 0.5, 0.85]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ---------------------------------------------------------------------------
|
| 93 |
+
# Private loader β shared by both public load_* functions
|
| 94 |
+
# ---------------------------------------------------------------------------
|
| 95 |
+
|
| 96 |
+
def _load_store(dataset_files: dict[str, list[str]], target_col: str, *, drop_nan_metric: bool = True) -> pd.DataFrame:
|
| 97 |
+
"""Load and merge NetCDF metric files into a long-format DataFrame.
|
| 98 |
+
|
| 99 |
+
Parameters
|
| 100 |
+
----------
|
| 101 |
+
dataset_files:
|
| 102 |
+
Mapping of benchmark name β list of relative NetCDF paths under
|
| 103 |
+
``data/``.
|
| 104 |
+
target_col:
|
| 105 |
+
Name of the target-coordinate dimension in the NetCDF files, e.g.
|
| 106 |
+
``"rmse_target"`` or ``"uq_target"``.
|
| 107 |
+
"""
|
| 108 |
+
if not dataset_files:
|
| 109 |
+
return pd.DataFrame(
|
| 110 |
+
columns=["benchmark", "algorithm_type", target_col, "ensemble_size", "metric", "failure_rate"]
|
| 111 |
+
)
|
| 112 |
|
|
|
|
|
|
|
| 113 |
xr = importlib.import_module("xarray")
|
| 114 |
|
| 115 |
project_root = Path(__file__).resolve().parent.parent
|
|
|
|
| 117 |
|
| 118 |
records: list[pd.DataFrame] = []
|
| 119 |
|
| 120 |
+
for benchmark_name, filenames in dataset_files.items():
|
| 121 |
for filename in filenames:
|
| 122 |
file_path = data_dir / filename
|
| 123 |
if not file_path.exists():
|
|
|
|
| 130 |
continue
|
| 131 |
|
| 132 |
metric = dataset["metric"]
|
| 133 |
+
if not set(metric.dims).issubset(EXPECTED_DIMS_OPT):
|
| 134 |
+
warnings.warn(
|
| 135 |
+
f"{file_path.name}: metric dims {metric.dims} contain unexpected "
|
| 136 |
+
f"dimensions not in EXPECTED_DIMS_OPT {EXPECTED_DIMS_OPT} β skipping"
|
| 137 |
+
)
|
| 138 |
+
continue
|
| 139 |
+
|
| 140 |
# Track failures (metric == -1) as a percentage
|
| 141 |
failures_count = (metric == -1).sum(dim="random_seed")
|
| 142 |
|
| 143 |
failure_rate = (failures_count / dataset.sizes["random_seed"]) * 100
|
| 144 |
+
|
| 145 |
+
# Filter out failed runs (metric == -1)
|
| 146 |
metric = metric.where(metric != -1)
|
| 147 |
+
|
| 148 |
metric_mean = metric.mean(dim="random_seed", skipna=True)
|
| 149 |
df = metric_mean.to_dataframe(name="metric").reset_index()
|
| 150 |
+
|
| 151 |
df_failures = failure_rate.to_dataframe(name="failure_rate").reset_index()
|
| 152 |
df["failure_rate"] = df_failures["failure_rate"]
|
| 153 |
|
| 154 |
df["benchmark"] = benchmark_name
|
| 155 |
+
|
| 156 |
if "algorithm_type" not in df.columns:
|
| 157 |
if "abc" in file_path.name:
|
| 158 |
df["algorithm_type"] = "abc"
|
| 159 |
elif "hm" in file_path.name:
|
| 160 |
df["algorithm_type"] = "hm"
|
| 161 |
+
|
| 162 |
if "ensemble_size" not in df.columns:
|
| 163 |
if "abc" in file_path.name:
|
| 164 |
df["ensemble_size"] = 1
|
| 165 |
+
|
| 166 |
+
required_cols = ["benchmark", "algorithm_type", target_col, "ensemble_size", "metric", "failure_rate"]
|
| 167 |
+
missing_cols = [c for c in required_cols if c not in df.columns]
|
| 168 |
if missing_cols:
|
| 169 |
st.warning(
|
| 170 |
f"Skipping {file_path.name}: metric dataframe is missing expected columns {missing_cols}"
|
| 171 |
)
|
| 172 |
continue
|
| 173 |
|
| 174 |
+
records.append(df[required_cols])
|
| 175 |
|
| 176 |
if not records:
|
| 177 |
+
return pd.DataFrame(
|
| 178 |
+
columns=["benchmark", "algorithm_type", target_col, "ensemble_size", "metric", "failure_rate"]
|
| 179 |
+
)
|
| 180 |
|
| 181 |
merged = pd.concat(records, ignore_index=True)
|
| 182 |
merged["algorithm_alias"] = merged["algorithm_type"].map(normalize_method_name)
|
|
|
|
| 184 |
merged["ensemble_size"] = pd.to_numeric(merged["ensemble_size"], errors="coerce")
|
| 185 |
merged["metric"] = pd.to_numeric(merged["metric"], errors="coerce")
|
| 186 |
merged["failure_rate"] = pd.to_numeric(merged["failure_rate"], errors="coerce").fillna(0.0)
|
| 187 |
+
if drop_nan_metric:
|
| 188 |
+
merged = merged.dropna(subset=["ensemble_size", "metric"])
|
| 189 |
+
else:
|
| 190 |
+
merged = merged.dropna(subset=["ensemble_size"])
|
| 191 |
merged["ensemble_size"] = merged["ensemble_size"].astype(int)
|
|
|
|
|
|
|
| 192 |
merged["abbreviation"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("abbreviation"))
|
| 193 |
merged["Method"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("Method"))
|
| 194 |
merged["family"] = merged["algorithm_type"].map(lambda method: get_method_meta(method).get("family"))
|
|
|
|
| 204 |
return merged
|
| 205 |
|
| 206 |
|
| 207 |
+
# ---------------------------------------------------------------------------
|
| 208 |
+
# Public cached loaders
|
| 209 |
+
# ---------------------------------------------------------------------------
|
| 210 |
+
|
| 211 |
+
@st.cache_data(show_spinner=False)
|
| 212 |
+
def load_metric_store() -> pd.DataFrame:
|
| 213 |
+
"""Load the optimization (RMSE) metric store from ``DATASET_FILES``."""
|
| 214 |
+
return _load_store(DATASET_FILES, "rmse_target")
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
@st.cache_data(show_spinner=False)
|
| 218 |
+
def load_uq_store() -> pd.DataFrame:
|
| 219 |
+
"""Load the UQ metric store derived from budget-for-coverage computations.
|
| 220 |
+
|
| 221 |
+
Returns a DataFrame compatible with ``render_leaderboard`` where ``metric``
|
| 222 |
+
is the mean budget (N_ensΒ·k_iter) to satisfy the coverage condition across
|
| 223 |
+
all three quantiles in ``UQ_COVERAGE_QUANTILES``. ``uq_target`` is the
|
| 224 |
+
target-scaling level c. Failure means the coverage target was never
|
| 225 |
+
reached within the k_iter range.
|
| 226 |
+
"""
|
| 227 |
+
budget_df = load_uq_budget_store()
|
| 228 |
+
if budget_df.empty:
|
| 229 |
return pd.DataFrame(
|
| 230 |
columns=[
|
| 231 |
+
"benchmark", "algorithm_type", "algorithm_alias", "abbreviation",
|
| 232 |
+
"Method", "family", "uq_target", "ensemble_size", "metric", "failure_rate",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
]
|
| 234 |
)
|
| 235 |
+
df = budget_df.copy()
|
| 236 |
+
df["metric"] = df["mean_budget"]
|
| 237 |
+
df["algorithm_alias"] = df["algorithm_type"].map(normalize_method_name)
|
| 238 |
+
df["Method"] = df["algorithm_type"].map(
|
| 239 |
+
lambda m: get_method_meta(canonicalize_method_name(normalize_method_name(m))).get(
|
| 240 |
+
"Method", m.upper()
|
| 241 |
+
)
|
| 242 |
)
|
| 243 |
+
return df[
|
| 244 |
+
[
|
| 245 |
+
"benchmark", "algorithm_type", "algorithm_alias", "abbreviation",
|
| 246 |
+
"Method", "family", "uq_target", "ensemble_size", "metric", "failure_rate",
|
| 247 |
+
]
|
| 248 |
+
]
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@st.cache_data(show_spinner=False)
|
| 252 |
+
def load_uq_budget_store() -> pd.DataFrame:
|
| 253 |
+
"""Compute budget-for-coverage and iterations-for-coverage from ensemble result NC files.
|
| 254 |
+
|
| 255 |
+
For each (target_scaling c, ensemble_size N, random_seed):
|
| 256 |
+
Find the minimum k_iter s.t. |S(q) β q| β€ cΒ·β(q(1βq)/N_y) for ALL q in
|
| 257 |
+
UQ_COVERAGE_QUANTILES. Budget = N Β· k_iter; NaN when target never reached.
|
| 258 |
+
|
| 259 |
+
Returns a DataFrame with columns:
|
| 260 |
+
benchmark, algorithm_type, abbreviation, family, uq_target,
|
| 261 |
+
ensemble_size, mean_budget, mean_iters, failure_count, failure_rate, n_seeds
|
| 262 |
+
"""
|
| 263 |
+
xr = importlib.import_module("xarray")
|
| 264 |
+
|
| 265 |
+
project_root = Path(__file__).resolve().parent.parent
|
| 266 |
+
data_dir = project_root / "data"
|
| 267 |
+
|
| 268 |
+
bq_vals = np.array(UQ_COVERAGE_QUANTILES)
|
| 269 |
+
records: list[dict] = []
|
| 270 |
+
|
| 271 |
+
for benchmark_name, method_files in UQ_BUDGET_FILES.items():
|
| 272 |
+
for algorithm_type, filename in method_files:
|
| 273 |
+
file_path = data_dir / filename
|
| 274 |
+
if not file_path.exists():
|
| 275 |
+
warnings.warn(f"UQ budget file {file_path} does not exist")
|
| 276 |
+
continue
|
| 277 |
+
|
| 278 |
+
with xr.open_dataset(file_path) as ds:
|
| 279 |
+
if "output_coverage" not in ds:
|
| 280 |
+
warnings.warn(f"{file_path.name}: missing output_coverage")
|
| 281 |
+
continue
|
| 282 |
+
|
| 283 |
+
if set(ds["output_coverage"].dims) != set(EXPECTED_DIMS_UQ):
|
| 284 |
+
warnings.warn(
|
| 285 |
+
f"{file_path.name}: output_coverage dims {ds['output_coverage'].dims} "
|
| 286 |
+
f"do not match EXPECTED_DIMS_UQ {EXPECTED_DIMS_UQ} β skipping"
|
| 287 |
+
)
|
| 288 |
+
continue
|
| 289 |
+
|
| 290 |
+
cov_q = ds["coverage_quantile"].values # (n_cov_q,)
|
| 291 |
+
k_vals = ds["k_iter"].values # (n_k,) 1-indexed
|
| 292 |
+
ens_vals = ds["ensemble_size"].values # (n_ens,)
|
| 293 |
+
ts_vals = ds["target_scaling"].values # (n_ts,)
|
| 294 |
+
# xarray drops dimensions that no variable uses; read output_dim
|
| 295 |
+
# directly from the underlying netCDF4 file to handle minimal files.
|
| 296 |
+
nc4 = importlib.import_module("netCDF4")
|
| 297 |
+
with nc4.Dataset(file_path) as _nc:
|
| 298 |
+
if "output_dim" not in _nc.dimensions:
|
| 299 |
+
warnings.warn(f"{file_path.name}: missing output_dim dimension")
|
| 300 |
+
continue
|
| 301 |
+
n_y = len(_nc.dimensions["output_dim"])
|
| 302 |
+
# dims: (coverage_quantile, k_iter, ensemble_size, random_seed)
|
| 303 |
+
cov_np = ds["output_coverage"].values
|
| 304 |
+
|
| 305 |
+
n_rng = cov_np.shape[3]
|
| 306 |
+
|
| 307 |
+
# Indices into coverage_quantile for [0.15, 0.5, 0.85]
|
| 308 |
+
bq_idx = [int(np.argmin(np.abs(cov_q - q))) for q in bq_vals]
|
| 309 |
+
|
| 310 |
+
for si, c in enumerate(ts_vals):
|
| 311 |
+
tol = c * np.sqrt(bq_vals * (1.0 - bq_vals) / n_y)
|
| 312 |
+
|
| 313 |
+
for ei, ens_f in enumerate(ens_vals):
|
| 314 |
+
N_ens = int(ens_f)
|
| 315 |
+
budgets = np.full(n_rng, np.nan)
|
| 316 |
+
kiters = np.full(n_rng, np.nan)
|
| 317 |
+
|
| 318 |
+
for ri in range(n_rng):
|
| 319 |
+
for ki, kv in enumerate(k_vals):
|
| 320 |
+
s_q = cov_np[bq_idx, ki, ei, ri]
|
| 321 |
+
if np.any(np.isnan(s_q)):
|
| 322 |
+
continue
|
| 323 |
+
if np.all(np.abs(s_q - bq_vals) <= tol):
|
| 324 |
+
budgets[ri] = N_ens * float(kv)
|
| 325 |
+
kiters[ri] = float(kv)
|
| 326 |
+
break
|
| 327 |
+
|
| 328 |
+
valid_b = budgets[~np.isnan(budgets)]
|
| 329 |
+
valid_k = kiters[~np.isnan(kiters)]
|
| 330 |
+
n_fail = n_rng - len(valid_b)
|
| 331 |
+
records.append(
|
| 332 |
+
{
|
| 333 |
+
"benchmark": benchmark_name,
|
| 334 |
+
"algorithm_type": algorithm_type,
|
| 335 |
+
"uq_target": float(c),
|
| 336 |
+
"ensemble_size": N_ens,
|
| 337 |
+
"mean_budget": float(np.mean(valid_b)) if len(valid_b) > 0 else np.nan,
|
| 338 |
+
"mean_iters": float(np.mean(valid_k)) if len(valid_k) > 0 else np.nan,
|
| 339 |
+
"failure_count": n_fail,
|
| 340 |
+
"failure_rate": 100.0 * n_fail / n_rng,
|
| 341 |
+
"n_seeds": n_rng,
|
| 342 |
+
}
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
if not records:
|
| 346 |
+
return pd.DataFrame(
|
| 347 |
+
columns=[
|
| 348 |
+
"benchmark", "algorithm_type", "abbreviation", "family",
|
| 349 |
+
"uq_target", "ensemble_size", "mean_budget", "mean_iters",
|
| 350 |
+
"failure_count", "failure_rate", "n_seeds",
|
| 351 |
+
]
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
df = pd.DataFrame(records)
|
| 355 |
+
|
| 356 |
+
def _meta(m: str, key: str, fallback: str) -> str:
|
| 357 |
+
return get_method_meta(canonicalize_method_name(normalize_method_name(m))).get(key, fallback)
|
| 358 |
+
|
| 359 |
+
df["abbreviation"] = df["algorithm_type"].map(lambda m: _meta(m, "abbreviation", m.upper()))
|
| 360 |
+
df["family"] = df["algorithm_type"].map(lambda m: _meta(m, "family", "Kalman"))
|
| 361 |
+
df["Method"] = df["algorithm_type"].map(lambda m: _meta(m, "Method", m.upper()))
|
| 362 |
+
return df
|
src/media/posterior_ribbons_20_13_k5.png
ADDED
|
Git LFS Details
|
src/media/posterior_ribbons_const-force_12_1_k3.png
ADDED
|
Git LFS Details
|
src/media/posterior_ribbons_flux-force_80_1_k3.png
ADDED
|
Git LFS Details
|
src/media/posterior_ribbons_vec-force_65_1_k3.png
ADDED
|
Git LFS Details
|
src/pages/MethodDetails.py
CHANGED
|
@@ -15,6 +15,8 @@ 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/MethodDetails.py", label="Methods", icon="π")
|
| 19 |
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 20 |
|
|
@@ -38,24 +40,24 @@ abbrs = methods_df["abbreviation"].tolist()
|
|
| 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,
|
| 42 |
"url": "https://doi.org/10.1137/17M114402X",
|
| 43 |
-
"summary": "EKI variant with Tikhonov regularization
|
| 44 |
},
|
| 45 |
"ETKI": {
|
| 46 |
-
"citation": "
|
| 47 |
-
"url": "https://
|
| 48 |
-
"summary": "Transform
|
| 49 |
},
|
| 50 |
"IEKF": {
|
| 51 |
-
"citation": "
|
| 52 |
-
"url": "https://
|
| 53 |
-
"summary": "
|
| 54 |
},
|
| 55 |
"UKI": {
|
| 56 |
-
"citation": "
|
| 57 |
-
"url": "https://
|
| 58 |
-
"summary": "
|
| 59 |
},
|
| 60 |
"ABC": {
|
| 61 |
"citation": "Approximate Bayesian Calibration",
|
|
@@ -63,10 +65,15 @@ method_meta = {
|
|
| 63 |
"summary": "Sample without exact likelihoods until error falls below a target convergence.",
|
| 64 |
},
|
| 65 |
"HM": {
|
| 66 |
-
"citation": "
|
| 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,7 +109,7 @@ st.markdown("**Benchmark Slice**")
|
|
| 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 = ["
|
| 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,7 +118,7 @@ best_idx = best_table_view.groupby("benchmark")["metric"].idxmin()
|
|
| 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,
|
| 115 |
|
| 116 |
if sel == "HM":
|
| 117 |
st.markdown("### Failure Analysis")
|
|
@@ -145,7 +152,7 @@ st.markdown("**Best by Benchmark**")
|
|
| 145 |
st.dataframe(
|
| 146 |
best_by_benchmark,
|
| 147 |
hide_index=True,
|
| 148 |
-
|
| 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,13 +176,13 @@ chart = (
|
|
| 169 |
tooltip=["benchmark", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
|
| 170 |
)
|
| 171 |
)
|
| 172 |
-
st.altair_chart(chart,
|
| 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 |
-
|
| 179 |
column_config={
|
| 180 |
"benchmark": st.column_config.TextColumn("Benchmark"),
|
| 181 |
"algorithm_alias": st.column_config.TextColumn("Source Alias"),
|
|
@@ -185,4 +192,4 @@ st.dataframe(
|
|
| 185 |
},
|
| 186 |
)
|
| 187 |
|
| 188 |
-
st.page_link("
|
|
|
|
| 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 |
|
|
|
|
| 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 |
"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 |
}
|
| 78 |
|
| 79 |
# Selection UI (defaults to query param if valid)
|
|
|
|
| 109 |
slice_df = metric_store[metric_store["abbreviation"] == sel].copy()
|
| 110 |
slice_df = slice_df.sort_values(["benchmark", "rmse_target", "ensemble_size"])
|
| 111 |
|
| 112 |
+
target_options = sorted(slice_df["rmse_target"].astype(str).unique().tolist())
|
| 113 |
selected_target = st.radio("RMSE Target Level", options=target_options, horizontal=True)
|
| 114 |
|
| 115 |
best_table_view = slice_df[slice_df["rmse_target"].astype(str) == selected_target]
|
|
|
|
| 118 |
best_ensemble_df = best_table_view.loc[best_idx, ["benchmark", "rmse_target", "ensemble_size", "metric", "failure_rate"]].rename(
|
| 119 |
columns={"metric": "Mean Forward Model Runs", "failure_rate": "Failure Rate (%)", "ensemble_size": "Optimal Ensemble Size", "rmse_target": "RMSE Target"}
|
| 120 |
)
|
| 121 |
+
st.dataframe(best_ensemble_df, hide_index=True, use_container_width=True)
|
| 122 |
|
| 123 |
if sel == "HM":
|
| 124 |
st.markdown("### Failure Analysis")
|
|
|
|
| 152 |
st.dataframe(
|
| 153 |
best_by_benchmark,
|
| 154 |
hide_index=True,
|
| 155 |
+
use_container_width=True,
|
| 156 |
column_config={
|
| 157 |
"benchmark": st.column_config.TextColumn("Benchmark"),
|
| 158 |
"Best Mean Forward Model Runs": st.column_config.NumberColumn("Best Mean Forward Model Runs", format="%.4f"),
|
|
|
|
| 176 |
tooltip=["benchmark", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
|
| 177 |
)
|
| 178 |
)
|
| 179 |
+
st.altair_chart(chart, use_container_width=True)
|
| 180 |
|
| 181 |
st.markdown("**All Averaged Configurations for Method**")
|
| 182 |
st.dataframe(
|
| 183 |
slice_df[["benchmark", "algorithm_alias", "rmse_target", "ensemble_size", "metric"]],
|
| 184 |
hide_index=True,
|
| 185 |
+
use_container_width=True,
|
| 186 |
column_config={
|
| 187 |
"benchmark": st.column_config.TextColumn("Benchmark"),
|
| 188 |
"algorithm_alias": st.column_config.TextColumn("Source Alias"),
|
|
|
|
| 192 |
},
|
| 193 |
)
|
| 194 |
|
| 195 |
+
st.page_link("pages/OptimizationLeaderboard.py", label="β¬
Back to Optimization Leaderboard", icon="β©οΈ")
|
src/pages/OptimizationLeaderboard.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
from data_store import load_metric_store
|
| 8 |
+
from common.leaderboard import render_leaderboard
|
| 9 |
+
except ModuleNotFoundError:
|
| 10 |
+
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
+
from data_store import load_metric_store
|
| 12 |
+
from common.leaderboard import render_leaderboard
|
| 13 |
+
|
| 14 |
+
st.set_page_config(page_title="Optimization Leaderboard", page_icon="π", layout="wide")
|
| 15 |
+
|
| 16 |
+
# Sidebar navigation
|
| 17 |
+
st.sidebar.title("Navigation")
|
| 18 |
+
st.sidebar.page_link("streamlit_app.py", label="Home", icon="π ")
|
| 19 |
+
st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="π")
|
| 20 |
+
st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="π―")
|
| 21 |
+
st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="π")
|
| 22 |
+
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 23 |
+
|
| 24 |
+
st.markdown(r"**Optimization target.** A run succeeds when the normalized weighted distance from the ensemble-mean forward-model output to the true data falls at or below the (selected in the target controls) RMSE target level $\tau$:")
|
| 25 |
+
st.latex(r"\frac{1}{N_y}\bigl(y - G(\bar{\theta})\bigr)^\top \Gamma^{-1}\bigl(y - G(\bar{\theta})\bigr) \leq \tau")
|
| 26 |
+
st.markdown(r"where $y$ is the true observation, $G(\bar{\theta})$ is the forward-model output at the final ensemble mean $\bar{\theta}$, and $\Gamma$ is the observation noise covariance, and $N_y = \dim(y)$.")
|
| 27 |
+
|
| 28 |
+
render_leaderboard(
|
| 29 |
+
load_metric_store(),
|
| 30 |
+
target_col="rmse_target",
|
| 31 |
+
target_label="RMSE Target Level",
|
| 32 |
+
title="Optimization Leaderboard",
|
| 33 |
+
state_prefix="opt",
|
| 34 |
+
raw_page="pages/RawData.py",
|
| 35 |
+
)
|
src/pages/RawData.py
CHANGED
|
@@ -14,6 +14,8 @@ st.set_page_config(page_title="Raw Data", page_icon="π§Ύ", layout="wide")
|
|
| 14 |
# Sidebar navigation
|
| 15 |
st.sidebar.title("Navigation")
|
| 16 |
st.sidebar.page_link("streamlit_app.py", label="Home", icon="π ")
|
|
|
|
|
|
|
| 17 |
st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="π")
|
| 18 |
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 19 |
|
|
@@ -47,7 +49,7 @@ st.dataframe(
|
|
| 47 |
]
|
| 48 |
],
|
| 49 |
hide_index=True,
|
| 50 |
-
|
| 51 |
column_config={
|
| 52 |
"benchmark": st.column_config.TextColumn("Benchmark"),
|
| 53 |
"abbreviation": st.column_config.TextColumn("Abbrev."),
|
|
@@ -69,4 +71,4 @@ st.download_button(
|
|
| 69 |
mime="text/csv",
|
| 70 |
)
|
| 71 |
|
| 72 |
-
st.page_link("
|
|
|
|
| 14 |
# Sidebar navigation
|
| 15 |
st.sidebar.title("Navigation")
|
| 16 |
st.sidebar.page_link("streamlit_app.py", label="Home", icon="π ")
|
| 17 |
+
st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="π")
|
| 18 |
+
st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="π―")
|
| 19 |
st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="π")
|
| 20 |
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 21 |
|
|
|
|
| 49 |
]
|
| 50 |
],
|
| 51 |
hide_index=True,
|
| 52 |
+
use_container_width=True,
|
| 53 |
column_config={
|
| 54 |
"benchmark": st.column_config.TextColumn("Benchmark"),
|
| 55 |
"abbreviation": st.column_config.TextColumn("Abbrev."),
|
|
|
|
| 71 |
mime="text/csv",
|
| 72 |
)
|
| 73 |
|
| 74 |
+
st.page_link("pages/OptimizationLeaderboard.py", label="β¬
Back to Optimization Leaderboard", icon="β©οΈ")
|
src/pages/UQLeaderboard.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS
|
| 8 |
+
from common.leaderboard import render_leaderboard
|
| 9 |
+
except ModuleNotFoundError:
|
| 10 |
+
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
| 11 |
+
from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS
|
| 12 |
+
from common.leaderboard import render_leaderboard
|
| 13 |
+
|
| 14 |
+
st.set_page_config(page_title="UQ Leaderboard", page_icon="π―", layout="wide")
|
| 15 |
+
|
| 16 |
+
# Sidebar navigation
|
| 17 |
+
st.sidebar.title("Navigation")
|
| 18 |
+
st.sidebar.page_link("streamlit_app.py", label="Home", icon="π ")
|
| 19 |
+
st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="π")
|
| 20 |
+
st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="π―")
|
| 21 |
+
st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="π")
|
| 22 |
+
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 23 |
+
|
| 24 |
+
st.markdown(r"**UQ target.** Marginal coverage, $\widetilde{S}(q)$ measures the fraction of output dimensions where the true observation $y$ falls below the $q$-quantile of the posterior predictive distribution. This effectively treats each output dimension as an independent trial, so we also whitened the outputs and data by $\Gamma$, Though not notated, we also truncate to retain 99% of the variance:")
|
| 25 |
+
st.latex(r"\widetilde{S}(q) = \frac{1}{N_y}\sum_{n=1}^{N_y} \mathbb{I}\!\left(\mathrm{cdf}_{\,\Gamma^{-1/2}\mathrm{post}}\!\left(\Gamma^{-1/2}y\right) \leq q\right)")
|
| 26 |
+
st.markdown(r"A run succeeds when **all three of the target quantiles** $q \in \{0.15, 0.50, 0.85\}$ simultaneously satisfy")
|
| 27 |
+
st.latex(r"\left|\widetilde{S}(q) - q\right| \leq \alpha_c(q), \qquad \alpha_c(q) = c\sqrt{\frac{q(1-q)}{N_y}}")
|
| 28 |
+
st.markdown(r"The boundary $\alpha_c(q)$ adapts the race-goal tolerance for different quantiles, as some are easier to estimate than others with finite samples. while $c$ is the target-scaling level that can be selected in the controls below, and $N_y = \dim(\Gamma^{-1/2}y)$ (and after truncation).")
|
| 29 |
+
|
| 30 |
+
render_leaderboard(
|
| 31 |
+
load_uq_store(),
|
| 32 |
+
target_col="uq_target",
|
| 33 |
+
target_label="UQ Target (coverage tolerance scaling)",
|
| 34 |
+
title="Uncertainty Quantification Leaderboard",
|
| 35 |
+
state_prefix="uq",
|
| 36 |
+
show_failure_panel=True,
|
| 37 |
+
show_scoring_modes=False,
|
| 38 |
+
canonical_target_levels=UQ_TARGET_LEVELS,
|
| 39 |
+
budget_store=load_uq_budget_store(),
|
| 40 |
+
)
|
src/streamlit_app.py
CHANGED
|
@@ -1,324 +1,103 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import importlib
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
| 5 |
|
| 6 |
-
st.set_page_config(layout="wide")
|
| 7 |
-
|
| 8 |
-
if __package__:
|
| 9 |
-
data_store = importlib.import_module(f"{__package__}.data_store")
|
| 10 |
-
else:
|
| 11 |
-
data_store = importlib.import_module("data_store")
|
| 12 |
-
|
| 13 |
-
load_metric_store = data_store.load_metric_store
|
| 14 |
-
|
| 15 |
-
st.title("Calibration Benchmark")
|
| 16 |
|
| 17 |
# Sidebar navigation
|
| 18 |
st.sidebar.title("Navigation")
|
| 19 |
st.sidebar.page_link("streamlit_app.py", label="Home", icon="π ")
|
|
|
|
|
|
|
| 20 |
st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="π")
|
| 21 |
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
target_options = ["All targets"] + sorted(metric_store["rmse_target"].astype(str).unique().tolist())
|
| 42 |
-
scoring_options = [
|
| 43 |
-
"Mean Forward Model Runs",
|
| 44 |
-
"Minimum Forward Model Runs",
|
| 45 |
-
"Smallest Optimal Ensemble Size",
|
| 46 |
-
"Custom Blend",
|
| 47 |
-
]
|
| 48 |
-
|
| 49 |
-
current_target = st.session_state.get("selected_target", "All targets")
|
| 50 |
-
if current_target not in target_options:
|
| 51 |
-
current_target = target_options[0]
|
| 52 |
-
|
| 53 |
-
current_scoring_mode = st.session_state.get("scoring_mode", "Mean Forward Model Runs")
|
| 54 |
-
if current_scoring_mode not in scoring_options:
|
| 55 |
-
current_scoring_mode = scoring_options[0]
|
| 56 |
-
|
| 57 |
-
current_fwdruns_weight_percent = int(st.session_state.get("fwdruns_weight_percent", 80))
|
| 58 |
-
current_fwdruns_weight_percent = max(0, min(100, current_fwdruns_weight_percent))
|
| 59 |
-
|
| 60 |
-
selected_target = current_target
|
| 61 |
-
scoring_mode = current_scoring_mode
|
| 62 |
-
fwdruns_weight = current_fwdruns_weight_percent / 100.0
|
| 63 |
-
ensemble_weight = 1.0 - fwdruns_weight
|
| 64 |
-
|
| 65 |
-
def build_scored_table(input_df, add_rank=True):
|
| 66 |
-
ranking_source = input_df if selected_target == "All targets" else input_df[input_df["rmse_target_str"] == selected_target]
|
| 67 |
-
if ranking_source.empty:
|
| 68 |
-
return ranking_source
|
| 69 |
-
|
| 70 |
-
scored_df = ranking_source.groupby(["algorithm_type", "abbreviation", "Method", "family"], as_index=False).agg(
|
| 71 |
-
**{"Mean Forward Model Runs": ("metric", "mean")},
|
| 72 |
-
**{"Minimum Forward Model Runs": ("metric", "min")},
|
| 73 |
-
**{"Targets Used": ("rmse_target_str", "nunique")},
|
| 74 |
-
**{"Ensemble Sizes Used": ("ensemble_size", "nunique")},
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
best_per_target = (
|
| 78 |
-
ranking_source.sort_values(["algorithm_type", "rmse_target_str", "metric", "ensemble_size"])
|
| 79 |
-
.groupby(["algorithm_type", "abbreviation", "Method", "family", "rmse_target_str"], as_index=False)
|
| 80 |
-
.first()[
|
| 81 |
-
[
|
| 82 |
-
"algorithm_type",
|
| 83 |
-
"abbreviation",
|
| 84 |
-
"Method",
|
| 85 |
-
"family",
|
| 86 |
-
"rmse_target_str",
|
| 87 |
-
"ensemble_size",
|
| 88 |
-
]
|
| 89 |
-
]
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
optimal_ensemble = best_per_target.groupby(
|
| 93 |
-
["algorithm_type", "abbreviation", "Method", "family"], as_index=False
|
| 94 |
-
).agg(**{"Optimal Ensemble Size": ("ensemble_size", "mean")})
|
| 95 |
-
|
| 96 |
-
scored_df = scored_df.merge(
|
| 97 |
-
optimal_ensemble,
|
| 98 |
-
on=["algorithm_type", "abbreviation", "Method", "family"],
|
| 99 |
-
how="left",
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
scored_df["Optimal Ensemble Size"] = scored_df["Optimal Ensemble Size"].round(2)
|
| 103 |
-
scored_df["Mean Forward Model Runs"] = scored_df["Mean Forward Model Runs"].round(4)
|
| 104 |
-
scored_df["Minimum Forward Model Runs"] = scored_df["Minimum Forward Model Runs"].round(4)
|
| 105 |
-
|
| 106 |
-
mean_runs_min = scored_df["Mean Forward Model Runs"].min()
|
| 107 |
-
mean_runs_max = scored_df["Mean Forward Model Runs"].max()
|
| 108 |
-
if mean_runs_max > mean_runs_min:
|
| 109 |
-
scored_df["mean_runs_score"] = 100.0 * (mean_runs_max - scored_df["Mean Forward Model Runs"]) / (mean_runs_max - mean_runs_min)
|
| 110 |
-
else:
|
| 111 |
-
scored_df["mean_runs_score"] = 100.0
|
| 112 |
-
|
| 113 |
-
minimum_runs_min = scored_df["Minimum Forward Model Runs"].min()
|
| 114 |
-
minimum_runs_max = scored_df["Minimum Forward Model Runs"].max()
|
| 115 |
-
if minimum_runs_max > minimum_runs_min:
|
| 116 |
-
scored_df["minimum_runs_score"] = 100.0 * (minimum_runs_max - scored_df["Minimum Forward Model Runs"]) / (minimum_runs_max - minimum_runs_min)
|
| 117 |
-
else:
|
| 118 |
-
scored_df["minimum_runs_score"] = 100.0
|
| 119 |
-
|
| 120 |
-
ens_min = scored_df["Optimal Ensemble Size"].min()
|
| 121 |
-
ens_max = scored_df["Optimal Ensemble Size"].max()
|
| 122 |
-
if ens_max > ens_min:
|
| 123 |
-
scored_df["ensemble_score"] = 100.0 * (ens_max - scored_df["Optimal Ensemble Size"]) / (ens_max - ens_min)
|
| 124 |
-
else:
|
| 125 |
-
scored_df["ensemble_score"] = 100.0
|
| 126 |
-
|
| 127 |
-
if scoring_mode == "Mean Forward Model Runs":
|
| 128 |
-
scored_df["Score"] = scored_df["mean_runs_score"]
|
| 129 |
-
sort_columns = ["Mean Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
|
| 130 |
-
ascending = [True, True, True]
|
| 131 |
-
elif scoring_mode == "Minimum Forward Model Runs":
|
| 132 |
-
scored_df["Score"] = scored_df["minimum_runs_score"]
|
| 133 |
-
sort_columns = ["Minimum Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
|
| 134 |
-
ascending = [True, True, True]
|
| 135 |
-
elif scoring_mode == "Smallest Optimal Ensemble Size":
|
| 136 |
-
scored_df["Score"] = scored_df["ensemble_score"]
|
| 137 |
-
sort_columns = ["Optimal Ensemble Size", "Mean Forward Model Runs", "abbreviation"]
|
| 138 |
-
ascending = [True, True, True]
|
| 139 |
-
else:
|
| 140 |
-
scored_df["Score"] = (
|
| 141 |
-
fwdruns_weight * scored_df["mean_runs_score"]
|
| 142 |
-
+ ensemble_weight * scored_df["ensemble_score"]
|
| 143 |
-
)
|
| 144 |
-
sort_columns = ["Score", "Mean Forward Model Runs", "Optimal Ensemble Size", "abbreviation"]
|
| 145 |
-
ascending = [False, True, True, True]
|
| 146 |
-
|
| 147 |
-
scored_df = scored_df.sort_values(sort_columns, ascending=ascending).reset_index(drop=True)
|
| 148 |
-
|
| 149 |
-
if add_rank:
|
| 150 |
-
scored_df["Rank"] = scored_df.index + 1
|
| 151 |
-
scored_df["Placement"] = scored_df["Rank"].apply(
|
| 152 |
-
lambda rank: f"{ {1: 'π₯', 2: 'π₯', 3: 'π₯'}.get(rank, '')} #{rank}".strip()
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
return scored_df
|
| 156 |
-
|
| 157 |
-
if selected_benchmark == "All":
|
| 158 |
-
benchmark_scores = []
|
| 159 |
-
for benchmark_name in benchmark_values:
|
| 160 |
-
benchmark_df = metric_store[metric_store["benchmark"] == benchmark_name].copy()
|
| 161 |
-
benchmark_df["rmse_target_str"] = benchmark_df["rmse_target"].astype(str)
|
| 162 |
-
scored = build_scored_table(benchmark_df, add_rank=False)
|
| 163 |
-
if scored.empty:
|
| 164 |
-
continue
|
| 165 |
-
scored["benchmark"] = benchmark_name
|
| 166 |
-
benchmark_scores.append(scored)
|
| 167 |
-
|
| 168 |
-
if benchmark_scores:
|
| 169 |
-
combined_scores = benchmark_scores[0].copy() if len(benchmark_scores) == 1 else pd.concat(benchmark_scores, ignore_index=True)
|
| 170 |
-
leaderboard_df = combined_scores.groupby(["algorithm_type", "abbreviation", "Method", "family"], as_index=False).agg(
|
| 171 |
-
Score=("Score", "mean"),
|
| 172 |
-
**{"Mean Forward Model Runs": ("Mean Forward Model Runs", "mean")},
|
| 173 |
-
**{"Minimum Forward Model Runs": ("Minimum Forward Model Runs", "mean")},
|
| 174 |
-
**{"Optimal Ensemble Size": ("Optimal Ensemble Size", "mean")},
|
| 175 |
-
**{"Targets Used": ("Targets Used", "mean")},
|
| 176 |
-
**{"Ensemble Sizes Used": ("Ensemble Sizes Used", "mean")},
|
| 177 |
-
**{"Benchmarks Used": ("benchmark", "nunique")},
|
| 178 |
-
)
|
| 179 |
-
|
| 180 |
-
leaderboard_df["Mean Forward Model Runs"] = leaderboard_df["Mean Forward Model Runs"].round(4)
|
| 181 |
-
leaderboard_df["Minimum Forward Model Runs"] = leaderboard_df["Minimum Forward Model Runs"].round(4)
|
| 182 |
-
leaderboard_df["Optimal Ensemble Size"] = leaderboard_df["Optimal Ensemble Size"].round(2)
|
| 183 |
-
leaderboard_df["Targets Used"] = leaderboard_df["Targets Used"].round().astype(int)
|
| 184 |
-
leaderboard_df["Ensemble Sizes Used"] = leaderboard_df["Ensemble Sizes Used"].round().astype(int)
|
| 185 |
-
leaderboard_df = leaderboard_df.sort_values(["Score", "Mean Forward Model Runs", "abbreviation"], ascending=[False, True, True]).reset_index(drop=True)
|
| 186 |
-
leaderboard_df["Rank"] = leaderboard_df.index + 1
|
| 187 |
-
leaderboard_df["Placement"] = leaderboard_df["Rank"].apply(
|
| 188 |
-
lambda rank: f"{ {1: 'π₯', 2: 'π₯', 3: 'π₯'}.get(rank, '')} #{rank}".strip()
|
| 189 |
-
)
|
| 190 |
-
else:
|
| 191 |
-
leaderboard_df = pd.DataFrame(
|
| 192 |
-
columns=[
|
| 193 |
-
"Placement",
|
| 194 |
-
"abbreviation",
|
| 195 |
-
"Method",
|
| 196 |
-
"family",
|
| 197 |
-
"Mean Forward Model Runs",
|
| 198 |
-
"Minimum Forward Model Runs",
|
| 199 |
-
"Score",
|
| 200 |
-
"Optimal Ensemble Size",
|
| 201 |
-
"Targets Used",
|
| 202 |
-
"Ensemble Sizes Used",
|
| 203 |
-
"Benchmarks Used",
|
| 204 |
-
]
|
| 205 |
-
)
|
| 206 |
-
else:
|
| 207 |
-
leaderboard_df = build_scored_table(filtered, add_rank=True)
|
| 208 |
-
leaderboard_df["Benchmarks Used"] = 1
|
| 209 |
-
|
| 210 |
-
if scoring_mode == "Mean Forward Model Runs":
|
| 211 |
-
score_basis = "normalized mean of best forward model runs over selected target levels (lower is better)"
|
| 212 |
-
elif scoring_mode == "Minimum Forward Model Runs":
|
| 213 |
-
score_basis = "normalized minimum of forward model runs over selected targets and ensemble sizes (lower is better)"
|
| 214 |
-
elif scoring_mode == "Smallest Optimal Ensemble Size":
|
| 215 |
-
score_basis = "normalized mean optimal ensemble size over selected target levels (lower is better)"
|
| 216 |
-
else:
|
| 217 |
-
score_basis = (
|
| 218 |
-
"weighted blend of normalized forward-model-runs score and normalized ensemble-size score "
|
| 219 |
-
f"(forward-runs weight {fwdruns_weight:.0%}, ensemble-size weight {ensemble_weight:.0%})"
|
| 220 |
-
)
|
| 221 |
-
|
| 222 |
-
if selected_benchmark == "All":
|
| 223 |
-
score_basis = f"{score_basis}; in All mode, each method's final score is the mean of its per-benchmark scores"
|
| 224 |
-
|
| 225 |
-
if leaderboard_df.empty:
|
| 226 |
-
st.warning("No rows available for the current benchmark/target selection.")
|
| 227 |
-
st.stop()
|
| 228 |
-
|
| 229 |
-
if selected_benchmark == "All":
|
| 230 |
-
table_column_order = [
|
| 231 |
-
"Placement",
|
| 232 |
-
"abbreviation",
|
| 233 |
-
"Method",
|
| 234 |
-
"family",
|
| 235 |
-
"Score",
|
| 236 |
-
"Targets Used",
|
| 237 |
-
"Ensemble Sizes Used",
|
| 238 |
-
"Benchmarks Used",
|
| 239 |
-
]
|
| 240 |
-
else:
|
| 241 |
-
table_column_order = [
|
| 242 |
-
"Placement",
|
| 243 |
-
"abbreviation",
|
| 244 |
-
"Method",
|
| 245 |
-
"family",
|
| 246 |
-
"Score",
|
| 247 |
-
"Mean Forward Model Runs",
|
| 248 |
-
"Minimum Forward Model Runs",
|
| 249 |
-
"Optimal Ensemble Size",
|
| 250 |
-
"Targets Used",
|
| 251 |
-
"Ensemble Sizes Used",
|
| 252 |
-
"Benchmarks Used",
|
| 253 |
-
]
|
| 254 |
-
|
| 255 |
-
st.subheader(f"Ranked Leaderboard β {selected_benchmark}")
|
| 256 |
-
st.dataframe(
|
| 257 |
-
leaderboard_df,
|
| 258 |
-
hide_index=True,
|
| 259 |
-
width="stretch",
|
| 260 |
-
column_config={
|
| 261 |
-
"Placement": st.column_config.TextColumn("Placement"),
|
| 262 |
-
"family": st.column_config.TextColumn("Family"),
|
| 263 |
-
"Method": st.column_config.TextColumn("Method"),
|
| 264 |
-
"abbreviation": st.column_config.TextColumn("Abbrev."),
|
| 265 |
-
"Mean Forward Model Runs": st.column_config.NumberColumn("Mean Forward Model Runs", format="%.4f"),
|
| 266 |
-
"Minimum Forward Model Runs": st.column_config.NumberColumn("Minimum Forward Model Runs", format="%.4f"),
|
| 267 |
-
"Score": st.column_config.ProgressColumn("Score (0-100)", min_value=0.0, max_value=100.0, format="%.1f"),
|
| 268 |
-
"Optimal Ensemble Size": st.column_config.NumberColumn("Mean Optimal Ensemble Size", format="%.2f"),
|
| 269 |
-
"Targets Used": st.column_config.NumberColumn("Targets Used", format="%d"),
|
| 270 |
-
"Ensemble Sizes Used": st.column_config.NumberColumn("Ensemble Sizes Used", format="%d"),
|
| 271 |
-
"Benchmarks Used": st.column_config.NumberColumn("Benchmarks Used", format="%d"),
|
| 272 |
-
},
|
| 273 |
-
column_order=table_column_order,
|
| 274 |
)
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
st.radio(
|
| 285 |
-
"Scoring Method",
|
| 286 |
-
options=scoring_options,
|
| 287 |
-
horizontal=True,
|
| 288 |
-
key="scoring_mode",
|
| 289 |
-
)
|
| 290 |
-
|
| 291 |
-
if st.session_state.get("scoring_mode", "Mean Forward Model Runs") == "Custom Blend":
|
| 292 |
-
st.slider(
|
| 293 |
-
"Blend Weight: Forward Runs vs Ensemble Size",
|
| 294 |
-
min_value=0,
|
| 295 |
-
max_value=100,
|
| 296 |
-
step=5,
|
| 297 |
-
key="fwdruns_weight_percent",
|
| 298 |
-
help="Higher forward-runs weight prioritizes fewer model evaluations; higher ensemble-size weight prioritizes smaller ensembles.",
|
| 299 |
-
)
|
| 300 |
-
|
| 301 |
-
st.info(
|
| 302 |
-
f"Score is a normalized 0β100 ranking based on **{score_basis}**. "
|
| 303 |
-
"For Mean/Minimum forward-runs scoring, values are computed from all selected metric target levels and all ensemble sizes "
|
| 304 |
-
"after averaging over random seeds."
|
| 305 |
)
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
st.set_page_config(page_title="Calibration Benchmark", page_icon="π ", layout="wide")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
# Sidebar navigation
|
| 7 |
st.sidebar.title("Navigation")
|
| 8 |
st.sidebar.page_link("streamlit_app.py", label="Home", icon="π ")
|
| 9 |
+
st.sidebar.page_link("pages/OptimizationLeaderboard.py", label="Optimization Leaderboard", icon="π")
|
| 10 |
+
st.sidebar.page_link("pages/UQLeaderboard.py", label="UQ Leaderboard", icon="π―")
|
| 11 |
st.sidebar.page_link("pages/MethodDetails.py", label="Methods", icon="π")
|
| 12 |
st.sidebar.page_link("pages/RawData.py", label="Get Data", icon="π§Ύ")
|
| 13 |
|
| 14 |
+
st.title("Calibration Benchmark")
|
| 15 |
+
st.markdown(
|
| 16 |
+
"A benchmark comparing parameter-calibration methods on chaotic dynamical systems. "
|
| 17 |
+
"Methods are ranked by **forward-model run efficiency** β how many forward-model "
|
| 18 |
+
"evaluations are needed, on average across random seeds, to reach a target accuracy."
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
st.divider()
|
| 22 |
+
|
| 23 |
+
col1, col2 = st.columns(2, gap="large")
|
| 24 |
+
|
| 25 |
+
with col1:
|
| 26 |
+
st.subheader("π Optimization Leaderboard")
|
| 27 |
+
st.markdown(
|
| 28 |
+
"Ranks methods by mean forward-model runs to reach an **RMSE target** on "
|
| 29 |
+
"Lorenz-63 and Lorenz-96 benchmarks. "
|
| 30 |
+
"Lower is better; failed runs are tracked separately as a failure rate."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
)
|
| 32 |
+
st.page_link("pages/OptimizationLeaderboard.py", label="Go to Optimization Leaderboard β")
|
| 33 |
+
|
| 34 |
+
with col2:
|
| 35 |
+
st.subheader("π― UQ Leaderboard")
|
| 36 |
+
st.markdown(
|
| 37 |
+
"Ranks methods by mean forward-model runs to reach an **uncertainty quantification "
|
| 38 |
+
"target**. Same metric and benchmarks as the Optimization Leaderboard, evaluated "
|
| 39 |
+
"at a UQ-specific convergence criterion."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
)
|
| 41 |
+
st.page_link("pages/UQLeaderboard.py", label="Go to UQ Leaderboard β")
|
| 42 |
+
|
| 43 |
+
st.divider()
|
| 44 |
+
|
| 45 |
+
st.subheader("Benchmarks")
|
| 46 |
+
st.markdown(
|
| 47 |
+
"Results are reported on four benchmark configurations of the [Lorenz system]"
|
| 48 |
+
"(https://en.wikipedia.org/wiki/Lorenz_system), a standard testbed for "
|
| 49 |
+
"data-assimilation and calibration algorithms:"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
_media = Path(__file__).parent / "media"
|
| 53 |
+
|
| 54 |
+
st.markdown("- **L63** β Lorenz-63 β 3-variable chaotic attractor; learn 2 parameters, strongly nonlinear.")
|
| 55 |
+
st.image(
|
| 56 |
+
str(_media / "posterior_ribbons_20_13_k5.png"),
|
| 57 |
+
caption="Example prior-posterior & truth. L: Difference to true parameter. R: data-sample/output predictived distribution (state-mean [1:3], state-covariance (diag [4:6] and off-diag [7:9]))",
|
| 58 |
+
width=800,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
st.markdown("- **L96** β Lorenz-96 (40-variable); learn 1-parameter constant forcing.")
|
| 63 |
+
st.image(
|
| 64 |
+
str(_media / "posterior_ribbons_const-force_12_1_k3.png"),
|
| 65 |
+
caption="Example prior-posterior & truth. L: Difference to true parameter. C: parameter-induced forcing of the L96 system. R: data-sample/output predictived distribution ([1:40] state-mean [41:80] state-std)",
|
| 66 |
+
width=1200,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
st.markdown("- **L96_SPATIAL_FORCING** β Lorenz-96 (40-variable) with spatially-varying forcing; learn 40 parameters; moderately correlated prior.")
|
| 70 |
+
st.image(
|
| 71 |
+
str(_media / "posterior_ribbons_vec-force_65_1_k3.png"),
|
| 72 |
+
caption="Example prior-posterior & truth. L: Difference to true parameters. C: parameter-induced forcing of the L96 system. R: data-sample/output predictived distribution ([1:40] state-mean [41:80] state-std)",
|
| 73 |
+
width=1200,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
st.markdown("- **L96_NN_FORCING** β Lorenz-96 (100-variable) with a neural-network forcing; Learn 61 parameters (weights and biases of the network). Reasonable prior given.")
|
| 78 |
+
st.image(
|
| 79 |
+
str(_media / "posterior_ribbons_flux-force_80_1_k3.png"),
|
| 80 |
+
caption="Example prior-posterior & truth. L: Difference to true parameters (weights). C: parameter-induced forcing of the L96 system. R: data-sample/output predictived distribution ([1:100] state-mean [101:200] state-std)",
|
| 81 |
+
width=1200,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
st.subheader("Method families")
|
| 85 |
+
st.markdown(
|
| 86 |
+
"Methods are grouped into three families. See the **π Methods** page for "
|
| 87 |
+
"citations and per-method performance charts."
|
| 88 |
+
)
|
| 89 |
+
families = {
|
| 90 |
+
"Kalman": "Ensemble Kalman variants (TEKI, ETKI, IEKF, UKI) β update an ensemble of parameter guesses via a linearized observation operator.",
|
| 91 |
+
"Bayesian": "Sampling-based approaches (ABC, HM) β explore parameter space without requiring gradient information.",
|
| 92 |
+
"Calibrate-then-emulate": "Two-stage pipelines (CES-EKI-DMC) β use an initial calibration phase to build a cheap emulator, then sample the posterior via MCMC.",
|
| 93 |
+
}
|
| 94 |
+
for family, desc in families.items():
|
| 95 |
+
st.markdown(f"- **{family}** β {desc}")
|
| 96 |
+
|
| 97 |
+
st.subheader("Key metric")
|
| 98 |
+
st.markdown(
|
| 99 |
+
"The reported metric is the **mean number of forward-model evaluations** required "
|
| 100 |
+
"to reach the target, averaged over random seeds. "
|
| 101 |
+
"A value of **β1** indicates a failed run (did not reach the target); "
|
| 102 |
+
"the **failure rate** shows the fraction of seeds that failed."
|
| 103 |
+
)
|