josephsoo commited on
Commit
6554c13
·
1 Parent(s): eec8730

Redesign dashboard around leaderboard

Browse files
Files changed (3) hide show
  1. README.md +7 -8
  2. app.py +956 -689
  3. assets/styles.css +174 -87
README.md CHANGED
@@ -1,18 +1,17 @@
1
  ---
2
- title: BenchDash
3
  sdk: docker
4
  app_port: 7860
5
  ---
6
 
7
- # BenchDash
8
 
9
- Interactive result dashboard for the Tang Lab neural-behavioral model
10
- benchmark.
11
 
12
- The dashboard summarizes exported benchmark results across five datasets and
13
- 23 model variants, including clean prediction, count-noise robustness,
14
- cross-session latent consistency, attribution analyses, and compute cost.
15
 
16
- The bundled files in `data/` are lightweight summary exports for interactive
17
  inspection. The full benchmark artifacts and reproduction workflow live in the
18
  main benchmark repository.
 
1
  ---
2
+ title: Neural Model Benchmark
3
  sdk: docker
4
  app_port: 7860
5
  ---
6
 
7
+ # Neural Model Benchmark
8
 
9
+ Interactive dashboard for the Tang Lab neural model benchmark.
 
10
 
11
+ The dashboard compares 23 methods across five datasets, with sortable task
12
+ performance, robustness, cross-session alignment, neuron and trial influence,
13
+ compute cost, and 3D latent-space views.
14
 
15
+ The bundled files in `data/` are lightweight summary tables for interactive
16
  inspection. The full benchmark artifacts and reproduction workflow live in the
17
  main benchmark repository.
app.py CHANGED
@@ -1,11 +1,11 @@
1
  from __future__ import annotations
2
 
 
3
  from pathlib import Path
4
  from typing import Iterable
5
 
6
  import numpy as np
7
  import pandas as pd
8
- import plotly.express as px
9
  import plotly.graph_objects as go
10
  from dash import Dash, Input, Output, dash_table, dcc, html
11
  from plotly.subplots import make_subplots
@@ -13,50 +13,172 @@ from plotly.subplots import make_subplots
13
 
14
  DATA_DIR = Path(__file__).resolve().parent / "data"
15
 
16
- MODEL_INFO = {
17
- "blend": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
18
- "blend_ndt": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
19
- "cebra": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
20
- "dnn": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
21
- "dpad": ("gpu", "full_pipeline", "tensorflow_gpu.sif", "comprehensive, consistency, neuron_shap"),
22
- "gpfa": ("cpu", "full_pipeline", "sklearn_cpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
23
- "gru": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
24
- "langevinflow_ccn": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
25
- "ldns": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
26
- "lfads_torch": ("gpu", "full_pipeline", "lfads_torch.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
27
- "lstm": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
28
- "marble": ("gpu", "full_pipeline", "marble_h100.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
29
- "mint": ("cpu", "comprehensive_only", "mint.sif", "comprehensive, neuron_shap, robustness"),
30
- "neds": ("gpu", "full_pipeline", "neds.sif", "stage1, stage2, comprehensive, neuron_shap"),
31
- "neds_pretrained": ("gpu", "full_pipeline", "neds.sif", "stage1, stage2, comprehensive, neuron_shap"),
32
- "neuro_behavior_conditioning": ("gpu", "full_pipeline", "pytorch_gpu.sif", "comprehensive, consistency, neuron_shap"),
33
- "pca": ("cpu", "full_pipeline", "sklearn_cpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
34
- "rnn": ("gpu", "comprehensive_only", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, neuron_shap"),
35
- "smc_rnns": ("gpu", "full_pipeline", "pytorch_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
36
- "svc": ("cpu", "comprehensive_only", "sklearn_cpu.sif", "comprehensive, neuron_shap"),
37
- "tndm": ("gpu", "full_pipeline", "tensorflow_gpu.sif", "stage1, stage2, comprehensive, consistency, neuron_shap"),
38
- "torchdfine": ("gpu", "full_pipeline", "pytorch_gpu.sif", "comprehensive, consistency, neuron_shap"),
39
- "xg": ("cpu", "comprehensive_only", "sklearn_cpu.sif", "comprehensive, neuron_shap"),
40
- }
41
-
42
- DEFAULT_MODELS = [
43
- "pca",
44
- "gpfa",
45
  "cebra",
 
46
  "dpad",
 
 
 
47
  "ldns",
48
  "lfads_torch",
 
49
  "marble",
 
 
 
 
 
50
  "rnn",
 
51
  "svc",
 
 
52
  "xg",
53
  ]
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  def load_csv(name: str) -> pd.DataFrame:
57
  path = DATA_DIR / name
58
  if not path.exists():
59
- raise FileNotFoundError(f"Missing bundled dashboard data: {path}")
60
  return pd.read_csv(path)
61
 
62
 
@@ -76,6 +198,12 @@ def present_rows(df: pd.DataFrame) -> pd.DataFrame:
76
  return df[df["status"].fillna("") == "present"].copy()
77
 
78
 
 
 
 
 
 
 
79
  def ordered_unique(values: Iterable[object]) -> list[str]:
80
  seen: set[str] = set()
81
  out: list[str] = []
@@ -90,8 +218,6 @@ def ordered_unique(values: Iterable[object]) -> list[str]:
90
 
91
 
92
  def build_dataset_labels() -> dict[str, str]:
93
- if prediction.empty:
94
- return {}
95
  pairs = (
96
  prediction[["dataset", "dataset_display"]]
97
  .dropna(subset=["dataset"])
@@ -102,8 +228,10 @@ def build_dataset_labels() -> dict[str, str]:
102
 
103
  DATASET_LABELS = build_dataset_labels()
104
  DATASETS = ordered_unique(prediction.get("dataset", pd.Series(dtype=str)))
105
- MODELS = sorted(set(prediction.get("model", pd.Series(dtype=str)).dropna().astype(str)) | set(MODEL_INFO))
106
- DEFAULT_SELECTED_MODELS = [m for m in DEFAULT_MODELS if m in MODELS] or MODELS[:10]
 
 
107
 
108
  CONDITION_LABELS = {
109
  "monkey": {
@@ -133,6 +261,33 @@ CONDITION_LABELS = {
133
  }
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def condition_sort_key(value: object) -> tuple[int, float | str]:
137
  try:
138
  return (0, float(value))
@@ -156,71 +311,91 @@ def condition_label(dataset: str, condition: object) -> str:
156
  return text
157
 
158
 
159
- def model_metadata_frame() -> pd.DataFrame:
160
- rows = []
161
- present = present_rows(prediction)
162
- coverage = (
163
- present.groupby("model")["dataset"]
164
- .nunique()
165
- .rename("present_datasets")
166
- .to_dict()
167
- if not present.empty
168
- else {}
169
- )
170
- for model in MODELS:
171
- accelerator, pipeline, container, stages = MODEL_INFO.get(
172
- model, ("unknown", "unknown", "unknown", "")
173
- )
174
- rows.append(
175
- {
176
- "model": model,
177
- "accelerator": accelerator,
178
- "pipeline": pipeline,
179
- "container": container,
180
- "present_datasets": int(coverage.get(model, 0)),
181
- "expected_datasets": len(DATASETS),
182
- "stages": stages,
183
- }
184
- )
185
- return pd.DataFrame(rows).sort_values(["accelerator", "pipeline", "model"])
186
 
187
 
188
- model_inventory = model_metadata_frame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
 
191
- def compact_number(value: float | int | None, *, digits: int = 3) -> str:
192
- if value is None or pd.isna(value):
193
- return ""
194
- value = float(value)
195
- if abs(value) >= 1000:
196
- return f"{value:,.0f}"
197
- return f"{value:.{digits}g}"
198
 
199
 
200
- def selected_models(models: list[str] | None) -> list[str]:
201
- if not models:
202
- return MODELS
203
- return [m for m in models if m in MODELS]
204
 
205
 
206
- def filter_models(df: pd.DataFrame, models: list[str] | None) -> pd.DataFrame:
207
- if df.empty or "model" not in df.columns:
208
- return df.copy()
209
- chosen = selected_models(models)
210
- return df[df["model"].astype(str).isin(chosen)].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
 
213
  def fig_layout(fig: go.Figure, *, height: int = 420) -> go.Figure:
214
  fig.update_layout(
215
  height=height,
216
- paper_bgcolor="white",
217
- plot_bgcolor="white",
218
- margin=dict(l=18, r=18, t=54, b=36),
219
- font=dict(family="Inter, Arial, sans-serif", size=13, color="#1f2933"),
 
220
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
221
  )
222
- fig.update_xaxes(showgrid=True, gridcolor="#e8edf1", zerolinecolor="#cfd8df")
223
- fig.update_yaxes(showgrid=True, gridcolor="#e8edf1", zerolinecolor="#cfd8df")
224
  return fig
225
 
226
 
@@ -233,29 +408,11 @@ def empty_figure(message: str) -> go.Figure:
233
  xref="paper",
234
  yref="paper",
235
  showarrow=False,
236
- font=dict(size=15, color="#536471"),
237
  )
238
  fig.update_xaxes(visible=False)
239
  fig.update_yaxes(visible=False)
240
- return fig_layout(fig)
241
-
242
-
243
- def latent_fig_layout(fig: go.Figure, *, height: int) -> go.Figure:
244
- fig.update_layout(
245
- height=height,
246
- paper_bgcolor="white",
247
- plot_bgcolor="white",
248
- margin=dict(l=8, r=8, t=76, b=84),
249
- font=dict(family="Inter, Arial, sans-serif", size=13, color="#1f2933"),
250
- legend=dict(
251
- orientation="h",
252
- yanchor="top",
253
- y=-0.08,
254
- xanchor="left",
255
- x=0,
256
- ),
257
- )
258
- return fig
259
 
260
 
261
  def parse_float_list(value: object) -> list[float]:
@@ -273,46 +430,304 @@ def parse_float_list(value: object) -> list[float]:
273
  return out
274
 
275
 
276
- def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
277
- if latent_samples.empty:
278
- return empty_figure("Latent-space samples are not packaged for this build.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
 
280
  chosen = selected_models(models)
281
- df = latent_samples[
282
- (latent_samples["dataset"].astype(str) == str(dataset))
283
- & (latent_samples["model"].astype(str).isin(chosen))
284
- ].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  if df.empty:
286
- label = DATASET_LABELS.get(dataset, dataset)
287
- return empty_figure(f"Latent-space samples are not available for {label}.")
 
 
 
 
 
288
 
289
- score_df = consistency.copy()
290
- if "is_active_model" in score_df.columns:
291
- score_df = score_df[score_df["is_active_model"].astype(str).str.lower() == "true"]
292
- score_df = score_df[
293
- (score_df["dataset"].astype(str) == str(dataset))
294
- & (score_df["model"].astype(str).isin(df["model"].astype(str).unique()))
295
- ].copy()
296
- if not score_df.empty:
297
- score_df["mean_r2"] = pd.to_numeric(score_df["mean_r2"], errors="coerce")
298
- model = (
299
- score_df.sort_values("mean_r2", ascending=False)["model"]
300
- .astype(str)
301
- .iloc[0]
 
 
 
 
 
 
 
 
 
302
  )
303
- else:
304
- model = sorted(df["model"].astype(str).unique())[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
- plot_df = df[df["model"].astype(str) == model].copy()
307
  for col in ["x", "y", "z"]:
308
  plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
309
  plot_df["condition_num"] = pd.to_numeric(plot_df["condition"], errors="coerce")
310
- plot_df["condition_label"] = plot_df["condition"].map(
311
- lambda value: condition_label(dataset, value)
312
- )
313
  plot_df = plot_df.dropna(subset=["x", "y", "z"])
314
  if plot_df.empty:
315
- return empty_figure("Latent-space samples are empty after filtering.")
316
 
317
  trajectory_df = latent_trajectories[
318
  (latent_trajectories["dataset"].astype(str) == str(dataset))
@@ -322,72 +737,48 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
322
  if col in trajectory_df:
323
  trajectory_df[col] = pd.to_numeric(trajectory_df[col], errors="coerce")
324
  if not trajectory_df.empty:
325
- trajectory_df["condition_label"] = trajectory_df["condition"].map(
326
- lambda value: condition_label(dataset, value)
327
- )
328
  trajectory_df = trajectory_df.dropna(subset=["x", "y", "z"])
329
 
330
  sessions = ordered_unique(plot_df["session_label"])
331
- n_cols = 2
 
332
  n_rows = int(np.ceil(len(sessions) / n_cols))
 
333
  fig = make_subplots(
334
  rows=n_rows,
335
  cols=n_cols,
336
- specs=[[{"type": "scene"} for _ in range(n_cols)] for _ in range(n_rows)],
337
- subplot_titles=sessions,
338
- horizontal_spacing=0.035,
339
- vertical_spacing=0.105,
340
  )
341
 
342
- condition_values = sorted(
343
- plot_df["condition"].astype(str).unique(),
344
- key=condition_sort_key,
345
- )
346
- use_categorical_conditions = len(condition_values) <= 12
347
- direction_palette = [
348
- "#B23AEE",
349
- "#3B1C54",
350
- "#2DD4F6",
351
- "#289285",
352
- "#E3D724",
353
- "#00A65A",
354
- "#5B8FF9",
355
- "#F97316",
356
- "#E45756",
357
- "#72B7B2",
358
- "#54A24B",
359
- "#B279A2",
360
- ]
361
  condition_colors = {
362
- condition: direction_palette[idx % len(direction_palette)]
363
  for idx, condition in enumerate(condition_values)
364
  }
365
- if dataset == "monkey":
366
- condition_prefix = "Reach direction"
367
- elif dataset == "ratinabox":
368
- condition_prefix = "Position bin"
369
- elif dataset == "allen_neuropixels":
370
- condition_prefix = "Orientation"
371
- elif dataset == "speech":
372
- condition_prefix = "Cue"
373
- else:
374
- condition_prefix = "Condition"
375
 
376
  for session_idx, session in enumerate(sessions):
377
- session_df = plot_df[plot_df["session_label"].astype(str) == session]
378
  if session_df.empty:
379
  continue
380
  row = session_idx // n_cols + 1
381
  col = session_idx % n_cols + 1
 
382
 
383
- if use_categorical_conditions:
384
  for condition in condition_values:
385
  cond_df = session_df[session_df["condition"].astype(str) == condition]
386
  if cond_df.empty:
387
  continue
388
  trace_name = condition_label(dataset, condition)
389
  session_traj = trajectory_df[
390
- (trajectory_df["session_label"].astype(str) == session)
391
  & (trajectory_df["condition"].astype(str) == condition)
392
  ].sort_values("time_index")
393
  fig.add_trace(
@@ -400,13 +791,13 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
400
  legendgroup=condition,
401
  showlegend=session_idx == 0,
402
  marker=dict(
403
- size=1.9 if not session_traj.empty else 2.6,
404
- opacity=0.28 if not session_traj.empty else 0.72,
405
  color=condition_colors[condition],
406
  ),
407
  customdata=np.stack(
408
  [
409
- cond_df["session_label"].astype(str),
410
  cond_df["condition_label"].astype(str),
411
  cond_df["trial_index"].astype(str),
412
  cond_df["time_index"].astype(str),
@@ -414,9 +805,9 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
414
  axis=-1,
415
  ),
416
  hovertemplate=(
417
- "session=%{customdata[0]}<br>"
418
- f"{condition_prefix.lower()}=%{{customdata[1]}}<br>"
419
- "trial=%{customdata[2]} time=%{customdata[3]}"
420
  "<extra></extra>"
421
  ),
422
  ),
@@ -435,8 +826,8 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
435
  showlegend=False,
436
  line=dict(color=condition_colors[condition], width=5),
437
  hovertemplate=(
438
- f"{condition_prefix.lower()}={trace_name}<br>"
439
- "time=%{customdata}<extra></extra>"
440
  ),
441
  customdata=session_traj["time_index"],
442
  ),
@@ -450,19 +841,19 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
450
  y=session_df["y"],
451
  z=session_df["z"],
452
  mode="markers",
453
- name=session,
454
  showlegend=False,
455
  marker=dict(
456
- size=2.6,
457
  opacity=0.72,
458
  color=session_df["condition_num"],
459
  colorscale="Viridis",
460
  showscale=session_idx == 0,
461
- colorbar=dict(title=condition_prefix),
462
  ),
463
  customdata=np.stack(
464
  [
465
- session_df["condition"].astype(str),
466
  session_df["condition"].map(lambda value: condition_label(dataset, value)).astype(str),
467
  session_df["trial_index"].astype(str),
468
  session_df["time_index"].astype(str),
@@ -470,9 +861,9 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
470
  axis=-1,
471
  ),
472
  hovertemplate=(
473
- "session=%{fullData.name}<br>"
474
- f"{condition_prefix.lower()}=%{{customdata[1]}}<br>"
475
- "trial=%{customdata[2]} time=%{customdata[3]}"
476
  "<extra></extra>"
477
  ),
478
  ),
@@ -492,108 +883,222 @@ def latent_space_figure(dataset: str, models: list[str] | None) -> go.Figure:
492
  fig.update_layout(
493
  **{
494
  scene_id: dict(
495
- xaxis=dict(
496
- title="",
497
- range=[-lim, lim],
498
- showgrid=False,
499
- zeroline=False,
500
- showticklabels=False,
501
- ),
502
- yaxis=dict(
503
- title="",
504
- range=[-lim, lim],
505
- showgrid=False,
506
- zeroline=False,
507
- showticklabels=False,
508
- ),
509
- zaxis=dict(
510
- title="",
511
- range=[-lim, lim],
512
- showgrid=False,
513
- zeroline=False,
514
- showticklabels=False,
515
- ),
516
  aspectmode="cube",
517
  bgcolor="#ffffff",
518
  camera=dict(eye=dict(x=1.55, y=1.45, z=1.05)),
519
  )
520
  }
521
  )
 
 
 
 
 
 
522
  score = None
523
- if not score_df.empty and model in set(score_df["model"].astype(str)):
524
- score = float(score_df.loc[score_df["model"].astype(str) == model, "mean_r2"].iloc[0])
525
- score_text = "" if score is None or pd.isna(score) else f" (mean R2={score:.2f})"
 
526
  fig.update_layout(
527
- title=(
528
- f"{DATASET_LABELS.get(dataset, dataset)} latent samples: {model}{score_text}"
529
- ),
 
 
 
 
530
  )
531
- fig.for_each_annotation(lambda ann: ann.update(font=dict(size=11, color="#536271")))
532
- fig.update_layout(legend=dict(title=condition_prefix))
533
- return latent_fig_layout(fig, height=720 if n_rows > 1 else 430)
534
 
535
 
536
- def dataframe_table(
537
- table_id: str,
538
- *,
539
- columns: list[str] | None = None,
540
- data: list[dict] | None = None,
541
- page_size: int = 12,
542
- ) -> dash_table.DataTable:
543
- return dash_table.DataTable(
544
- id=table_id,
545
- columns=[{"name": c, "id": c} for c in (columns or [])],
546
- data=data or [],
547
- page_size=page_size,
548
- sort_action="native",
549
- filter_action="native",
550
- style_table={"overflowX": "auto"},
551
- style_header={
552
- "backgroundColor": "#f6f8fa",
553
- "fontWeight": "700",
554
- "border": "1px solid #d9e2e8",
555
- },
556
- style_cell={
557
- "fontFamily": "Inter, Arial, sans-serif",
558
- "fontSize": "13px",
559
- "padding": "9px",
560
- "textAlign": "left",
561
- "minWidth": "100px",
562
- "maxWidth": "280px",
563
- "whiteSpace": "normal",
564
- "height": "auto",
565
- "border": "1px solid #e6edf2",
566
- },
 
 
 
 
 
 
 
 
 
 
567
  )
 
 
568
 
569
 
570
- def stat_card(label: str, value: str, detail: str) -> html.Div:
571
- return html.Div(
572
- [
573
- html.Div(label, className="stat-label"),
574
- html.Div(value, className="stat-value"),
575
- html.Div(detail, className="stat-detail"),
576
- ],
577
- className="stat-card",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  )
 
 
 
 
579
 
 
 
 
 
 
 
 
 
580
 
581
- def panel(title: str, *children, subtitle: str | None = None) -> html.Div:
582
- heading = [html.H2(title)]
583
- if subtitle:
584
- heading.append(html.P(subtitle, className="panel-subtitle"))
585
- return html.Div([html.Div(heading, className="panel-heading"), *children], className="panel")
586
 
587
 
588
- present_prediction = present_rows(prediction)
589
- present_cells = len(present_prediction)
590
- total_cells = len(prediction)
591
- coverage_text = f"{present_cells}/{total_cells}" if total_cells else "0/0"
592
- coverage_pct = 100.0 * present_cells / total_cells if total_cells else 0.0
593
- runtime_counts = model_inventory["accelerator"].value_counts().to_dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
 
595
 
596
- app = Dash(__name__, title="BenchDash")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  server = app.server
598
 
599
  app.layout = html.Div(
@@ -603,10 +1108,9 @@ app.layout = html.Div(
603
  html.Div(
604
  [
605
  html.Div("Tang Lab", className="eyebrow"),
606
- html.H1("Benchmark results"),
607
  html.P(
608
- "Compare neural-behavioral models across prediction, robustness, "
609
- "cross-session latent geometry, attribution, and compute cost.",
610
  className="lede",
611
  ),
612
  ],
@@ -614,16 +1118,10 @@ app.layout = html.Div(
614
  ),
615
  html.Div(
616
  [
617
- stat_card("Models", str(len(MODELS)), "registered benchmark methods"),
618
- stat_card("Datasets", str(len(DATASETS)), "neural-behavioral tasks"),
619
- stat_card("Result Coverage", coverage_text, f"{coverage_pct:.1f}% complete"),
620
- stat_card(
621
- "Compute Mix",
622
- f"{runtime_counts.get('cpu', 0)} CPU / {runtime_counts.get('gpu', 0)} GPU",
623
- "registered model runtimes",
624
- ),
625
  ],
626
- className="stat-grid",
627
  ),
628
  ],
629
  className="hero",
@@ -635,10 +1133,7 @@ app.layout = html.Div(
635
  html.Label("Dataset"),
636
  dcc.Dropdown(
637
  id="dataset-filter",
638
- options=[
639
- {"label": DATASET_LABELS.get(ds, ds), "value": ds}
640
- for ds in DATASETS
641
- ],
642
  value=DATASETS[0] if DATASETS else None,
643
  clearable=False,
644
  ),
@@ -647,116 +1142,158 @@ app.layout = html.Div(
647
  ),
648
  html.Div(
649
  [
650
- html.Label("Models"),
651
  dcc.Dropdown(
652
  id="model-filter",
653
- options=[{"label": m, "value": m} for m in MODELS],
654
- value=DEFAULT_SELECTED_MODELS,
655
  multi=True,
656
- placeholder="Clear to show all models",
657
  ),
658
  ],
659
  className="control control-wide",
660
  ),
661
  ],
662
- className="controls",
663
  ),
664
  dcc.Tabs(
665
  id="tabs",
666
- value="prediction",
667
  className="tabs",
668
  children=[
669
  dcc.Tab(
670
- label="Overview",
671
- value="prediction",
 
 
672
  children=[
673
  panel(
674
- "Clean Prediction Scores",
675
- dcc.Graph(id="prediction-heatmap", config={"displayModeBar": False}),
676
- dcc.Graph(id="prediction-ranking", config={"displayModeBar": False}),
677
- dataframe_table("prediction-table"),
678
- subtitle="Scores are shown within dataset. Regression tasks use R2; classification tasks use accuracy.",
 
 
 
 
 
 
 
 
 
 
679
  )
680
  ],
681
  ),
682
  dcc.Tab(
683
- label="Robustness",
684
- value="robustness",
 
 
685
  children=[
686
  panel(
687
- "Count-Noise Robustness",
688
- dcc.Graph(id="robustness-curve", config={"displayModeBar": False}),
689
- dataframe_table("robustness-table"),
690
- subtitle="Curves show score as deterministic count noise increases from the clean condition.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  )
692
  ],
693
  ),
694
  dcc.Tab(
695
- label="Consistency",
696
- value="consistency",
 
 
697
  children=[
698
  panel(
699
- "Cross-Session Latent Consistency",
700
- dcc.Graph(
701
- id="latent-space",
702
- config={
703
- "displayModeBar": "hover",
704
- "toImageButtonOptions": {
705
- "format": "png",
706
- "filename": "benchdash_latent_space",
707
- "height": 900,
708
- "width": 1200,
709
- "scale": 2,
710
- },
711
- },
712
- ),
713
- dcc.Graph(id="consistency-bars", config={"displayModeBar": False}),
714
- dcc.Graph(id="consistency-heatmap", config={"displayModeBar": False}),
715
- dataframe_table("consistency-table"),
716
- subtitle="Latent clouds show sampled 3D embeddings from real consistency artifacts. Scores summarize cross-session alignment.",
717
  )
718
  ],
719
  ),
720
  dcc.Tab(
721
- label="Runtime",
722
- value="runtime",
 
 
723
  children=[
724
  panel(
725
- "Score, Time, and Memory",
726
- dcc.Graph(id="runtime-scatter", config={"displayModeBar": False}),
727
- dcc.Graph(id="memory-bars", config={"displayModeBar": False}),
728
- dataframe_table("runtime-table"),
729
- subtitle="Training time and memory are summarized from benchmark run metadata.",
 
 
 
 
 
730
  )
731
  ],
732
  ),
733
  dcc.Tab(
734
- label="Attribution",
735
- value="attribution",
 
 
736
  children=[
737
  panel(
738
- "Perturbation and Attribution Summaries",
739
- dcc.Graph(id="neuron-shap-bars", config={"displayModeBar": False}),
740
- dcc.Graph(id="trial-shapley-bars", config={"displayModeBar": False}),
741
- dataframe_table("attribution-table"),
742
- subtitle="Signed SHAP and trial-Shapley summaries are shown without absolute-value ranking.",
 
 
 
 
 
743
  )
744
  ],
745
  ),
746
  dcc.Tab(
747
  label="Methods",
748
- value="coverage",
 
 
749
  children=[
750
  panel(
751
- "Model and Analysis Coverage",
752
- dcc.Graph(id="coverage-bars", config={"displayModeBar": False}),
753
- dataframe_table(
754
- "coverage-table",
755
- columns=list(model_inventory.columns),
756
- data=model_inventory.to_dict("records"),
757
- page_size=25,
758
- ),
759
- subtitle="Coverage reports which methods have exported benchmark summaries in this dashboard.",
760
  )
761
  ],
762
  ),
@@ -768,381 +1305,111 @@ app.layout = html.Div(
768
 
769
 
770
  @app.callback(
771
- Output("prediction-heatmap", "figure"),
772
- Output("prediction-ranking", "figure"),
773
- Output("prediction-table", "columns"),
774
- Output("prediction-table", "data"),
 
775
  Input("dataset-filter", "value"),
776
  Input("model-filter", "value"),
777
  )
778
- def update_prediction(dataset: str, models: list[str] | None):
779
- df_all = filter_models(prediction, models)
780
- if df_all.empty:
781
- cols = [{"name": c, "id": c} for c in ["model", "dataset", "status", "score"]]
782
- return empty_figure("No prediction rows found."), empty_figure("No selected models."), cols, []
783
-
784
- heat_df = df_all.copy()
785
- heat_df["score"] = pd.to_numeric(heat_df["score"], errors="coerce")
786
- heat_df["dataset_label"] = heat_df["dataset"].map(DATASET_LABELS).fillna(heat_df["dataset"])
787
- pivot = heat_df.pivot_table(index="model", columns="dataset_label", values="score", aggfunc="first")
788
- if not pivot.empty:
789
- preferred = [m for m in selected_models(models) if m in pivot.index]
790
- remaining = [m for m in sorted(pivot.index.astype(str)) if m not in preferred]
791
- pivot = pivot.loc[preferred + remaining]
792
- dataset_order = [DATASET_LABELS.get(ds, ds) for ds in DATASETS if DATASET_LABELS.get(ds, ds) in pivot.columns]
793
- pivot = pivot.reindex(columns=dataset_order)
794
- text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.3f}") if not pivot.empty else pivot
795
- zmin = min(0.0, float(np.nanmin(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 0.0
796
- zmax = max(1.0, float(np.nanmax(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 1.0
797
- heatmap = go.Figure(
798
- go.Heatmap(
799
- z=pivot.values if not pivot.empty else [[]],
800
- x=list(pivot.columns),
801
- y=list(pivot.index),
802
- text=text.values if not pivot.empty else [[]],
803
- texttemplate="%{text}",
804
- colorscale=[[0.0, "#b85c38"], [0.5, "#f7f2e8"], [1.0, "#176b5a"]],
805
- zmin=zmin,
806
- zmax=zmax,
807
- colorbar=dict(title="score"),
808
- hovertemplate="model=%{y}<br>dataset=%{x}<br>score=%{z:.4f}<extra></extra>",
809
- )
810
- )
811
- heatmap.update_layout(title="Clean-score matrix by dataset")
812
- fig_layout(heatmap, height=max(430, 28 * len(pivot.index) + 170))
813
-
814
- rank_df = present_rows(df_all)
815
- rank_df = rank_df[rank_df["dataset"] == dataset].copy()
816
- rank_df["score"] = pd.to_numeric(rank_df["score"], errors="coerce")
817
- rank_df = rank_df.dropna(subset=["score"]).sort_values("score", ascending=True)
818
- if rank_df.empty:
819
- ranking = empty_figure(f"No clean prediction rows for {DATASET_LABELS.get(dataset, dataset)}.")
820
- else:
821
- metric = ordered_unique(rank_df["metric"])[0]
822
- ranking = go.Figure(
823
- go.Bar(
824
- x=rank_df["score"],
825
- y=rank_df["model"],
826
- orientation="h",
827
- marker=dict(color=rank_df["score"], colorscale="Teal", colorbar=dict(title=metric)),
828
- hovertemplate="model=%{y}<br>score=%{x:.4f}<extra></extra>",
829
- )
830
- )
831
- ranking.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} ranking ({metric})")
832
- ranking.update_xaxes(title=metric)
833
- ranking.update_yaxes(title="")
834
- fig_layout(ranking, height=max(420, 27 * len(rank_df) + 160))
835
-
836
- table_cols = [
837
- "model",
838
- "dataset_display",
839
- "status",
840
  "metric",
841
- "score",
842
- "decoder",
843
- "latent_dim",
844
- "n_train_trials",
845
- "n_test_trials",
846
- "n_neurons",
847
  ]
848
- table_df = df_all[df_all["dataset"] == dataset].copy()
849
- for col in ["score", "latent_dim", "n_train_trials", "n_test_trials", "n_neurons"]:
850
- if col in table_df:
851
- table_df[col] = table_df[col].map(lambda v: compact_number(v))
852
- table_df = table_df[[c for c in table_cols if c in table_df.columns]]
853
  return (
854
- heatmap,
855
- ranking,
856
- [{"name": c, "id": c} for c in table_df.columns],
857
- table_df.to_dict("records"),
 
858
  )
859
 
860
 
861
  @app.callback(
862
- Output("robustness-curve", "figure"),
863
- Output("robustness-table", "columns"),
864
- Output("robustness-table", "data"),
 
 
865
  Input("dataset-filter", "value"),
866
  Input("model-filter", "value"),
 
867
  )
868
- def update_robustness(dataset: str, models: list[str] | None):
869
- df = filter_models(present_rows(robustness), models)
870
- df = df[df["dataset"] == dataset].copy() if not df.empty else df
871
- if df.empty:
872
- cols = [{"name": c, "id": c} for c in ["model", "metric", "raw_auc", "score_at_noise0"]]
873
- return empty_figure("No robustness rows for this selection."), cols, []
874
-
875
- fig = go.Figure()
876
- for row in df.sort_values("model").itertuples():
877
- xs = parse_float_list(row.noise_levels)
878
- ys = parse_float_list(row.scores)
879
- if xs and len(xs) == len(ys):
880
- fig.add_trace(
881
- go.Scatter(
882
- x=xs,
883
- y=ys,
884
- mode="lines+markers",
885
- name=row.model,
886
- hovertemplate="noise=%{x:.2f}<br>score=%{y:.4f}<extra></extra>",
887
- )
888
- )
889
- fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} robustness curves")
890
- fig.update_xaxes(title="noise fraction")
891
- fig.update_yaxes(title=ordered_unique(df["metric"])[0] if "metric" in df else "score")
892
- fig_layout(fig, height=520)
893
-
894
- table_cols = ["model", "metric", "score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]
895
- table_df = df[table_cols].copy()
896
- for col in ["score_at_noise0", "score_at_max_noise", "raw_auc", "mean_score"]:
897
- table_df[col] = table_df[col].map(lambda v: compact_number(v))
898
- return fig, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
899
 
900
 
901
  @app.callback(
902
- Output("latent-space", "figure"),
903
- Output("consistency-bars", "figure"),
904
- Output("consistency-heatmap", "figure"),
905
- Output("consistency-table", "columns"),
906
- Output("consistency-table", "data"),
907
  Input("dataset-filter", "value"),
908
  Input("model-filter", "value"),
909
  )
910
- def update_consistency(dataset: str, models: list[str] | None):
911
- latent_fig = latent_space_figure(dataset, models)
912
- df_all = filter_models(consistency, models)
913
- if "is_active_model" in df_all.columns:
914
- df_all = df_all[df_all["is_active_model"].astype(str).str.lower() == "true"]
915
- df = df_all[df_all["dataset"] == dataset].copy() if not df_all.empty else df_all
916
- if df.empty:
917
- cols = [{"name": c, "id": c} for c in ["model", "dataset", "mean_r2"]]
918
- return (
919
- latent_fig,
920
- empty_figure("Consistency analysis is not available for this dataset."),
921
- empty_figure("Consistency analysis is not available for this dataset."),
922
- cols,
923
- [],
924
- )
925
-
926
- df["mean_r2"] = pd.to_numeric(df["mean_r2"], errors="coerce")
927
- bar_df = df.sort_values("mean_r2", ascending=True)
928
- bars = go.Figure(
929
- go.Bar(
930
- x=bar_df["mean_r2"],
931
- y=bar_df["model"],
932
- orientation="h",
933
- marker=dict(color=bar_df["mean_r2"], colorscale="Bluyl"),
934
- hovertemplate="model=%{y}<br>R2=%{x:.4f}<extra></extra>",
935
- )
936
- )
937
- bars.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} cross-session alignment")
938
- bars.update_xaxes(title="mean R2")
939
- fig_layout(bars, height=max(420, 27 * len(bar_df) + 150))
940
-
941
- heat = df_all.copy()
942
- heat["mean_r2"] = pd.to_numeric(heat["mean_r2"], errors="coerce")
943
- heat["dataset_label"] = heat["dataset"].map(DATASET_LABELS).fillna(heat["dataset"])
944
- pivot = heat.pivot_table(index="model", columns="dataset_label", values="mean_r2", aggfunc="first")
945
- if not pivot.empty:
946
- pivot = pivot.loc[pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index]
947
- text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.2f}") if not pivot.empty else pivot
948
- heatmap = go.Figure(
949
- go.Heatmap(
950
- z=pivot.values if not pivot.empty else [[]],
951
- x=list(pivot.columns),
952
- y=list(pivot.index),
953
- text=text.values if not pivot.empty else [[]],
954
- texttemplate="%{text}",
955
- colorscale=[[0.0, "#b85c38"], [0.5, "#f7f2e8"], [1.0, "#176b5a"]],
956
- colorbar=dict(title="R2"),
957
- hovertemplate="model=%{y}<br>dataset=%{x}<br>R2=%{z:.4f}<extra></extra>",
958
- )
959
- )
960
- heatmap.update_layout(title="Consistency matrix")
961
- fig_layout(heatmap, height=max(410, 28 * len(pivot.index) + 160))
962
-
963
- table_cols = [
964
- "model",
965
- "dataset",
966
- "n_sessions",
967
- "latent_dim",
968
- "mean_r2",
969
- "n_pairwise",
970
- "scoring_modes",
971
- ]
972
- table_df = df[[c for c in table_cols if c in df.columns]].copy()
973
- for col in ["mean_r2", "latent_dim", "n_sessions", "n_pairwise"]:
974
- if col in table_df:
975
- table_df[col] = table_df[col].map(lambda v: compact_number(v))
976
  return (
977
- latent_fig,
978
- bars,
979
- heatmap,
980
- [{"name": c, "id": c} for c in table_df.columns],
981
- table_df.to_dict("records"),
982
  )
983
 
984
 
985
  @app.callback(
986
- Output("runtime-scatter", "figure"),
987
  Output("memory-bars", "figure"),
988
- Output("runtime-table", "columns"),
989
- Output("runtime-table", "data"),
990
  Input("dataset-filter", "value"),
991
  Input("model-filter", "value"),
992
  )
993
- def update_runtime(dataset: str, models: list[str] | None):
994
- df = filter_models(present_rows(scalability), models)
995
- df = df[df["dataset"] == dataset].copy() if not df.empty else df
996
- if df.empty:
997
- cols = [{"name": c, "id": c} for c in ["model", "training_time_sec", "peak_ram_gb"]]
998
- return empty_figure("No runtime rows for this selection."), empty_figure("No memory rows."), cols, []
999
-
1000
- pred = present_rows(prediction)[["model", "dataset", "score", "metric"]].copy()
1001
- df = df.merge(pred, on=["model", "dataset"], how="left")
1002
- for col in ["training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb", "score"]:
1003
- df[col] = pd.to_numeric(df[col], errors="coerce")
1004
- df["accelerator"] = df["model"].map(lambda m: MODEL_INFO.get(m, ("unknown", "", "", ""))[0])
1005
- df["pipeline"] = df["model"].map(lambda m: MODEL_INFO.get(m, ("", "unknown", "", ""))[1])
1006
-
1007
- scatter = px.scatter(
1008
- df,
1009
- x="training_time_sec",
1010
- y="score",
1011
- size="peak_ram_gb",
1012
- color="accelerator",
1013
- symbol="pipeline",
1014
- hover_name="model",
1015
- hover_data={
1016
- "training_time_sec": ":.3f",
1017
- "inference_time_sec": ":.3f",
1018
- "peak_ram_gb": ":.3f",
1019
- "peak_vram_gb": ":.3f",
1020
- "score": ":.4f",
1021
- },
1022
- log_x=True,
1023
- title=f"{DATASET_LABELS.get(dataset, dataset)} score vs training time",
1024
- )
1025
- scatter.update_xaxes(title="training time (sec, log)")
1026
- scatter.update_yaxes(title=ordered_unique(df["metric"].dropna())[0] if df["metric"].notna().any() else "score")
1027
- fig_layout(scatter, height=500)
1028
-
1029
- mem_df = df.sort_values("peak_ram_gb", ascending=True)
1030
- memory = go.Figure()
1031
- memory.add_trace(go.Bar(x=mem_df["peak_ram_gb"], y=mem_df["model"], orientation="h", name="RAM GB"))
1032
- memory.add_trace(go.Bar(x=mem_df["peak_vram_gb"], y=mem_df["model"], orientation="h", name="VRAM GB"))
1033
- memory.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} peak memory", barmode="group")
1034
- memory.update_xaxes(title="GB")
1035
- fig_layout(memory, height=max(420, 29 * len(mem_df) + 160))
1036
-
1037
- table_cols = [
1038
- "model",
1039
- "accelerator",
1040
- "score",
1041
- "training_time_sec",
1042
- "inference_time_sec",
1043
- "peak_ram_gb",
1044
- "peak_vram_gb",
1045
- ]
1046
- table_df = df[table_cols].copy()
1047
- for col in ["score", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]:
1048
- table_df[col] = table_df[col].map(lambda v: compact_number(v))
1049
- return scatter, memory, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
1050
 
1051
 
1052
  @app.callback(
1053
- Output("neuron-shap-bars", "figure"),
1054
- Output("trial-shapley-bars", "figure"),
1055
- Output("attribution-table", "columns"),
1056
- Output("attribution-table", "data"),
1057
  Input("dataset-filter", "value"),
1058
  Input("model-filter", "value"),
1059
  )
1060
- def update_attribution(dataset: str, models: list[str] | None):
1061
- nshap = filter_models(neuron_shap, models)
1062
- if "is_active_model" in nshap.columns:
1063
- nshap = nshap[nshap["is_active_model"].astype(str).str.lower() == "true"]
1064
- nshap = nshap[nshap["dataset"] == dataset].copy() if not nshap.empty else nshap
1065
- if nshap.empty:
1066
- nshap_fig = empty_figure("No neuron-SHAP rows for this dataset.")
1067
- else:
1068
- nshap["auc"] = pd.to_numeric(nshap["auc"], errors="coerce")
1069
- bar_df = nshap.dropna(subset=["auc"]).sort_values("auc", ascending=True)
1070
- nshap_fig = go.Figure(
1071
- go.Bar(
1072
- x=bar_df["auc"],
1073
- y=bar_df["model"],
1074
- orientation="h",
1075
- marker=dict(color=bar_df["auc"], colorscale="Teal"),
1076
- hovertemplate="model=%{y}<br>AUC=%{x:.4f}<extra></extra>",
1077
- )
1078
- )
1079
- nshap_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} neuron perturbation AUC")
1080
- nshap_fig.update_xaxes(title="AUC")
1081
- fig_layout(nshap_fig, height=max(420, 27 * len(bar_df) + 150))
1082
-
1083
- tshap = filter_models(trial_shapley, models)
1084
- if "is_active_model" in tshap.columns:
1085
- tshap = tshap[tshap["is_active_model"].astype(str).str.lower() == "true"]
1086
- tshap = tshap[tshap["dataset"] == dataset].copy() if not tshap.empty else tshap
1087
- if tshap.empty:
1088
- tshap_fig = empty_figure("No trial-Shapley rows for this dataset.")
1089
- else:
1090
- tshap["perturbation_auc"] = pd.to_numeric(tshap["perturbation_auc"], errors="coerce")
1091
- trial_df = tshap.dropna(subset=["perturbation_auc"]).sort_values("perturbation_auc", ascending=True)
1092
- tshap_fig = go.Figure(
1093
- go.Bar(
1094
- x=trial_df["perturbation_auc"],
1095
- y=trial_df["model"],
1096
- orientation="h",
1097
- marker=dict(color=trial_df["perturbation_auc"], colorscale="Bluyl"),
1098
- hovertemplate="model=%{y}<br>perturbation AUC=%{x:.4f}<extra></extra>",
1099
- )
1100
- )
1101
- tshap_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} trial-Shapley perturbation AUC")
1102
- tshap_fig.update_xaxes(title="perturbation AUC")
1103
- fig_layout(tshap_fig, height=max(420, 27 * len(trial_df) + 150))
1104
-
1105
- table_cols = [
1106
- "model",
1107
- "dataset",
1108
- "metric",
1109
- "baseline_score",
1110
- "full_model_score",
1111
- "auc",
1112
- "spearman_corr",
1113
- "shap_mean_value",
1114
- "shap_fraction_positive",
1115
- ]
1116
- table_df = nshap[[c for c in table_cols if c in nshap.columns]].copy()
1117
- for col in table_df.columns:
1118
- if col not in {"model", "dataset", "metric"}:
1119
- table_df[col] = table_df[col].map(lambda v: compact_number(v))
1120
- return nshap_fig, tshap_fig, [{"name": c, "id": c} for c in table_df.columns], table_df.to_dict("records")
1121
 
1122
 
1123
- @app.callback(Output("coverage-bars", "figure"), Input("model-filter", "value"))
1124
- def update_coverage(models: list[str] | None):
1125
- df = filter_models(model_inventory, models)
1126
- if df.empty:
1127
- return empty_figure("No models selected.")
1128
- counts = (
1129
- df.groupby(["accelerator", "pipeline"])
1130
- .size()
1131
- .reset_index(name="n_models")
1132
- .sort_values("n_models", ascending=False)
1133
- )
1134
- fig = px.bar(
1135
- counts,
1136
- x="pipeline",
1137
- y="n_models",
1138
- color="accelerator",
1139
- barmode="group",
1140
- text="n_models",
1141
- title="Registered model runtimes",
1142
- )
1143
- fig.update_xaxes(title="")
1144
- fig.update_yaxes(title="models")
1145
- return fig_layout(fig, height=420)
1146
 
1147
 
1148
  if __name__ == "__main__":
 
1
  from __future__ import annotations
2
 
3
+ import re
4
  from pathlib import Path
5
  from typing import Iterable
6
 
7
  import numpy as np
8
  import pandas as pd
 
9
  import plotly.graph_objects as go
10
  from dash import Dash, Input, Output, dash_table, dcc, html
11
  from plotly.subplots import make_subplots
 
13
 
14
  DATA_DIR = Path(__file__).resolve().parent / "data"
15
 
16
+ PAPER_MODEL_ORDER = [
17
+ "blend",
18
+ "blend_ndt",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  "cebra",
20
+ "dnn",
21
  "dpad",
22
+ "gpfa",
23
+ "gru",
24
+ "langevinflow_ccn",
25
  "ldns",
26
  "lfads_torch",
27
+ "lstm",
28
  "marble",
29
+ "mint",
30
+ "neds",
31
+ "neds_pretrained",
32
+ "neuro_behavior_conditioning",
33
+ "pca",
34
  "rnn",
35
+ "smc_rnns",
36
  "svc",
37
+ "tndm",
38
+ "torchdfine",
39
  "xg",
40
  ]
41
 
42
+ DISPLAY_NAMES = {
43
+ "blend": "BLEND-LFADS",
44
+ "blend_ndt": "BLEND-NDT",
45
+ "cebra": "CEBRA",
46
+ "dnn": "DNN",
47
+ "dpad": "DPAD",
48
+ "gpfa": "GPFA",
49
+ "gru": "GRU",
50
+ "langevinflow_ccn": "LangevinFlow",
51
+ "ldns": "LDNS",
52
+ "lfads_torch": "AutoLFADS",
53
+ "lstm": "LSTM",
54
+ "marble": "MARBLE",
55
+ "mint": "MINT",
56
+ "neds": "NEDS",
57
+ "neds_pretrained": "NEDS-pt",
58
+ "neuro_behavior_conditioning": "mVAE",
59
+ "pca": "PCA",
60
+ "rnn": "RNN",
61
+ "smc_rnns": "SMC-RNN",
62
+ "svc": "SVC",
63
+ "tndm": "TNDM",
64
+ "torchdfine": "DFINE",
65
+ "xg": "XGBoost",
66
+ }
67
+
68
+ METHOD_FAMILY = {
69
+ "pca": "Linear latent",
70
+ "gpfa": "Linear latent",
71
+ "dnn": "Supervised decoder",
72
+ "gru": "Supervised decoder",
73
+ "lstm": "Supervised decoder",
74
+ "rnn": "Supervised decoder",
75
+ "svc": "Supervised decoder",
76
+ "xg": "Supervised decoder",
77
+ "lfads_torch": "Sequential latent",
78
+ "dpad": "Sequential latent",
79
+ "torchdfine": "Sequential latent",
80
+ "smc_rnns": "Sequential latent",
81
+ "tndm": "Sequential latent",
82
+ "langevinflow_ccn": "Sequential latent",
83
+ "ldns": "Sequential latent",
84
+ "neuro_behavior_conditioning": "Sequential latent",
85
+ "blend": "Distillation",
86
+ "blend_ndt": "Distillation",
87
+ "neds": "Foundation model",
88
+ "neds_pretrained": "Foundation model",
89
+ "cebra": "Contrastive",
90
+ "marble": "Geometric",
91
+ "mint": "Non-parametric",
92
+ }
93
+
94
+ METHOD_HARDWARE = {
95
+ "gpfa": "CPU",
96
+ "mint": "CPU",
97
+ "pca": "CPU",
98
+ "svc": "CPU",
99
+ "xg": "CPU",
100
+ }
101
+
102
+ TABLE_LABELS = {
103
+ "rank": "Rank",
104
+ "method": "Method",
105
+ "family": "Family",
106
+ "hardware": "Hardware",
107
+ "metric": "Metric",
108
+ "task_score": "Task performance",
109
+ "score": "Task performance",
110
+ "robustness_auc": "Robustness AUC",
111
+ "alignment_score": "Cross-session alignment",
112
+ "training_time_sec": "Training time (s)",
113
+ "inference_time_sec": "Evaluation time (s)",
114
+ "peak_ram_gb": "Peak memory (GB)",
115
+ "peak_vram_gb": "Peak GPU memory (GB)",
116
+ "reference_score": "Reference score",
117
+ "highest_noise_score": "Highest-noise score",
118
+ "average_noisy_score": "Average noisy score",
119
+ "latent_dim": "Latent dimensions",
120
+ "n_sessions": "Sessions",
121
+ "n_pairwise": "Session pairs",
122
+ "baseline_score": "Baseline score",
123
+ "full_model_score": "Full-model score",
124
+ "neuron_influence_auc": "Neuron influence AUC",
125
+ "trial_influence_auc": "Trial influence AUC",
126
+ "shap_mean_value": "Mean signed neuron contribution",
127
+ "shap_fraction_positive": "Fraction positive",
128
+ "n_train_trials": "Training trials",
129
+ "n_test_trials": "Test trials",
130
+ "n_neurons": "Neurons",
131
+ "notes": "Notes",
132
+ }
133
+
134
+ NUMERIC_COLUMNS = {
135
+ "task_score",
136
+ "score",
137
+ "robustness_auc",
138
+ "alignment_score",
139
+ "training_time_sec",
140
+ "inference_time_sec",
141
+ "peak_ram_gb",
142
+ "peak_vram_gb",
143
+ "reference_score",
144
+ "highest_noise_score",
145
+ "average_noisy_score",
146
+ "latent_dim",
147
+ "n_sessions",
148
+ "n_pairwise",
149
+ "baseline_score",
150
+ "full_model_score",
151
+ "neuron_influence_auc",
152
+ "trial_influence_auc",
153
+ "shap_mean_value",
154
+ "shap_fraction_positive",
155
+ "n_train_trials",
156
+ "n_test_trials",
157
+ "n_neurons",
158
+ }
159
+ RIGHT_ALIGNED_COLUMNS = NUMERIC_COLUMNS | {"rank"}
160
+
161
+ SCORE_SCALE = [[0.0, "#9f3a38"], [0.5, "#f3efe4"], [1.0, "#006d77"]]
162
+ CATEGORICAL_PALETTE = [
163
+ "#0072B2",
164
+ "#E69F00",
165
+ "#009E73",
166
+ "#CC79A7",
167
+ "#D55E00",
168
+ "#56B4E9",
169
+ "#F0E442",
170
+ "#6A3D9A",
171
+ "#8C564B",
172
+ "#4E79A7",
173
+ "#59A14F",
174
+ "#AF7AA1",
175
+ ]
176
+
177
 
178
  def load_csv(name: str) -> pd.DataFrame:
179
  path = DATA_DIR / name
180
  if not path.exists():
181
+ raise FileNotFoundError(f"Missing dashboard data: {path}")
182
  return pd.read_csv(path)
183
 
184
 
 
198
  return df[df["status"].fillna("") == "present"].copy()
199
 
200
 
201
+ def active_rows(df: pd.DataFrame) -> pd.DataFrame:
202
+ if df.empty or "is_active_model" not in df.columns:
203
+ return df.copy()
204
+ return df[df["is_active_model"].astype(str).str.lower() == "true"].copy()
205
+
206
+
207
  def ordered_unique(values: Iterable[object]) -> list[str]:
208
  seen: set[str] = set()
209
  out: list[str] = []
 
218
 
219
 
220
  def build_dataset_labels() -> dict[str, str]:
 
 
221
  pairs = (
222
  prediction[["dataset", "dataset_display"]]
223
  .dropna(subset=["dataset"])
 
228
 
229
  DATASET_LABELS = build_dataset_labels()
230
  DATASETS = ordered_unique(prediction.get("dataset", pd.Series(dtype=str)))
231
+ MODEL_SET = set(prediction.get("model", pd.Series(dtype=str)).dropna().astype(str))
232
+ MODELS = [model for model in PAPER_MODEL_ORDER if model in MODEL_SET]
233
+ MODELS += sorted(model for model in MODEL_SET if model not in set(MODELS))
234
+ MODEL_RANK = {model: idx for idx, model in enumerate(MODELS)}
235
 
236
  CONDITION_LABELS = {
237
  "monkey": {
 
261
  }
262
 
263
 
264
+ def model_label(model: object) -> str:
265
+ if pd.isna(model):
266
+ return ""
267
+ text = str(model)
268
+ return DISPLAY_NAMES.get(text, text)
269
+
270
+
271
+ def model_sort_value(model: object) -> int:
272
+ return MODEL_RANK.get(str(model), len(MODEL_RANK))
273
+
274
+
275
+ def hardware_label(model: object) -> str:
276
+ return METHOD_HARDWARE.get(str(model), "GPU")
277
+
278
+
279
+ def selected_models(models: list[str] | None) -> list[str]:
280
+ if not models:
281
+ return MODELS.copy()
282
+ return [model for model in MODELS if model in set(models)]
283
+
284
+
285
+ def filter_models(df: pd.DataFrame, models: list[str] | None) -> pd.DataFrame:
286
+ if df.empty or "model" not in df.columns:
287
+ return df.copy()
288
+ return df[df["model"].astype(str).isin(selected_models(models))].copy()
289
+
290
+
291
  def condition_sort_key(value: object) -> tuple[int, float | str]:
292
  try:
293
  return (0, float(value))
 
311
  return text
312
 
313
 
314
+ def condition_axis_label(dataset: str) -> str:
315
+ return {
316
+ "monkey": "Reach direction",
317
+ "allen_neuropixels": "Orientation",
318
+ "speech": "Cue",
319
+ "ratinabox": "Position bin",
320
+ }.get(dataset, "Condition")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
 
323
+ def session_display_label(dataset: str, session: object) -> str:
324
+ text = "" if pd.isna(session) else str(session)
325
+ if dataset == "monkey":
326
+ match = re.search(r"sub-([A-Za-z])_ses-CO-(\d{4})(\d{2})(\d{2})", text)
327
+ if match:
328
+ monkey, year, month, day = match.groups()
329
+ return f"Monkey {monkey}, {year}-{month}-{day}"
330
+ if dataset == "ratinabox":
331
+ match = re.search(r"s(\d+)", text)
332
+ if match:
333
+ return f"Run {match.group(1)}"
334
+ if dataset == "speech":
335
+ return f"Session {text}"
336
+ if dataset == "allen_neuropixels":
337
+ return f"Session {text}"
338
+ return text or "Session"
339
+
340
+
341
+ def add_method_columns(df: pd.DataFrame) -> pd.DataFrame:
342
+ out = df.copy()
343
+ if "model" in out.columns:
344
+ out["method"] = out["model"].map(model_label)
345
+ out["model_order"] = out["model"].map(model_sort_value)
346
+ return out
347
 
348
 
349
+ def round_numeric(df: pd.DataFrame, columns: Iterable[str], digits: int = 3) -> pd.DataFrame:
350
+ out = df.copy()
351
+ for col in columns:
352
+ if col in out.columns:
353
+ out[col] = pd.to_numeric(out[col], errors="coerce").round(digits)
354
+ return out
 
355
 
356
 
357
+ def records(df: pd.DataFrame) -> list[dict]:
358
+ clean = df.astype(object).where(pd.notna(df), None)
359
+ return clean.to_dict("records")
 
360
 
361
 
362
+ def column_defs(columns: Iterable[str]) -> list[dict]:
363
+ out = []
364
+ for col in columns:
365
+ item = {"name": TABLE_LABELS.get(col, col), "id": col}
366
+ if col in RIGHT_ALIGNED_COLUMNS:
367
+ item["type"] = "numeric"
368
+ out.append(item)
369
+ return out
370
+
371
+
372
+ def metric_text(value: object) -> str:
373
+ if pd.isna(value):
374
+ return "score"
375
+ return str(value)
376
+
377
+
378
+ def value_text(value: object, digits: int = 3) -> str:
379
+ if value is None or pd.isna(value):
380
+ return "Not available"
381
+ number = float(value)
382
+ if abs(number) >= 1000:
383
+ return f"{number:,.0f}"
384
+ return f"{number:.{digits}f}"
385
 
386
 
387
  def fig_layout(fig: go.Figure, *, height: int = 420) -> go.Figure:
388
  fig.update_layout(
389
  height=height,
390
+ paper_bgcolor="#ffffff",
391
+ plot_bgcolor="#ffffff",
392
+ margin=dict(l=22, r=22, t=52, b=38),
393
+ font=dict(family="Inter, Arial, sans-serif", size=13, color="#17202a"),
394
+ title=dict(font=dict(size=15, color="#17202a")),
395
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
396
  )
397
+ fig.update_xaxes(showgrid=True, gridcolor="#edf1f4", zerolinecolor="#d7e0e6")
398
+ fig.update_yaxes(showgrid=True, gridcolor="#edf1f4", zerolinecolor="#d7e0e6")
399
  return fig
400
 
401
 
 
408
  xref="paper",
409
  yref="paper",
410
  showarrow=False,
411
+ font=dict(size=15, color="#637381"),
412
  )
413
  fig.update_xaxes(visible=False)
414
  fig.update_yaxes(visible=False)
415
+ return fig_layout(fig, height=360)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
 
418
  def parse_float_list(value: object) -> list[float]:
 
430
  return out
431
 
432
 
433
+ def dataframe_table(
434
+ table_id: str,
435
+ *,
436
+ page_size: int = 8,
437
+ max_height: str = "520px",
438
+ ) -> dash_table.DataTable:
439
+ return dash_table.DataTable(
440
+ id=table_id,
441
+ columns=[],
442
+ data=[],
443
+ page_size=page_size,
444
+ sort_action="native",
445
+ cell_selectable=True,
446
+ style_as_list_view=True,
447
+ fixed_rows={"headers": True},
448
+ style_table={"overflowX": "auto", "overflowY": "auto", "maxHeight": max_height},
449
+ style_header={
450
+ "backgroundColor": "#f3f6f8",
451
+ "fontWeight": "700",
452
+ "border": "0",
453
+ "borderBottom": "1px solid #cfd8df",
454
+ "color": "#26323f",
455
+ },
456
+ style_cell={
457
+ "fontFamily": "Inter, Arial, sans-serif",
458
+ "fontSize": "13px",
459
+ "padding": "10px 12px",
460
+ "textAlign": "left",
461
+ "minWidth": "90px",
462
+ "maxWidth": "260px",
463
+ "whiteSpace": "normal",
464
+ "height": "auto",
465
+ "border": "0",
466
+ "borderBottom": "1px solid #edf1f4",
467
+ },
468
+ style_cell_conditional=[
469
+ {"if": {"column_id": col}, "textAlign": "right"} for col in RIGHT_ALIGNED_COLUMNS
470
+ ],
471
+ style_data_conditional=[
472
+ {"if": {"row_index": "odd"}, "backgroundColor": "#fbfcfd"},
473
+ {"if": {"state": "active"}, "backgroundColor": "#e5f3f2", "border": "1px solid #4c908b"},
474
+ ],
475
+ )
476
+
477
+
478
+ def panel(title: str, *children, subtitle: str | None = None, className: str = "") -> html.Div:
479
+ heading = [html.H2(title)]
480
+ if subtitle:
481
+ heading.append(html.P(subtitle, className="panel-subtitle"))
482
+ classes = "panel" if not className else f"panel {className}"
483
+ return html.Div([html.Div(heading, className="panel-heading"), *children], className=classes)
484
+
485
+
486
+ def details_table(summary: str, table: dash_table.DataTable) -> html.Details:
487
+ return html.Details(
488
+ [html.Summary(summary), html.Div(table, className="details-body")],
489
+ className="details-table",
490
+ )
491
+
492
+
493
+ def metric_card(label: str, value: str, detail: str | None = None) -> html.Div:
494
+ children = [
495
+ html.Div(label, className="metric-label"),
496
+ html.Div(value, className="metric-value"),
497
+ ]
498
+ if detail:
499
+ children.append(html.Div(detail, className="metric-detail"))
500
+ return html.Div(children, className="metric-card")
501
+
502
 
503
+ def leaderboard_frame(dataset: str, models: list[str] | None) -> pd.DataFrame:
504
  chosen = selected_models(models)
505
+ base = pd.DataFrame({"model": chosen})
506
+ base = add_method_columns(base)
507
+
508
+ pred = filter_models(prediction, chosen)
509
+ pred = pred[pred["dataset"].astype(str) == str(dataset)].copy()
510
+ pred["score"] = pd.to_numeric(pred["score"], errors="coerce")
511
+ pred = pred[["model", "metric", "score", "n_train_trials", "n_test_trials", "n_neurons", "latent_dim"]]
512
+ pred = pred.rename(columns={"score": "task_score"})
513
+
514
+ rob = filter_models(present_rows(robustness), chosen)
515
+ rob = rob[rob["dataset"].astype(str) == str(dataset)].copy()
516
+ rob["raw_auc"] = pd.to_numeric(rob["raw_auc"], errors="coerce")
517
+ rob = rob[["model", "raw_auc"]].rename(columns={"raw_auc": "robustness_auc"})
518
+
519
+ cons = filter_models(active_rows(consistency), chosen)
520
+ cons = cons[cons["dataset"].astype(str) == str(dataset)].copy()
521
+ cons["mean_r2"] = pd.to_numeric(cons["mean_r2"], errors="coerce")
522
+ cons = cons[["model", "mean_r2"]].rename(columns={"mean_r2": "alignment_score"})
523
+
524
+ scale = filter_models(present_rows(scalability), chosen)
525
+ scale = scale[scale["dataset"].astype(str) == str(dataset)].copy()
526
+ scale = scale[["model", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]]
527
+ for col in ["training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]:
528
+ scale[col] = pd.to_numeric(scale[col], errors="coerce")
529
+
530
+ df = base.merge(pred, on="model", how="left")
531
+ df = df.merge(rob, on="model", how="left")
532
+ df = df.merge(cons, on="model", how="left")
533
+ df = df.merge(scale, on="model", how="left")
534
+ df["notes"] = np.where(df["task_score"].isna(), "Not available", "")
535
+
536
+ available = df["task_score"].notna()
537
+ order = df.loc[available].sort_values(
538
+ ["task_score", "model_order"], ascending=[False, True]
539
+ ).index
540
+ df["rank"] = None
541
+ for rank, idx in enumerate(order, start=1):
542
+ df.at[idx, "rank"] = rank
543
+
544
+ df = df.sort_values(
545
+ ["task_score", "model_order"], ascending=[False, True], na_position="last"
546
+ )
547
+ df["id"] = df["model"]
548
+ return round_numeric(df, NUMERIC_COLUMNS)
549
+
550
+
551
+ def leaderboard_summary(dataset: str, table_df: pd.DataFrame) -> list[html.Div]:
552
+ label = DATASET_LABELS.get(dataset, dataset)
553
+ metric = metric_text(table_df["metric"].dropna().iloc[0]) if table_df["metric"].notna().any() else "score"
554
+ available = table_df.dropna(subset=["task_score"]).head(3)
555
+ cards = [metric_card("Dataset", label, f"Primary metric: {metric}")]
556
+ for _, row in available.iterrows():
557
+ cards.append(
558
+ metric_card(
559
+ f"Rank {int(row['rank'])}",
560
+ str(row["method"]),
561
+ f"{value_text(row['task_score'])} {metric}",
562
+ )
563
+ )
564
+ if len(cards) == 1:
565
+ cards.append(metric_card("Top method", "Not available"))
566
+ return cards
567
+
568
+
569
+ def performance_heatmap(dataset: str, models: list[str] | None) -> go.Figure:
570
+ df = filter_models(prediction, models)
571
  if df.empty:
572
+ return empty_figure("No task performance results are available.")
573
+ df = add_method_columns(df)
574
+ df["score"] = pd.to_numeric(df["score"], errors="coerce")
575
+ df["dataset_label"] = df["dataset"].map(DATASET_LABELS).fillna(df["dataset"])
576
+ pivot = df.pivot_table(
577
+ index="method", columns="dataset_label", values="score", aggfunc="first"
578
+ )
579
 
580
+ selected_label = DATASET_LABELS.get(dataset, dataset)
581
+ order_df = leaderboard_frame(dataset, models)
582
+ method_order = [m for m in order_df["method"] if m in set(pivot.index)]
583
+ pivot = pivot.reindex(method_order)
584
+ dataset_order = [DATASET_LABELS.get(ds, ds) for ds in DATASETS]
585
+ pivot = pivot.reindex(columns=[label for label in dataset_order if label in pivot.columns])
586
+ text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.3f}") if not pivot.empty else pivot
587
+
588
+ zmin = min(0.0, float(np.nanmin(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 0.0
589
+ zmax = max(1.0, float(np.nanmax(pivot.values))) if pivot.size and not np.isnan(pivot.values).all() else 1.0
590
+ fig = go.Figure(
591
+ go.Heatmap(
592
+ z=pivot.values if not pivot.empty else [[]],
593
+ x=list(pivot.columns),
594
+ y=list(pivot.index),
595
+ text=text.values if not pivot.empty else [[]],
596
+ texttemplate="%{text}",
597
+ colorscale=SCORE_SCALE,
598
+ zmin=zmin,
599
+ zmax=zmax,
600
+ colorbar=dict(title="Score", thickness=12),
601
+ hovertemplate="Method=%{y}<br>Dataset=%{x}<br>Score=%{z:.4f}<extra></extra>",
602
  )
603
+ )
604
+ fig.update_layout(title=f"Task performance matrix, sorted by {selected_label}")
605
+ return fig_layout(fig, height=max(430, 26 * len(pivot.index) + 150))
606
+
607
+
608
+ def ranking_figure(dataset: str, table_df: pd.DataFrame) -> go.Figure:
609
+ rank_df = table_df.dropna(subset=["task_score"]).sort_values("task_score", ascending=True)
610
+ if rank_df.empty:
611
+ return empty_figure(f"No task performance results are available for {DATASET_LABELS.get(dataset, dataset)}.")
612
+ metric = metric_text(rank_df["metric"].dropna().iloc[0]) if rank_df["metric"].notna().any() else "score"
613
+ fig = go.Figure(
614
+ go.Bar(
615
+ x=rank_df["task_score"],
616
+ y=rank_df["method"],
617
+ orientation="h",
618
+ marker=dict(color="#006d77"),
619
+ hovertemplate="Method=%{y}<br>Score=%{x:.4f}<extra></extra>",
620
+ )
621
+ )
622
+ fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} ranking")
623
+ fig.update_xaxes(title=metric)
624
+ fig.update_yaxes(title="")
625
+ return fig_layout(fig, height=max(420, 25 * len(rank_df) + 150))
626
+
627
+
628
+ def consistency_frame(dataset: str, models: list[str] | None) -> pd.DataFrame:
629
+ chosen = selected_models(models)
630
+ base = add_method_columns(pd.DataFrame({"model": chosen}))
631
+
632
+ cons = filter_models(active_rows(consistency), chosen)
633
+ cons = cons[cons["dataset"].astype(str) == str(dataset)].copy()
634
+ for col in ["mean_r2", "latent_dim", "n_sessions", "n_pairwise"]:
635
+ cons[col] = pd.to_numeric(cons[col], errors="coerce")
636
+ cons = cons[["model", "mean_r2", "latent_dim", "n_sessions", "n_pairwise"]]
637
+ cons = cons.rename(columns={"mean_r2": "alignment_score"})
638
+
639
+ df = base.merge(cons, on="model", how="left")
640
+ df["notes"] = np.where(df["alignment_score"].isna(), "Not available", "")
641
+ available = df["alignment_score"].notna()
642
+ order = df.loc[available].sort_values(
643
+ ["alignment_score", "model_order"], ascending=[False, True]
644
+ ).index
645
+ df["rank"] = None
646
+ for rank, idx in enumerate(order, start=1):
647
+ df.at[idx, "rank"] = rank
648
+ df = df.sort_values(
649
+ ["alignment_score", "model_order"], ascending=[False, True], na_position="last"
650
+ )
651
+ df["id"] = df["model"]
652
+ return round_numeric(df, NUMERIC_COLUMNS)
653
+
654
+
655
+ def selected_consistency_model(df: pd.DataFrame, active_cell: dict | None) -> str | None:
656
+ if active_cell and active_cell.get("row_id") in set(df["model"]):
657
+ return str(active_cell["row_id"])
658
+ available = df.dropna(subset=["alignment_score"])
659
+ if available.empty:
660
+ return None
661
+ return str(available.iloc[0]["model"])
662
+
663
+
664
+ def consistency_bar_figure(dataset: str, df: pd.DataFrame) -> go.Figure:
665
+ bar_df = df.dropna(subset=["alignment_score"]).sort_values("alignment_score", ascending=True)
666
+ if bar_df.empty:
667
+ return empty_figure(f"No cross-session alignment results are available for {DATASET_LABELS.get(dataset, dataset)}.")
668
+ fig = go.Figure(
669
+ go.Bar(
670
+ x=bar_df["alignment_score"],
671
+ y=bar_df["method"],
672
+ orientation="h",
673
+ marker=dict(color="#4c908b"),
674
+ hovertemplate="Method=%{y}<br>Alignment=%{x:.4f}<extra></extra>",
675
+ )
676
+ )
677
+ fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} cross-session alignment")
678
+ fig.update_xaxes(title="Alignment score")
679
+ fig.update_yaxes(title="")
680
+ return fig_layout(fig, height=max(360, 25 * len(bar_df) + 130))
681
+
682
+
683
+ def consistency_heatmap(models: list[str] | None) -> go.Figure:
684
+ df = filter_models(active_rows(consistency), models)
685
+ if df.empty:
686
+ return empty_figure("No cross-session alignment results are available.")
687
+ df = add_method_columns(df)
688
+ df["mean_r2"] = pd.to_numeric(df["mean_r2"], errors="coerce")
689
+ df["dataset_label"] = df["dataset"].map(DATASET_LABELS).fillna(df["dataset"])
690
+ pivot = df.pivot_table(index="method", columns="dataset_label", values="mean_r2", aggfunc="first")
691
+ if not pivot.empty:
692
+ pivot = pivot.loc[pivot.mean(axis=1, skipna=True).sort_values(ascending=False).index]
693
+ text = pivot.map(lambda x: "" if pd.isna(x) else f"{x:.2f}") if not pivot.empty else pivot
694
+ fig = go.Figure(
695
+ go.Heatmap(
696
+ z=pivot.values if not pivot.empty else [[]],
697
+ x=list(pivot.columns),
698
+ y=list(pivot.index),
699
+ text=text.values if not pivot.empty else [[]],
700
+ texttemplate="%{text}",
701
+ colorscale=SCORE_SCALE,
702
+ colorbar=dict(title="Score", thickness=12),
703
+ hovertemplate="Method=%{y}<br>Dataset=%{x}<br>Alignment=%{z:.4f}<extra></extra>",
704
+ )
705
+ )
706
+ fig.update_layout(title="Alignment across datasets")
707
+ return fig_layout(fig, height=max(360, 25 * len(pivot.index) + 135))
708
+
709
+
710
+ def latent_space_figure(dataset: str, model: str | None) -> go.Figure:
711
+ if not model:
712
+ return empty_figure("No latent-space view is available for this selection.")
713
+ if latent_samples.empty:
714
+ return empty_figure("Latent-space samples are unavailable in this view.")
715
+
716
+ plot_df = latent_samples[
717
+ (latent_samples["dataset"].astype(str) == str(dataset))
718
+ & (latent_samples["model"].astype(str) == str(model))
719
+ ].copy()
720
+ if plot_df.empty:
721
+ return empty_figure(f"No latent-space samples are available for {model_label(model)} on {DATASET_LABELS.get(dataset, dataset)}.")
722
 
 
723
  for col in ["x", "y", "z"]:
724
  plot_df[col] = pd.to_numeric(plot_df[col], errors="coerce")
725
  plot_df["condition_num"] = pd.to_numeric(plot_df["condition"], errors="coerce")
726
+ plot_df["condition_label"] = plot_df["condition"].map(lambda value: condition_label(dataset, value))
727
+ plot_df["session_display"] = plot_df["session_label"].map(lambda value: session_display_label(dataset, value))
 
728
  plot_df = plot_df.dropna(subset=["x", "y", "z"])
729
  if plot_df.empty:
730
+ return empty_figure("No latent-space samples match this selection.")
731
 
732
  trajectory_df = latent_trajectories[
733
  (latent_trajectories["dataset"].astype(str) == str(dataset))
 
737
  if col in trajectory_df:
738
  trajectory_df[col] = pd.to_numeric(trajectory_df[col], errors="coerce")
739
  if not trajectory_df.empty:
740
+ trajectory_df["condition_label"] = trajectory_df["condition"].map(lambda value: condition_label(dataset, value))
741
+ trajectory_df["session_display"] = trajectory_df["session_label"].map(lambda value: session_display_label(dataset, value))
 
742
  trajectory_df = trajectory_df.dropna(subset=["x", "y", "z"])
743
 
744
  sessions = ordered_unique(plot_df["session_label"])
745
+ session_titles = [session_display_label(dataset, session) for session in sessions]
746
+ n_cols = 2 if len(sessions) > 1 else 1
747
  n_rows = int(np.ceil(len(sessions) / n_cols))
748
+ specs = [[{"type": "scene"} for _ in range(n_cols)] for _ in range(n_rows)]
749
  fig = make_subplots(
750
  rows=n_rows,
751
  cols=n_cols,
752
+ specs=specs,
753
+ subplot_titles=session_titles,
754
+ horizontal_spacing=0.045,
755
+ vertical_spacing=0.12,
756
  )
757
 
758
+ condition_values = sorted(plot_df["condition"].astype(str).unique(), key=condition_sort_key)
759
+ use_categorical = len(condition_values) <= 12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  condition_colors = {
761
+ condition: CATEGORICAL_PALETTE[idx % len(CATEGORICAL_PALETTE)]
762
  for idx, condition in enumerate(condition_values)
763
  }
764
+ condition_name = condition_axis_label(dataset)
 
 
 
 
 
 
 
 
 
765
 
766
  for session_idx, session in enumerate(sessions):
767
+ session_df = plot_df[plot_df["session_label"].astype(str) == str(session)]
768
  if session_df.empty:
769
  continue
770
  row = session_idx // n_cols + 1
771
  col = session_idx % n_cols + 1
772
+ display_session = session_display_label(dataset, session)
773
 
774
+ if use_categorical:
775
  for condition in condition_values:
776
  cond_df = session_df[session_df["condition"].astype(str) == condition]
777
  if cond_df.empty:
778
  continue
779
  trace_name = condition_label(dataset, condition)
780
  session_traj = trajectory_df[
781
+ (trajectory_df["session_label"].astype(str) == str(session))
782
  & (trajectory_df["condition"].astype(str) == condition)
783
  ].sort_values("time_index")
784
  fig.add_trace(
 
791
  legendgroup=condition,
792
  showlegend=session_idx == 0,
793
  marker=dict(
794
+ size=2.4 if not session_traj.empty else 3.0,
795
+ opacity=0.32 if not session_traj.empty else 0.78,
796
  color=condition_colors[condition],
797
  ),
798
  customdata=np.stack(
799
  [
800
+ np.repeat(display_session, len(cond_df)),
801
  cond_df["condition_label"].astype(str),
802
  cond_df["trial_index"].astype(str),
803
  cond_df["time_index"].astype(str),
 
805
  axis=-1,
806
  ),
807
  hovertemplate=(
808
+ "Session=%{customdata[0]}<br>"
809
+ f"{condition_name}=%{{customdata[1]}}<br>"
810
+ "Trial=%{customdata[2]} time=%{customdata[3]}"
811
  "<extra></extra>"
812
  ),
813
  ),
 
826
  showlegend=False,
827
  line=dict(color=condition_colors[condition], width=5),
828
  hovertemplate=(
829
+ f"{condition_name}={trace_name}<br>"
830
+ "Time=%{customdata}<extra></extra>"
831
  ),
832
  customdata=session_traj["time_index"],
833
  ),
 
841
  y=session_df["y"],
842
  z=session_df["z"],
843
  mode="markers",
844
+ name=display_session,
845
  showlegend=False,
846
  marker=dict(
847
+ size=2.8,
848
  opacity=0.72,
849
  color=session_df["condition_num"],
850
  colorscale="Viridis",
851
  showscale=session_idx == 0,
852
+ colorbar=dict(title=condition_name, thickness=12),
853
  ),
854
  customdata=np.stack(
855
  [
856
+ np.repeat(display_session, len(session_df)),
857
  session_df["condition"].map(lambda value: condition_label(dataset, value)).astype(str),
858
  session_df["trial_index"].astype(str),
859
  session_df["time_index"].astype(str),
 
861
  axis=-1,
862
  ),
863
  hovertemplate=(
864
+ "Session=%{customdata[0]}<br>"
865
+ f"{condition_name}=%{{customdata[1]}}<br>"
866
+ "Trial=%{customdata[2]} time=%{customdata[3]}"
867
  "<extra></extra>"
868
  ),
869
  ),
 
883
  fig.update_layout(
884
  **{
885
  scene_id: dict(
886
+ xaxis=dict(title="", range=[-lim, lim], showgrid=False, zeroline=False, showticklabels=False),
887
+ yaxis=dict(title="", range=[-lim, lim], showgrid=False, zeroline=False, showticklabels=False),
888
+ zaxis=dict(title="", range=[-lim, lim], showgrid=False, zeroline=False, showticklabels=False),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  aspectmode="cube",
890
  bgcolor="#ffffff",
891
  camera=dict(eye=dict(x=1.55, y=1.45, z=1.05)),
892
  )
893
  }
894
  )
895
+
896
+ score_df = active_rows(consistency)
897
+ score_df = score_df[
898
+ (score_df["dataset"].astype(str) == str(dataset))
899
+ & (score_df["model"].astype(str) == str(model))
900
+ ].copy()
901
  score = None
902
+ if not score_df.empty:
903
+ score = pd.to_numeric(score_df["mean_r2"], errors="coerce").dropna()
904
+ score = float(score.iloc[0]) if not score.empty else None
905
+ score_suffix = "" if score is None else f" | alignment {score:.2f}"
906
  fig.update_layout(
907
+ title=f"{model_label(model)} latent space on {DATASET_LABELS.get(dataset, dataset)}{score_suffix}",
908
+ height=760 if n_rows > 1 else 520,
909
+ paper_bgcolor="#ffffff",
910
+ plot_bgcolor="#ffffff",
911
+ margin=dict(l=8, r=8, t=78, b=92),
912
+ font=dict(family="Inter, Arial, sans-serif", size=13, color="#17202a"),
913
+ legend=dict(orientation="h", yanchor="top", y=-0.08, xanchor="left", x=0, title=condition_name),
914
  )
915
+ fig.for_each_annotation(lambda ann: ann.update(font=dict(size=12, color="#526171")))
916
+ return fig
 
917
 
918
 
919
+ def robustness_figure(dataset: str, models: list[str] | None) -> go.Figure:
920
+ df = filter_models(present_rows(robustness), models)
921
+ df = df[df["dataset"].astype(str) == str(dataset)].copy() if not df.empty else df
922
+ if df.empty:
923
+ return empty_figure("No robustness results are available for this selection.")
924
+ df = add_method_columns(df).sort_values("model_order")
925
+ fig = go.Figure()
926
+ for idx, row in enumerate(df.itertuples()):
927
+ xs = parse_float_list(row.noise_levels)
928
+ ys = parse_float_list(row.scores)
929
+ if xs and len(xs) == len(ys):
930
+ fig.add_trace(
931
+ go.Scatter(
932
+ x=xs,
933
+ y=ys,
934
+ mode="lines+markers",
935
+ name=row.method,
936
+ line=dict(color=CATEGORICAL_PALETTE[idx % len(CATEGORICAL_PALETTE)], width=2),
937
+ marker=dict(size=5),
938
+ hovertemplate="Noise=%{x:.2f}<br>Score=%{y:.4f}<extra></extra>",
939
+ )
940
+ )
941
+ fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} robustness curves")
942
+ fig.update_xaxes(title="Noise fraction")
943
+ fig.update_yaxes(title=metric_text(df["metric"].dropna().iloc[0]) if df["metric"].notna().any() else "score")
944
+ return fig_layout(fig, height=520)
945
+
946
+
947
+ def robustness_table_frame(dataset: str, models: list[str] | None) -> pd.DataFrame:
948
+ df = filter_models(present_rows(robustness), models)
949
+ df = df[df["dataset"].astype(str) == str(dataset)].copy() if not df.empty else df
950
+ if df.empty:
951
+ return pd.DataFrame(columns=["method", "metric", "reference_score", "highest_noise_score", "robustness_auc", "average_noisy_score"])
952
+ df = add_method_columns(df)
953
+ df = df.rename(
954
+ columns={
955
+ "score_at_noise0": "reference_score",
956
+ "score_at_max_noise": "highest_noise_score",
957
+ "raw_auc": "robustness_auc",
958
+ "mean_score": "average_noisy_score",
959
+ }
960
  )
961
+ cols = ["method", "metric", "reference_score", "highest_noise_score", "robustness_auc", "average_noisy_score"]
962
+ return round_numeric(df[cols].sort_values("robustness_auc", ascending=False), NUMERIC_COLUMNS)
963
 
964
 
965
+ def compute_figures(dataset: str, models: list[str] | None) -> tuple[go.Figure, go.Figure, pd.DataFrame]:
966
+ df = filter_models(present_rows(scalability), models)
967
+ df = df[df["dataset"].astype(str) == str(dataset)].copy() if not df.empty else df
968
+ if df.empty:
969
+ empty = pd.DataFrame(columns=["method", "hardware", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"])
970
+ return (
971
+ empty_figure("No compute results are available for this selection."),
972
+ empty_figure("No memory results are available for this selection."),
973
+ empty,
974
+ )
975
+
976
+ pred = present_rows(prediction)[["model", "dataset", "score", "metric"]].copy()
977
+ df = df.merge(pred, on=["model", "dataset"], how="left")
978
+ df = add_method_columns(df)
979
+ df["hardware"] = df["model"].map(hardware_label)
980
+ for col in ["training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb", "score"]:
981
+ df[col] = pd.to_numeric(df[col], errors="coerce")
982
+
983
+ scatter = go.Figure(
984
+ go.Scatter(
985
+ x=df["training_time_sec"],
986
+ y=df["score"],
987
+ mode="markers",
988
+ text=df["method"],
989
+ marker=dict(
990
+ size=np.clip(df["peak_ram_gb"].fillna(1.0) * 4, 8, 26),
991
+ color=df["hardware"].map({"CPU": "#8a6f2a", "GPU": "#006d77"}).fillna("#637381"),
992
+ opacity=0.82,
993
+ line=dict(color="#ffffff", width=1),
994
+ ),
995
+ customdata=np.stack(
996
+ [
997
+ df["method"].astype(str),
998
+ df["hardware"].astype(str),
999
+ df["peak_ram_gb"].round(3).astype(str),
1000
+ df["peak_vram_gb"].round(3).astype(str),
1001
+ ],
1002
+ axis=-1,
1003
+ ),
1004
+ hovertemplate=(
1005
+ "Method=%{customdata[0]}<br>"
1006
+ "Hardware=%{customdata[1]}<br>"
1007
+ "Training time=%{x:.3f} s<br>"
1008
+ "Score=%{y:.4f}<br>"
1009
+ "Peak memory=%{customdata[2]} GB<br>"
1010
+ "Peak GPU memory=%{customdata[3]} GB"
1011
+ "<extra></extra>"
1012
+ ),
1013
+ )
1014
  )
1015
+ scatter.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} performance and training time")
1016
+ scatter.update_xaxes(title="Training time (s, log scale)", type="log")
1017
+ scatter.update_yaxes(title=metric_text(df["metric"].dropna().iloc[0]) if df["metric"].notna().any() else "score")
1018
+ fig_layout(scatter, height=500)
1019
 
1020
+ mem_df = df.sort_values("peak_ram_gb", ascending=True)
1021
+ memory = go.Figure()
1022
+ memory.add_trace(go.Bar(x=mem_df["peak_ram_gb"], y=mem_df["method"], orientation="h", name="RAM"))
1023
+ memory.add_trace(go.Bar(x=mem_df["peak_vram_gb"], y=mem_df["method"], orientation="h", name="GPU memory"))
1024
+ memory.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} peak memory", barmode="group")
1025
+ memory.update_xaxes(title="GB")
1026
+ memory.update_yaxes(title="")
1027
+ fig_layout(memory, height=max(420, 26 * len(mem_df) + 150))
1028
 
1029
+ table = df[["method", "hardware", "score", "training_time_sec", "inference_time_sec", "peak_ram_gb", "peak_vram_gb"]]
1030
+ return scatter, memory, round_numeric(table.sort_values("training_time_sec"), NUMERIC_COLUMNS)
 
 
 
1031
 
1032
 
1033
+ def influence_figures(dataset: str, models: list[str] | None) -> tuple[go.Figure, go.Figure, pd.DataFrame]:
1034
+ nshap = filter_models(active_rows(neuron_shap), models)
1035
+ nshap = nshap[nshap["dataset"].astype(str) == str(dataset)].copy() if not nshap.empty else nshap
1036
+ if nshap.empty:
1037
+ neuron_fig = empty_figure("No neuron-influence results are available for this dataset.")
1038
+ table = pd.DataFrame(columns=["method", "metric", "baseline_score", "full_model_score", "neuron_influence_auc", "shap_mean_value", "shap_fraction_positive"])
1039
+ else:
1040
+ nshap = add_method_columns(nshap)
1041
+ nshap["auc"] = pd.to_numeric(nshap["auc"], errors="coerce")
1042
+ bar_df = nshap.dropna(subset=["auc"]).sort_values("auc", ascending=True)
1043
+ neuron_fig = go.Figure(
1044
+ go.Bar(
1045
+ x=bar_df["auc"],
1046
+ y=bar_df["method"],
1047
+ orientation="h",
1048
+ marker=dict(color="#006d77"),
1049
+ hovertemplate="Method=%{y}<br>AUC=%{x:.4f}<extra></extra>",
1050
+ )
1051
+ )
1052
+ neuron_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} neuron influence")
1053
+ neuron_fig.update_xaxes(title="Neuron influence AUC")
1054
+ neuron_fig.update_yaxes(title="")
1055
+ fig_layout(neuron_fig, height=max(420, 25 * len(bar_df) + 150))
1056
+ table = nshap.rename(columns={"auc": "neuron_influence_auc"})
1057
+ table = table[
1058
+ ["method", "metric", "baseline_score", "full_model_score", "neuron_influence_auc", "shap_mean_value", "shap_fraction_positive"]
1059
+ ]
1060
+
1061
+ tshap = filter_models(active_rows(trial_shapley), models)
1062
+ tshap = tshap[tshap["dataset"].astype(str) == str(dataset)].copy() if not tshap.empty else tshap
1063
+ if tshap.empty:
1064
+ trial_fig = empty_figure("No trial-influence results are available for this dataset.")
1065
+ else:
1066
+ tshap = add_method_columns(tshap)
1067
+ tshap["perturbation_auc"] = pd.to_numeric(tshap["perturbation_auc"], errors="coerce")
1068
+ trial_df = tshap.dropna(subset=["perturbation_auc"]).sort_values("perturbation_auc", ascending=True)
1069
+ trial_fig = go.Figure(
1070
+ go.Bar(
1071
+ x=trial_df["perturbation_auc"],
1072
+ y=trial_df["method"],
1073
+ orientation="h",
1074
+ marker=dict(color="#8a6f2a"),
1075
+ hovertemplate="Method=%{y}<br>AUC=%{x:.4f}<extra></extra>",
1076
+ )
1077
+ )
1078
+ trial_fig.update_layout(title=f"{DATASET_LABELS.get(dataset, dataset)} trial influence")
1079
+ trial_fig.update_xaxes(title="Trial influence AUC")
1080
+ trial_fig.update_yaxes(title="")
1081
+ fig_layout(trial_fig, height=max(420, 25 * len(trial_df) + 150))
1082
+
1083
+ return neuron_fig, trial_fig, round_numeric(table, NUMERIC_COLUMNS)
1084
 
1085
 
1086
+ def methods_frame(models: list[str] | None) -> pd.DataFrame:
1087
+ chosen = selected_models(models)
1088
+ rows = []
1089
+ for model in chosen:
1090
+ rows.append(
1091
+ {
1092
+ "id": model,
1093
+ "method": model_label(model),
1094
+ "family": METHOD_FAMILY.get(model, "Model"),
1095
+ "hardware": hardware_label(model),
1096
+ }
1097
+ )
1098
+ return pd.DataFrame(rows).sort_values("method")
1099
+
1100
+
1101
+ app = Dash(__name__, title="Neural Model Benchmark")
1102
  server = app.server
1103
 
1104
  app.layout = html.Div(
 
1108
  html.Div(
1109
  [
1110
  html.Div("Tang Lab", className="eyebrow"),
1111
+ html.H1("Neural Model Benchmark"),
1112
  html.P(
1113
+ "Explore task performance, robustness, latent alignment, neuron and trial influence, and compute cost across 23 benchmarked methods.",
 
1114
  className="lede",
1115
  ),
1116
  ],
 
1118
  ),
1119
  html.Div(
1120
  [
1121
+ metric_card("Methods", str(len(MODELS)), "Benchmarked in the paper"),
1122
+ metric_card("Datasets", str(len(DATASETS)), "Selectable below"),
 
 
 
 
 
 
1123
  ],
1124
+ className="hero-metrics",
1125
  ),
1126
  ],
1127
  className="hero",
 
1133
  html.Label("Dataset"),
1134
  dcc.Dropdown(
1135
  id="dataset-filter",
1136
+ options=[{"label": DATASET_LABELS.get(ds, ds), "value": ds} for ds in DATASETS],
 
 
 
1137
  value=DATASETS[0] if DATASETS else None,
1138
  clearable=False,
1139
  ),
 
1142
  ),
1143
  html.Div(
1144
  [
1145
+ html.Label("Method filter"),
1146
  dcc.Dropdown(
1147
  id="model-filter",
1148
+ options=[{"label": model_label(model), "value": model} for model in MODELS],
1149
+ value=[],
1150
  multi=True,
1151
+ placeholder="All methods",
1152
  ),
1153
  ],
1154
  className="control control-wide",
1155
  ),
1156
  ],
1157
+ className="toolbar",
1158
  ),
1159
  dcc.Tabs(
1160
  id="tabs",
1161
+ value="leaderboard",
1162
  className="tabs",
1163
  children=[
1164
  dcc.Tab(
1165
+ label="Leaderboard",
1166
+ value="leaderboard",
1167
+ className="tab",
1168
+ selected_className="tab tab-selected",
1169
  children=[
1170
  panel(
1171
+ "Leaderboard",
1172
+ html.Div(id="leaderboard-top", className="top-strip"),
1173
+ html.Div(
1174
+ [
1175
+ html.Div(
1176
+ dataframe_table("leaderboard-table", page_size=23, max_height="680px"),
1177
+ className="leaderboard-table-wrap",
1178
+ ),
1179
+ dcc.Graph(id="dataset-ranking", config={"displayModeBar": False}),
1180
+ ],
1181
+ className="leaderboard-grid",
1182
+ ),
1183
+ dcc.Graph(id="performance-heatmap", config={"displayModeBar": False}),
1184
+ subtitle="Sorted by the selected dataset. Regression datasets use R2; classification datasets use accuracy. Higher is better.",
1185
+ className="leaderboard-panel",
1186
  )
1187
  ],
1188
  ),
1189
  dcc.Tab(
1190
+ label="Consistency",
1191
+ value="consistency",
1192
+ className="tab",
1193
+ selected_className="tab tab-selected",
1194
  children=[
1195
  panel(
1196
+ "Cross-session alignment",
1197
+ html.Div(
1198
+ [
1199
+ html.Div(
1200
+ [
1201
+ dataframe_table("consistency-table", page_size=23, max_height="520px"),
1202
+ html.P("Click a method to update the 3D latent view.", className="table-hint"),
1203
+ ],
1204
+ className="consistency-table-wrap",
1205
+ ),
1206
+ dcc.Graph(
1207
+ id="latent-space",
1208
+ config={
1209
+ "displayModeBar": "hover",
1210
+ "toImageButtonOptions": {
1211
+ "format": "png",
1212
+ "filename": "benchdash_latent_space",
1213
+ "height": 900,
1214
+ "width": 1200,
1215
+ "scale": 2,
1216
+ },
1217
+ },
1218
+ ),
1219
+ ],
1220
+ className="latent-grid",
1221
+ ),
1222
+ html.Div(
1223
+ [
1224
+ dcc.Graph(id="consistency-bars", config={"displayModeBar": False}),
1225
+ dcc.Graph(id="consistency-heatmap", config={"displayModeBar": False}),
1226
+ ],
1227
+ className="chart-grid two",
1228
+ ),
1229
+ subtitle="Each latent panel shows one recording session. Colors indicate task condition, stimulus, cue, or spatial bin.",
1230
  )
1231
  ],
1232
  ),
1233
  dcc.Tab(
1234
+ label="Robustness",
1235
+ value="robustness",
1236
+ className="tab",
1237
+ selected_className="tab tab-selected",
1238
  children=[
1239
  panel(
1240
+ "Robustness",
1241
+ dcc.Graph(id="robustness-curve", config={"displayModeBar": False}),
1242
+ details_table("View robustness rows", dataframe_table("robustness-table")),
1243
+ subtitle="Curves show how task performance changes as neural count noise increases.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1244
  )
1245
  ],
1246
  ),
1247
  dcc.Tab(
1248
+ label="Influence",
1249
+ value="influence",
1250
+ className="tab",
1251
+ selected_className="tab tab-selected",
1252
  children=[
1253
  panel(
1254
+ "Neuron and trial influence",
1255
+ html.Div(
1256
+ [
1257
+ dcc.Graph(id="neuron-influence-bars", config={"displayModeBar": False}),
1258
+ dcc.Graph(id="trial-influence-bars", config={"displayModeBar": False}),
1259
+ ],
1260
+ className="chart-grid two",
1261
+ ),
1262
+ details_table("View neuron-influence rows", dataframe_table("influence-table")),
1263
+ subtitle="Signed contribution summaries preserve whether neurons or trials helped or hurt prediction.",
1264
  )
1265
  ],
1266
  ),
1267
  dcc.Tab(
1268
+ label="Compute",
1269
+ value="compute",
1270
+ className="tab",
1271
+ selected_className="tab tab-selected",
1272
  children=[
1273
  panel(
1274
+ "Performance and compute cost",
1275
+ html.Div(
1276
+ [
1277
+ dcc.Graph(id="compute-scatter", config={"displayModeBar": False}),
1278
+ dcc.Graph(id="memory-bars", config={"displayModeBar": False}),
1279
+ ],
1280
+ className="chart-grid two",
1281
+ ),
1282
+ details_table("View compute rows", dataframe_table("compute-table")),
1283
+ subtitle="Shows training time and peak memory for each method on the selected dataset.",
1284
  )
1285
  ],
1286
  ),
1287
  dcc.Tab(
1288
  label="Methods",
1289
+ value="methods",
1290
+ className="tab",
1291
+ selected_className="tab tab-selected",
1292
  children=[
1293
  panel(
1294
+ "Benchmarked methods",
1295
+ dataframe_table("methods-table", page_size=23, max_height="600px"),
1296
+ subtitle="Paper-facing method names and broad method families.",
 
 
 
 
 
 
1297
  )
1298
  ],
1299
  ),
 
1305
 
1306
 
1307
  @app.callback(
1308
+ Output("leaderboard-top", "children"),
1309
+ Output("leaderboard-table", "columns"),
1310
+ Output("leaderboard-table", "data"),
1311
+ Output("dataset-ranking", "figure"),
1312
+ Output("performance-heatmap", "figure"),
1313
  Input("dataset-filter", "value"),
1314
  Input("model-filter", "value"),
1315
  )
1316
+ def update_leaderboard(dataset: str, models: list[str] | None):
1317
+ df = leaderboard_frame(dataset, models)
1318
+ visible_cols = [
1319
+ "rank",
1320
+ "method",
1321
+ "task_score",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1322
  "metric",
1323
+ "robustness_auc",
1324
+ "alignment_score",
1325
+ "training_time_sec",
1326
+ "peak_ram_gb",
1327
+ "notes",
 
1328
  ]
1329
+ table_df = df[[c for c in visible_cols + ["id", "model"] if c in df.columns]]
 
 
 
 
1330
  return (
1331
+ leaderboard_summary(dataset, df),
1332
+ column_defs([c for c in visible_cols if c in table_df.columns]),
1333
+ records(table_df),
1334
+ ranking_figure(dataset, df),
1335
+ performance_heatmap(dataset, models),
1336
  )
1337
 
1338
 
1339
  @app.callback(
1340
+ Output("consistency-table", "columns"),
1341
+ Output("consistency-table", "data"),
1342
+ Output("latent-space", "figure"),
1343
+ Output("consistency-bars", "figure"),
1344
+ Output("consistency-heatmap", "figure"),
1345
  Input("dataset-filter", "value"),
1346
  Input("model-filter", "value"),
1347
+ Input("consistency-table", "active_cell"),
1348
  )
1349
+ def update_consistency(dataset: str, models: list[str] | None, active_cell: dict | None):
1350
+ df = consistency_frame(dataset, models)
1351
+ model = selected_consistency_model(df, active_cell)
1352
+ visible_cols = ["rank", "method", "alignment_score", "n_sessions", "latent_dim", "n_pairwise", "notes"]
1353
+ table_df = df[[c for c in visible_cols + ["id", "model"] if c in df.columns]]
1354
+ return (
1355
+ column_defs([c for c in visible_cols if c in table_df.columns]),
1356
+ records(table_df),
1357
+ latent_space_figure(dataset, model),
1358
+ consistency_bar_figure(dataset, df),
1359
+ consistency_heatmap(models),
1360
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1361
 
1362
 
1363
  @app.callback(
1364
+ Output("robustness-curve", "figure"),
1365
+ Output("robustness-table", "columns"),
1366
+ Output("robustness-table", "data"),
 
 
1367
  Input("dataset-filter", "value"),
1368
  Input("model-filter", "value"),
1369
  )
1370
+ def update_robustness(dataset: str, models: list[str] | None):
1371
+ table = robustness_table_frame(dataset, models)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1372
  return (
1373
+ robustness_figure(dataset, models),
1374
+ column_defs(table.columns),
1375
+ records(table),
 
 
1376
  )
1377
 
1378
 
1379
  @app.callback(
1380
+ Output("compute-scatter", "figure"),
1381
  Output("memory-bars", "figure"),
1382
+ Output("compute-table", "columns"),
1383
+ Output("compute-table", "data"),
1384
  Input("dataset-filter", "value"),
1385
  Input("model-filter", "value"),
1386
  )
1387
+ def update_compute(dataset: str, models: list[str] | None):
1388
+ scatter, memory, table = compute_figures(dataset, models)
1389
+ return scatter, memory, column_defs(table.columns), records(table)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1390
 
1391
 
1392
  @app.callback(
1393
+ Output("neuron-influence-bars", "figure"),
1394
+ Output("trial-influence-bars", "figure"),
1395
+ Output("influence-table", "columns"),
1396
+ Output("influence-table", "data"),
1397
  Input("dataset-filter", "value"),
1398
  Input("model-filter", "value"),
1399
  )
1400
+ def update_influence(dataset: str, models: list[str] | None):
1401
+ neuron_fig, trial_fig, table = influence_figures(dataset, models)
1402
+ return neuron_fig, trial_fig, column_defs(table.columns), records(table)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1403
 
1404
 
1405
+ @app.callback(
1406
+ Output("methods-table", "columns"),
1407
+ Output("methods-table", "data"),
1408
+ Input("model-filter", "value"),
1409
+ )
1410
+ def update_methods(models: list[str] | None):
1411
+ table = methods_frame(models)
1412
+ return column_defs(table.columns), records(table)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1413
 
1414
 
1415
  if __name__ == "__main__":
assets/styles.css CHANGED
@@ -4,25 +4,31 @@
4
 
5
  body {
6
  margin: 0;
7
- background: #f7f8fa;
8
- color: #18212b;
9
  font-family: Inter, Arial, sans-serif;
10
  }
11
 
12
  .app-shell {
13
  min-height: 100vh;
14
- padding: 22px;
 
 
 
 
 
 
 
 
15
  }
16
 
17
  .hero {
18
  display: grid;
19
- grid-template-columns: minmax(340px, 1fr) minmax(420px, 0.9fr);
20
- gap: 28px;
21
  align-items: end;
22
- margin: 0 auto 18px;
23
- max-width: 1500px;
24
- padding: 14px 4px 18px;
25
- border-bottom: 1px solid #dfe6eb;
26
  }
27
 
28
  .hero-copy {
@@ -30,8 +36,8 @@ body {
30
  }
31
 
32
  .eyebrow {
33
- margin-bottom: 8px;
34
- color: #536271;
35
  font-size: 12px;
36
  font-weight: 800;
37
  letter-spacing: 0;
@@ -46,137 +52,222 @@ p {
46
  }
47
 
48
  h1 {
49
- margin-bottom: 10px;
50
- font-size: 44px;
51
  line-height: 1.05;
52
  letter-spacing: 0;
53
  }
54
 
55
  h2 {
56
- margin-bottom: 4px;
57
  font-size: 20px;
58
  letter-spacing: 0;
59
  }
60
 
61
- h3 {
62
- margin-bottom: 8px;
 
 
63
  font-size: 15px;
64
- letter-spacing: 0;
65
  }
66
 
67
- .lede {
68
- max-width: 760px;
69
- margin-bottom: 0;
70
- color: #536271;
71
- font-size: 16px;
72
- line-height: 1.5;
73
  }
74
 
75
- .stat-grid {
76
- display: grid;
77
- grid-template-columns: repeat(4, minmax(110px, 1fr));
78
- gap: 0;
79
- align-items: end;
80
- border: 1px solid #dfe6eb;
81
  border-radius: 8px;
82
  background: #ffffff;
83
  }
84
 
85
- .stat-card {
86
- min-height: 108px;
87
- padding: 18px;
88
- border-right: 1px solid #e6ecef;
89
- }
90
-
91
- .stat-card:last-child {
92
- border-right: 0;
93
- }
94
-
95
- .stat-label {
96
- color: #617282;
97
  font-size: 11px;
98
  font-weight: 800;
99
  letter-spacing: 0;
100
  text-transform: uppercase;
101
  }
102
 
103
- .stat-value {
104
- margin-top: 9px;
105
- color: #111827;
106
- font-size: 28px;
107
  font-weight: 800;
108
- line-height: 1;
109
  }
110
 
111
- .stat-detail {
112
- margin-top: 8px;
113
- color: #657483;
114
  font-size: 12px;
115
  line-height: 1.35;
116
  }
117
 
118
- .controls {
 
 
 
119
  display: grid;
120
- grid-template-columns: minmax(240px, 320px) minmax(320px, 1fr);
121
- gap: 18px;
122
- margin: 0 auto 18px;
123
- max-width: 1500px;
124
- padding: 14px;
125
- border: 1px solid #dfe6eb;
126
  border-radius: 8px;
127
- background: #ffffff;
 
 
128
  }
129
 
130
  .control label {
131
  display: block;
132
- margin-bottom: 7px;
133
- color: #394756;
134
  font-size: 12px;
135
  font-weight: 800;
136
  letter-spacing: 0;
137
  text-transform: uppercase;
138
  }
139
 
 
 
 
 
 
 
 
 
 
140
  .tabs {
141
- margin: 0 auto;
142
- max-width: 1500px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
  .panel {
146
  margin-top: 14px;
147
  padding: 18px;
148
- border: 1px solid #dfe6eb;
149
  border-radius: 8px;
150
  background: #ffffff;
151
  }
152
 
153
  .panel-heading {
154
- margin-bottom: 12px;
155
  }
156
 
157
  .panel-subtitle {
 
158
  margin-bottom: 0;
159
- color: #5b6b7b;
160
  font-size: 13px;
161
  line-height: 1.45;
162
  }
163
 
164
- @media (max-width: 1050px) {
165
- .hero,
166
- .controls {
167
- grid-template-columns: 1fr;
168
- }
169
 
170
- .stat-grid {
171
- grid-template-columns: repeat(2, minmax(120px, 1fr));
172
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- .stat-card:nth-child(2) {
175
- border-right: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  }
177
 
178
- .stat-card:nth-child(-n + 2) {
179
- border-bottom: 1px solid #e6ecef;
180
  }
181
  }
182
 
@@ -186,20 +277,16 @@ h3 {
186
  }
187
 
188
  h1 {
189
- font-size: 34px;
190
- }
191
-
192
- .stat-grid {
193
- grid-template-columns: 1fr;
194
  }
195
 
196
- .stat-card,
197
- .stat-card:nth-child(2) {
198
- border-right: 0;
199
- border-bottom: 1px solid #e6ecef;
200
  }
201
 
202
- .stat-card:last-child {
203
- border-bottom: 0;
 
 
204
  }
205
  }
 
4
 
5
  body {
6
  margin: 0;
7
+ background: #f4f6f7;
8
+ color: #17202a;
9
  font-family: Inter, Arial, sans-serif;
10
  }
11
 
12
  .app-shell {
13
  min-height: 100vh;
14
+ padding: 18px 22px 28px;
15
+ }
16
+
17
+ .hero,
18
+ .toolbar,
19
+ .tabs {
20
+ max-width: 1560px;
21
+ margin-left: auto;
22
+ margin-right: auto;
23
  }
24
 
25
  .hero {
26
  display: grid;
27
+ grid-template-columns: minmax(360px, 1fr) auto;
28
+ gap: 26px;
29
  align-items: end;
30
+ padding: 8px 2px 14px;
31
+ border-bottom: 1px solid #d8e1e7;
 
 
32
  }
33
 
34
  .hero-copy {
 
36
  }
37
 
38
  .eyebrow {
39
+ margin-bottom: 7px;
40
+ color: #526171;
41
  font-size: 12px;
42
  font-weight: 800;
43
  letter-spacing: 0;
 
52
  }
53
 
54
  h1 {
55
+ margin-bottom: 8px;
56
+ font-size: 40px;
57
  line-height: 1.05;
58
  letter-spacing: 0;
59
  }
60
 
61
  h2 {
62
+ margin-bottom: 5px;
63
  font-size: 20px;
64
  letter-spacing: 0;
65
  }
66
 
67
+ .lede {
68
+ max-width: 840px;
69
+ margin-bottom: 0;
70
+ color: #526171;
71
  font-size: 15px;
72
+ line-height: 1.45;
73
  }
74
 
75
+ .hero-metrics,
76
+ .top-strip {
77
+ display: flex;
78
+ gap: 10px;
79
+ align-items: stretch;
80
+ flex-wrap: wrap;
81
  }
82
 
83
+ .metric-card {
84
+ min-width: 145px;
85
+ padding: 12px 14px;
86
+ border: 1px solid #d8e1e7;
 
 
87
  border-radius: 8px;
88
  background: #ffffff;
89
  }
90
 
91
+ .metric-label {
92
+ color: #607080;
 
 
 
 
 
 
 
 
 
 
93
  font-size: 11px;
94
  font-weight: 800;
95
  letter-spacing: 0;
96
  text-transform: uppercase;
97
  }
98
 
99
+ .metric-value {
100
+ margin-top: 7px;
101
+ color: #101820;
102
+ font-size: 22px;
103
  font-weight: 800;
104
+ line-height: 1.1;
105
  }
106
 
107
+ .metric-detail {
108
+ margin-top: 6px;
109
+ color: #647383;
110
  font-size: 12px;
111
  line-height: 1.35;
112
  }
113
 
114
+ .toolbar {
115
+ position: sticky;
116
+ top: 0;
117
+ z-index: 10;
118
  display: grid;
119
+ grid-template-columns: minmax(230px, 310px) minmax(360px, 1fr);
120
+ gap: 16px;
121
+ margin-top: 14px;
122
+ margin-bottom: 14px;
123
+ padding: 12px;
124
+ border: 1px solid #d8e1e7;
125
  border-radius: 8px;
126
+ background: rgba(255, 255, 255, 0.96);
127
+ box-shadow: 0 8px 24px rgba(18, 28, 38, 0.06);
128
+ backdrop-filter: blur(6px);
129
  }
130
 
131
  .control label {
132
  display: block;
133
+ margin-bottom: 6px;
134
+ color: #344452;
135
  font-size: 12px;
136
  font-weight: 800;
137
  letter-spacing: 0;
138
  text-transform: uppercase;
139
  }
140
 
141
+ .Select--multi .Select-value {
142
+ margin-top: 3px;
143
+ }
144
+
145
+ .Select-multi-value-wrapper {
146
+ max-height: 86px;
147
+ overflow-y: auto;
148
+ }
149
+
150
  .tabs {
151
+ border: 0;
152
+ }
153
+
154
+ .tab {
155
+ padding: 11px 18px !important;
156
+ border: 0 !important;
157
+ border-bottom: 2px solid transparent !important;
158
+ background: transparent !important;
159
+ color: #637381 !important;
160
+ font-size: 14px;
161
+ font-weight: 700;
162
+ }
163
+
164
+ .tab-selected {
165
+ color: #12302f !important;
166
+ border-bottom-color: #006d77 !important;
167
+ background: #ffffff !important;
168
  }
169
 
170
  .panel {
171
  margin-top: 14px;
172
  padding: 18px;
173
+ border: 1px solid #d8e1e7;
174
  border-radius: 8px;
175
  background: #ffffff;
176
  }
177
 
178
  .panel-heading {
179
+ margin-bottom: 14px;
180
  }
181
 
182
  .panel-subtitle {
183
+ max-width: 980px;
184
  margin-bottom: 0;
185
+ color: #526171;
186
  font-size: 13px;
187
  line-height: 1.45;
188
  }
189
 
190
+ .leaderboard-panel {
191
+ padding-top: 16px;
192
+ }
 
 
193
 
194
+ .top-strip {
195
+ margin-bottom: 14px;
196
+ }
197
+
198
+ .leaderboard-grid {
199
+ display: grid;
200
+ grid-template-columns: minmax(560px, 1.08fr) minmax(360px, 0.92fr);
201
+ gap: 16px;
202
+ align-items: start;
203
+ }
204
+
205
+ .leaderboard-table-wrap,
206
+ .consistency-table-wrap {
207
+ min-width: 0;
208
+ }
209
+
210
+ .latent-grid {
211
+ display: grid;
212
+ grid-template-columns: minmax(360px, 0.42fr) minmax(620px, 1fr);
213
+ gap: 16px;
214
+ align-items: start;
215
+ }
216
 
217
+ .chart-grid {
218
+ display: grid;
219
+ gap: 16px;
220
+ align-items: start;
221
+ }
222
+
223
+ .chart-grid.two {
224
+ grid-template-columns: repeat(2, minmax(320px, 1fr));
225
+ }
226
+
227
+ .table-hint {
228
+ margin: 10px 2px 0;
229
+ color: #647383;
230
+ font-size: 12px;
231
+ }
232
+
233
+ .details-table {
234
+ margin-top: 12px;
235
+ border-top: 1px solid #e5ebef;
236
+ }
237
+
238
+ .details-table summary {
239
+ cursor: pointer;
240
+ padding: 12px 0 8px;
241
+ color: #344452;
242
+ font-size: 13px;
243
+ font-weight: 800;
244
+ }
245
+
246
+ .details-body {
247
+ padding-top: 4px;
248
+ }
249
+
250
+ .dash-table-container .dash-spreadsheet-container .dash-spreadsheet-inner th {
251
+ background: #f3f6f8 !important;
252
+ }
253
+
254
+ .dash-table-container .previous-next-container {
255
+ margin-top: 8px;
256
+ color: #526171;
257
+ font-size: 12px;
258
+ }
259
+
260
+ @media (max-width: 1180px) {
261
+ .hero,
262
+ .toolbar,
263
+ .leaderboard-grid,
264
+ .latent-grid,
265
+ .chart-grid.two {
266
+ grid-template-columns: 1fr;
267
  }
268
 
269
+ .hero-metrics {
270
+ justify-content: flex-start;
271
  }
272
  }
273
 
 
277
  }
278
 
279
  h1 {
280
+ font-size: 32px;
 
 
 
 
281
  }
282
 
283
+ .panel {
284
+ padding: 14px;
 
 
285
  }
286
 
287
+ .tab {
288
+ padding-left: 10px !important;
289
+ padding-right: 10px !important;
290
+ font-size: 13px;
291
  }
292
  }