CLAUDE.md CHANGED
@@ -47,8 +47,10 @@ calibration_benchmark/
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/
@@ -122,6 +124,21 @@ 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 |
@@ -144,8 +161,10 @@ All three stores are `@st.cache_data` — clear Streamlit cache to pick up new d
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`
@@ -183,3 +202,11 @@ the `budget_store` parameter of `render_leaderboard()` in `src/common/leaderboar
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.
 
 
 
 
 
 
 
 
 
47
  │ ├── streamlit_app.py Entrypoint + Home/"Optimization Leaderboard"
48
  │ ├── data_store.py load_metric_store() / load_uq_store() / load_uq_budget_store()
49
  │ │ → @st.cache_data DataFrames; DATASET_FILES, UQ_BUDGET_FILES
50
+ │ │ → BENCHMARK_DIMS — physical (param, state, output) dims per benchmark
51
  │ ├── common/
52
  │ │ ├── leaderboard.py render_leaderboard() — shared scored-table + chart pipeline
53
+ │ │ │ _render_suitability_table() — method × benchmark suitability grid
54
  │ │ │ optional budget_store param adds budget/iters chart section
55
  │ │ └── method_registry.py KNOWN_METHODS registry + name canonicalization
56
  │ └── pages/
 
124
 
125
  All three stores are `@st.cache_data` — clear Streamlit cache to pick up new data files.
126
 
127
+ ### Benchmark dimensions (`BENCHMARK_DIMS`)
128
+
129
+ Physical dimensions of each benchmark exported from `src/data_store.py` as
130
+ `BENCHMARK_DIMS: dict[str, tuple[int, int, int]]` — `(param_dim, state_dim, output_dim)`:
131
+
132
+ | Benchmark | param | state | output |
133
+ |---|---|---|---|
134
+ | `L63` | 2 | 3 | 9 |
135
+ | `L96` | 1 | 40 | 80 |
136
+ | `L96_NN_FORCING` | 61 | 100 | 200 |
137
+ | `L96_SPATIAL_FORCING` | 40 | 40 | 80 |
138
+
139
+ Passed as `benchmark_dims=BENCHMARK_DIMS` to `render_leaderboard()` for column-header
140
+ annotations in the suitability table. Update if a new benchmark is added.
141
+
142
  ## Method registry (`src/common/method_registry.py`)
143
 
144
  | Canonical | Abbreviation | Family | Aliases |
 
161
  3. Place result NetCDF(s) in `data/` and add paths to `DATASET_FILES` in `src/data_store.py`.
162
 
163
  ### Add an optimization benchmark dataset
164
+ 1. Add a `"BENCHMARK_NAME": [list_of_nc_paths]` entry to `DATASET_FILES` in `src/data_store.py`.
165
+ Paths are relative to the `data/` directory.
166
+ 2. Add a `"BENCHMARK_NAME": (param_dim, state_dim, output_dim)` entry to `BENCHMARK_DIMS`
167
+ in `src/data_store.py` for the suitability table column header.
168
 
169
  ### Add UQ data
170
  Add an `(algorithm_type, nc_path)` tuple to the appropriate benchmark key in `UQ_BUDGET_FILES`
 
202
  never reached (no `metric == -1` sentinel); `failure_rate` is computed differently.
203
  - **`render_leaderboard` budget_store param** — must be passed explicitly from
204
  `UQLeaderboard.py`; `leaderboard.py` is not aware of UQ-specific data paths.
205
+ - **`render_leaderboard` requires `default_target`** — callers must pass the fixed target level
206
+ shown on first load (`1.1` for optimization, `1.5` for UQ). There is no "All targets" option.
207
+ - **Suitability table is fixed at `default_target`** — `_render_suitability_table()` always
208
+ evaluates suitability at the `default_target` passed to `render_leaderboard()`, independent
209
+ of what the user selects in the target radio control below it.
210
+ - **Suitability criteria** — a method is green for a benchmark if there exists an ensemble size
211
+ with `failure_rate < 20%` and `mean_budget ≤ 3× global_best` at `default_target`. Gray means
212
+ no data for that benchmark; the method may be suitable on other benchmarks.
src/common/leaderboard.py CHANGED
@@ -14,6 +14,7 @@ Usage::
14
  target_label="RMSE Target Level",
15
  title="Optimization Leaderboard",
16
  state_prefix="opt",
 
17
  raw_page="pages/RawData.py",
18
  )
19
  """
@@ -31,6 +32,98 @@ _METHOD_PALETTE = [
31
  "#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
32
  ]
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  def render_leaderboard(
36
  metric_store: pd.DataFrame,
@@ -39,11 +132,13 @@ def render_leaderboard(
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
 
@@ -66,12 +161,17 @@ def render_leaderboard(
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
@@ -79,10 +179,11 @@ def render_leaderboard(
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
 
@@ -90,24 +191,25 @@ def render_leaderboard(
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
 
@@ -120,14 +222,19 @@ def render_leaderboard(
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:
@@ -136,22 +243,21 @@ def render_leaderboard(
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",
@@ -159,11 +265,7 @@ def render_leaderboard(
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
 
@@ -182,7 +284,6 @@ def render_leaderboard(
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
 
@@ -216,10 +317,10 @@ def render_leaderboard(
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()
@@ -278,85 +379,20 @@ def render_leaderboard(
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):
@@ -398,6 +434,19 @@ def render_leaderboard(
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. "
@@ -406,34 +455,6 @@ def render_leaderboard(
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,
@@ -449,91 +470,82 @@ def render_leaderboard(
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 = (
@@ -603,12 +615,8 @@ def render_leaderboard(
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(
@@ -616,12 +624,7 @@ def render_leaderboard(
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)
 
14
  target_label="RMSE Target Level",
15
  title="Optimization Leaderboard",
16
  state_prefix="opt",
17
+ default_target=1.1,
18
  raw_page="pages/RawData.py",
19
  )
20
  """
 
32
  "#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
33
  ]
34
 
35
+ _SUITABLE = "#009E73" # Okabe-Ito teal-green (colorblind-safe)
36
+ _UNSUITABLE = "#C0392B" # dark red
37
+ _UNTESTED = "#BDBDBD" # gray
38
+
39
+
40
+ def _render_suitability_table(
41
+ store: pd.DataFrame,
42
+ target_col: str,
43
+ suitability_target: float,
44
+ benchmark_dims: dict[str, tuple[int, int, int]] | None = None,
45
+ failure_threshold: float = 20.0,
46
+ ratio_threshold: float = 3.0,
47
+ ) -> None:
48
+ """Render the method × benchmark suitability grid above the leaderboard controls."""
49
+ benchmarks = sorted(
50
+ store["benchmark"].unique().tolist(),
51
+ key=lambda bm: benchmark_dims[bm][0] if (benchmark_dims and bm in benchmark_dims) else bm,
52
+ )
53
+ methods = sorted(store["abbreviation"].dropna().unique().tolist())
54
+
55
+ target_str = str(float(suitability_target))
56
+ target_str_col = f"{target_col}_str"
57
+ target_df = store.copy()
58
+ target_df[target_str_col] = target_df[target_col].astype(str)
59
+ target_df = target_df[target_df[target_str_col] == target_str]
60
+
61
+ # Global best per benchmark: min metric across all methods with failure_rate < threshold
62
+ global_best: dict[str, float | None] = {}
63
+ for bm in benchmarks:
64
+ bm_df = target_df[target_df["benchmark"] == bm]
65
+ qualifying = bm_df[bm_df["failure_rate"] < failure_threshold].dropna(subset=["metric"])
66
+ global_best[bm] = float(qualifying["metric"].min()) if not qualifying.empty else None
67
+
68
+ cell_text: dict[str, dict[str, str]] = {}
69
+ cell_color: dict[str, dict[str, str]] = {}
70
+
71
+ for method in methods:
72
+ cell_text[method] = {}
73
+ cell_color[method] = {}
74
+ for bm in benchmarks:
75
+ sub = target_df[
76
+ (target_df["abbreviation"] == method) & (target_df["benchmark"] == bm)
77
+ ]
78
+ if sub.empty:
79
+ cell_text[method][bm] = "—"
80
+ cell_color[method][bm] = _UNTESTED
81
+ continue
82
+ qualifying = sub[sub["failure_rate"] < failure_threshold].dropna(subset=["metric"])
83
+ if qualifying.empty:
84
+ cell_text[method][bm] = "failed"
85
+ cell_color[method][bm] = _UNSUITABLE
86
+ continue
87
+ method_best = float(qualifying["metric"].min())
88
+ gb = global_best.get(bm)
89
+ ratio = (method_best / gb) if (gb is not None and gb > 0) else 1.0
90
+ cell_text[method][bm] = f"{ratio:.1f}×"
91
+ cell_color[method][bm] = _SUITABLE if ratio <= ratio_threshold else _UNSUITABLE
92
+
93
+ col_labels: dict[str, str] = {}
94
+ for bm in benchmarks:
95
+ if benchmark_dims and bm in benchmark_dims:
96
+ p, s, o = benchmark_dims[bm]
97
+ col_labels[bm] = f"{bm} (p={p}, s={s}, o={o})"
98
+ else:
99
+ col_labels[bm] = bm
100
+
101
+ display_df = pd.DataFrame(cell_text).T.rename(columns=col_labels)
102
+ color_df = pd.DataFrame(cell_color).T.rename(columns=col_labels)
103
+ display_df.index.name = "Method"
104
+
105
+ def _style(df: pd.DataFrame) -> pd.DataFrame:
106
+ result = pd.DataFrame("", index=df.index, columns=df.columns)
107
+ for row in df.index:
108
+ for col in df.columns:
109
+ bg = color_df.loc[row, col]
110
+ fg = "#212529" if bg == _UNTESTED else "white"
111
+ result.loc[row, col] = (
112
+ f"background-color: {bg}; color: {fg}; "
113
+ "text-align: center; font-weight: bold"
114
+ )
115
+ return result
116
+
117
+ st.subheader("Method Suitability Overview")
118
+ st.caption(
119
+ f"Evaluated at target = {suitability_target}. "
120
+ f"**Green**: at some ensemble size, failure rate < {failure_threshold:.0f}% "
121
+ f"and mean budget ≤ {ratio_threshold:.0f}× the best method (ratio shown). "
122
+ "**Red**: does not meet criteria. "
123
+ "**Gray**: no data for this experiment."
124
+ )
125
+ st.dataframe(display_df.style.apply(_style, axis=None), use_container_width=True)
126
+
127
 
128
  def render_leaderboard(
129
  metric_store: pd.DataFrame,
 
132
  target_label: str,
133
  title: str,
134
  state_prefix: str,
135
+ default_target: float,
136
  raw_page: str | None = None,
137
  show_failure_panel: bool = False,
138
  show_scoring_modes: bool = True,
139
  canonical_target_levels: list[float] | None = None,
140
  budget_store: pd.DataFrame | None = None,
141
+ benchmark_dims: dict[str, tuple[int, int, int]] | None = None,
142
  ) -> None:
143
  """Render a scored leaderboard backed by *metric_store*.
144
 
 
161
  Short string used to namespace ``st.session_state`` keys so multiple
162
  leaderboard pages keep independent control state. Use ``"opt"`` for
163
  the Optimization leaderboard and ``"uq"`` for the UQ leaderboard.
164
+ default_target:
165
+ Target level shown on first load (e.g. ``1.1`` for optimization,
166
+ ``1.5`` for UQ).
167
  raw_page:
168
  Optional Streamlit page path for an "Open Raw Data" link shown at the
169
  bottom. Pass ``None`` to suppress the link.
170
  show_failure_panel:
171
  If ``True``, render a grouped-bar failure-rate chart below the main
172
  performance chart.
173
+ show_scoring_modes:
174
+ If ``True``, show scoring-mode radio controls in the expander.
175
  canonical_target_levels:
176
  If provided, the target-level selector always offers exactly these
177
  values (as strings) regardless of what is present in the data. Use
 
179
  levels even when some have 100 % failure.
180
  budget_store:
181
  Optional DataFrame produced by ``load_uq_budget_store()``. When
182
+ provided, an additional "Mean Iterations for Coverage" section is
183
+ rendered below the main performance chart.
184
+ benchmark_dims:
185
+ Optional mapping of benchmark name (param_dim, state_dim, output_dim)
186
+ used to annotate column headers in the suitability table.
187
  """
188
  st.header(title)
189
 
 
191
  st.warning("No metric data found. Expected NetCDF files in `data/` with a `metric` variable.")
192
  return
193
 
194
+ _render_suitability_table(
195
+ metric_store, target_col, default_target, benchmark_dims=benchmark_dims
196
+ )
197
+
198
+ st.divider()
199
+
200
  # Derived column name for the string version of the target coordinate
201
  target_str_col = f"{target_col}_str"
202
 
203
+ benchmark_values = sorted(metric_store["benchmark"].unique().tolist())
204
+ selected_benchmark = st.selectbox("Benchmark", options=benchmark_values, index=0)
 
205
 
206
+ filtered = metric_store[metric_store["benchmark"] == selected_benchmark].copy()
 
 
 
 
207
  filtered[target_str_col] = filtered[target_col].astype(str)
208
 
209
  if canonical_target_levels is not None:
210
+ target_options = [str(float(t)) for t in canonical_target_levels]
211
  else:
212
+ target_options = sorted(
213
  metric_store[target_col].astype(str).unique().tolist()
214
  )
215
 
 
222
 
223
  # Session-state keys namespaced by state_prefix so two leaderboard pages
224
  # don't share control state.
225
+ k_target = f"{state_prefix}_selected_target"
226
  k_scoring = f"{state_prefix}_scoring_mode"
227
+ k_weight = f"{state_prefix}_fwdruns_weight_percent"
228
  k_methods = f"{state_prefix}_selected_methods"
229
 
230
+ default_target_str = str(float(default_target))
231
+
232
+ # Pre-populate session state so the radio widget and the filter agree on first load.
233
+ if k_target not in st.session_state or st.session_state[k_target] not in target_options:
234
+ st.session_state[k_target] = (
235
+ default_target_str if default_target_str in target_options else target_options[0]
236
+ )
237
+ selected_target = st.session_state[k_target]
238
 
239
  current_scoring_mode = st.session_state.get(k_scoring, "Mean Forward Model Runs")
240
  if current_scoring_mode not in scoring_options:
 
243
  current_fwdruns_weight_percent = int(st.session_state.get(k_weight, 80))
244
  current_fwdruns_weight_percent = max(0, min(100, current_fwdruns_weight_percent))
245
 
246
+ scoring_mode = current_scoring_mode
247
+ fwdruns_weight = current_fwdruns_weight_percent / 100.0
 
248
  ensemble_weight = 1.0 - fwdruns_weight
249
 
250
  # Available methods for the current benchmark selection; used to populate the
251
  # multiselect and to prune any stale saved selections when the benchmark changes.
252
  available_methods = sorted(filtered["abbreviation"].dropna().unique().tolist())
253
+ saved_methods = st.session_state.get(k_methods, available_methods)
254
+ valid_saved = [m for m in saved_methods if m in available_methods]
255
  st.session_state[k_methods] = valid_saved if valid_saved else available_methods
256
 
257
  # Stable color scale: domain covers ALL methods so colors don't shift when a
258
  # subset is displayed.
259
  color_domain = available_methods
260
+ color_range = [_METHOD_PALETTE[i % len(_METHOD_PALETTE)] for i in range(len(available_methods))]
261
  method_color = alt.Color(
262
  "abbreviation:N",
263
  title="Method",
 
265
  )
266
 
267
  def build_scored_table(input_df: pd.DataFrame, add_rank: bool = True) -> pd.DataFrame:
268
+ ranking_source = input_df[input_df[target_str_col] == selected_target]
 
 
 
 
269
  if ranking_source.empty:
270
  return ranking_source
271
 
 
284
  ).agg(
285
  **{"Mean Forward Model Runs": ("metric", "mean")},
286
  **{"Minimum Forward Model Runs": ("metric", "min")},
 
287
  **{"Ensemble Sizes Used": ("ensemble_size", "nunique")},
288
  )
289
 
 
317
  how="left",
318
  )
319
 
320
+ scored_df["Optimal Ensemble Size"] = scored_df["Optimal Ensemble Size"].round(2)
321
+ scored_df["Mean Forward Model Runs"] = scored_df["Mean Forward Model Runs"].round(4)
322
  scored_df["Minimum Forward Model Runs"] = scored_df["Minimum Forward Model Runs"].round(4)
323
+ scored_df["Mean Failure Rate (%)"] = scored_df["Mean Failure Rate (%)"].round(1)
324
 
325
  mean_runs_min = scored_df["Mean Forward Model Runs"].min()
326
  mean_runs_max = scored_df["Mean Forward Model Runs"].max()
 
379
 
380
  return scored_df
381
 
382
+ leaderboard_df = build_scored_table(filtered, add_rank=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
 
384
  if scoring_mode == "Mean Forward Model Runs":
385
+ score_basis = "mean forward-model runs at the selected target level (lower is better)"
386
  elif scoring_mode == "Minimum Forward Model Runs":
387
+ score_basis = "minimum forward-model runs at the selected target level (lower is better)"
388
  elif scoring_mode == "Smallest Optimal Ensemble Size":
389
+ score_basis = "mean optimal ensemble size at the selected target level (lower is better)"
390
  else:
391
  score_basis = (
392
+ f"weighted blend of normalized forward-model-runs score ({fwdruns_weight:.0%}) "
393
+ f"and normalized ensemble-size score ({ensemble_weight:.0%})"
394
  )
395
 
 
 
 
396
  # Controls expander — always shown so users can change target even when the
397
  # current selection yields all failures.
398
  with st.expander("Scoring & Target Controls", expanded=False):
 
434
  if not selected_methods:
435
  selected_methods = available_methods
436
 
437
+ table_column_order = [
438
+ "Placement",
439
+ "abbreviation",
440
+ "Method",
441
+ "family",
442
+ "Score",
443
+ "Mean Forward Model Runs",
444
+ "Minimum Forward Model Runs",
445
+ "Mean Failure Rate (%)",
446
+ "Optimal Ensemble Size",
447
+ "Ensemble Sizes Used",
448
+ ]
449
+
450
  if leaderboard_df.empty:
451
  st.warning(
452
  "All runs failed to reach the target at this selection. "
 
455
  else "No rows available for the current benchmark/target selection."
456
  )
457
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  st.subheader(f"Ranked Leaderboard — {selected_benchmark}")
459
  st.dataframe(
460
  leaderboard_df,
 
470
  "Score": st.column_config.ProgressColumn("Score (0-100)", min_value=0.0, max_value=100.0, format="%.1f"),
471
  "Optimal Ensemble Size": st.column_config.NumberColumn("Mean Optimal Ensemble Size", format="%.2f"),
472
  "Mean Failure Rate (%)": st.column_config.NumberColumn("Mean Failure Rate (%)", format="%.1f"),
 
473
  "Ensemble Sizes Used": st.column_config.NumberColumn("Ensemble Sizes Used", format="%d"),
 
474
  },
475
  column_order=table_column_order,
476
  )
477
 
478
  st.info(
479
  f"Score is a normalized 0–100 ranking based on **{score_basis}**. "
480
+ "Values are computed from all ensemble sizes after averaging over random seeds."
 
481
  )
482
 
483
+ st.subheader("Mean Forward Model Runs vs Ensemble Size")
484
+ chart_source = filtered[filtered[target_str_col] == selected_target]
485
+ chart_source = chart_source[chart_source["abbreviation"].isin(selected_methods)]
486
+ chart_df = chart_source.dropna(subset=["metric"]).groupby(
487
+ ["abbreviation", "ensemble_size"], as_index=False
488
+ ).agg(mean_forward_runs=("metric", "mean"))
 
 
 
 
 
 
 
 
489
 
490
+ all_ens_combos = chart_source[["abbreviation", "ensemble_size"]].drop_duplicates()
491
+ ens_ticks = sorted(all_ens_combos["ensemble_size"].unique().tolist()) if not all_ens_combos.empty else []
 
 
 
 
492
 
493
+ if not chart_df.empty:
494
+ _ok = chart_df[["abbreviation", "ensemble_size"]].assign(_ok=True)
495
+ fail_df = all_ens_combos.merge(_ok, on=["abbreviation", "ensemble_size"], how="left")
496
+ fail_df = fail_df[fail_df["_ok"].isna()].drop(columns="_ok").assign(mean_forward_runs=0.0)
497
+ else:
498
+ fail_df = all_ens_combos.assign(mean_forward_runs=0.0)
499
+
500
+ all_failed = chart_df.empty
501
+ chart_layers = []
502
+ if not chart_df.empty:
503
+ chart_layers.append(
504
+ alt.Chart(chart_df)
505
+ .mark_line(point=True)
506
+ .encode(
507
+ x=alt.X(
508
+ "ensemble_size:Q",
509
+ title="Ensemble Size",
510
+ axis=alt.Axis(values=ens_ticks, format="d"),
511
+ ),
512
+ y=alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs"),
513
+ color=method_color,
514
+ tooltip=["abbreviation", "ensemble_size", alt.Tooltip("mean_forward_runs:Q", format=".4f")],
515
  )
516
+ )
517
+ if not fail_df.empty:
518
+ y_fwd = (
519
+ alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs", scale=alt.Scale(domain=[0, 1]))
520
+ if all_failed
521
+ else alt.Y("mean_forward_runs:Q", title="Mean Forward Model Runs")
522
+ )
523
+ chart_layers.append(
524
+ alt.Chart(fail_df)
525
+ .mark_point(shape="cross", angle=45, size=200, filled=True, opacity=1.0)
526
+ .encode(
527
+ x=alt.X(
528
+ "ensemble_size:Q",
529
+ title="Ensemble Size",
530
+ axis=alt.Axis(values=ens_ticks, format="d"),
531
+ ),
532
+ y=y_fwd,
533
+ color=method_color,
534
+ tooltip=[
535
+ alt.Tooltip("abbreviation:N", title="Method"),
536
+ alt.Tooltip("ensemble_size:Q", title="Ensemble Size"),
537
+ alt.Tooltip("mean_forward_runs:Q", title="Value (all failed)"),
538
+ ],
539
  )
540
+ )
541
+ if chart_layers:
542
+ st.altair_chart(alt.layer(*chart_layers), use_container_width=True)
543
 
544
  # Mean-iterations-for-coverage section (UQ only, when budget_store provided)
545
+ if budget_store is not None and not budget_store.empty:
546
  bf = budget_store[budget_store["benchmark"] == selected_benchmark].copy()
547
  bf[target_str_col] = bf[target_col].astype(str)
548
+ bf = bf[bf[target_str_col] == selected_target]
 
549
  bf = bf[bf["abbreviation"].isin(selected_methods)]
550
 
551
  iters_df = (
 
615
  st.altair_chart(alt.layer(*iters_layers), use_container_width=True)
616
 
617
  # Failure panel — rendered regardless of whether the scored table has rows
618
+ if show_failure_panel:
619
+ failure_source = filtered[filtered[target_str_col] == selected_target]
 
 
 
 
620
  failure_source = failure_source[failure_source["abbreviation"].isin(selected_methods)]
621
  if not failure_source.empty:
622
  failure_df = failure_source.groupby(
 
624
  ).agg(mean_failure_rate=("failure_rate", "mean"))
625
  failure_df = failure_df.sort_values("ensemble_size")
626
 
627
+ st.subheader(f"Failure Rate of Hitting Target {selected_target}")
 
 
 
 
 
628
  ens_ticks_fail = sorted(failure_df["ensemble_size"].unique().tolist())
629
  failure_chart = (
630
  alt.Chart(failure_df)
src/data_store.py CHANGED
@@ -87,6 +87,15 @@ UQ_BUDGET_FILES: dict[str, list[tuple[str, str]]] = {
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
  # ---------------------------------------------------------------------------
 
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
+ # Physical dimensions of each benchmark: (param_dim, state_dim, output_dim).
91
+ # Used only for column-header annotations in the suitability table.
92
+ BENCHMARK_DIMS: dict[str, tuple[int, int, int]] = {
93
+ "L63": (2, 3, 9),
94
+ "L96": (1, 40, 80),
95
+ "L96_NN_FORCING": (61, 100, 200),
96
+ "L96_SPATIAL_FORCING": (40, 40, 80),
97
+ }
98
+
99
 
100
 
101
  # ---------------------------------------------------------------------------
src/pages/OptimizationLeaderboard.py CHANGED
@@ -4,11 +4,11 @@ import sys
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")
@@ -31,5 +31,7 @@ render_leaderboard(
31
  target_label="RMSE Target Level",
32
  title="Optimization Leaderboard",
33
  state_prefix="opt",
 
34
  raw_page="pages/RawData.py",
 
35
  )
 
4
  import streamlit as st
5
 
6
  try:
7
+ from data_store import load_metric_store, BENCHMARK_DIMS
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, BENCHMARK_DIMS
12
  from common.leaderboard import render_leaderboard
13
 
14
  st.set_page_config(page_title="Optimization Leaderboard", page_icon="📊", layout="wide")
 
31
  target_label="RMSE Target Level",
32
  title="Optimization Leaderboard",
33
  state_prefix="opt",
34
+ default_target=1.1,
35
  raw_page="pages/RawData.py",
36
+ benchmark_dims=BENCHMARK_DIMS,
37
  )
src/pages/UQLeaderboard.py CHANGED
@@ -4,11 +4,11 @@ import sys
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")
@@ -33,8 +33,10 @@ render_leaderboard(
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
  )
 
4
  import streamlit as st
5
 
6
  try:
7
+ from data_store import load_uq_store, load_uq_budget_store, UQ_TARGET_LEVELS, BENCHMARK_DIMS
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, BENCHMARK_DIMS
12
  from common.leaderboard import render_leaderboard
13
 
14
  st.set_page_config(page_title="UQ Leaderboard", page_icon="🎯", layout="wide")
 
33
  target_label="UQ Target (coverage tolerance scaling)",
34
  title="Uncertainty Quantification Leaderboard",
35
  state_prefix="uq",
36
+ default_target=1.5,
37
  show_failure_panel=True,
38
  show_scoring_modes=False,
39
  canonical_target_levels=UQ_TARGET_LEVELS,
40
  budget_store=load_uq_budget_store(),
41
+ benchmark_dims=BENCHMARK_DIMS,
42
  )