whassan commited on
Commit
a2bbd94
·
verified ·
1 Parent(s): d49f9e6

Deploy Sensorflow Studio

Browse files
Files changed (40) hide show
  1. .gitignore +1 -0
  2. ARCHITECTURE.md +78 -0
  3. app_backend.py +4 -0
  4. sensorflow/evaluation/api.py +13 -0
  5. sensorflow/megaeval/__init__.py +16 -0
  6. sensorflow/megaeval/analysis.py +314 -0
  7. sensorflow/megaeval/api.py +387 -0
  8. sensorflow/megaeval/cube.py +270 -0
  9. sensorflow/megaeval/errors.py +106 -0
  10. sensorflow/megaeval/population.py +281 -0
  11. sensorflow/megaeval/runs.py +704 -0
  12. sensorflow/megaeval/sampling.py +261 -0
  13. sensorflow/megaeval/sketches.py +173 -0
  14. src/App.tsx +6 -1
  15. src/components/megaeval/CohortTab.tsx +266 -0
  16. src/components/megaeval/CompareTab.tsx +221 -0
  17. src/components/megaeval/ContainersTab.tsx +321 -0
  18. src/components/megaeval/HeaderBar.tsx +335 -0
  19. src/components/megaeval/HeroRow.tsx +117 -0
  20. src/components/megaeval/InvestigationTab.tsx +266 -0
  21. src/components/megaeval/LineageCard.tsx +100 -0
  22. src/components/megaeval/QualityTab.tsx +358 -0
  23. src/components/megaeval/ReviewTab.tsx +205 -0
  24. src/components/megaeval/RunProgressCard.tsx +63 -0
  25. src/components/megaeval/ShiftTab.tsx +96 -0
  26. src/components/megaeval/shared.tsx +304 -0
  27. src/context/LabelEvalContext.tsx +2 -0
  28. src/pages/CommandCenterPage.tsx +325 -0
  29. src/services/labeleval.ts +9 -0
  30. src/services/megaeval.ts +152 -0
  31. src/types/labeleval.ts +4 -0
  32. src/types/megaeval.ts +536 -0
  33. tests/test_megaeval/__init__.py +0 -0
  34. tests/test_megaeval/conftest.py +36 -0
  35. tests/test_megaeval/test_analysis.py +76 -0
  36. tests/test_megaeval/test_api_lifecycle.py +190 -0
  37. tests/test_megaeval/test_cube.py +109 -0
  38. tests/test_megaeval/test_error_index.py +64 -0
  39. tests/test_megaeval/test_sampling.py +64 -0
  40. tests/test_megaeval/test_sketches.py +70 -0
.gitignore CHANGED
@@ -64,6 +64,7 @@ temp/
64
 
65
  # Generated runtime/test artifacts
66
  runs/labeleval/
 
67
  runs/pipeline/frame_test/
68
  runs/pipeline/qg_test/
69
  runs/pipeline/roundtrip/
 
64
 
65
  # Generated runtime/test artifacts
66
  runs/labeleval/
67
+ runs/megaeval/
68
  runs/pipeline/frame_test/
69
  runs/pipeline/qg_test/
70
  runs/pipeline/roundtrip/
ARCHITECTURE.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sensorflow Studio — Aggregate-First Evaluation Architecture (megaeval)
2
+
3
+ The `sensorflow/megaeval/` layer replaces the "evaluation-record browser" model
4
+ (annotation as the primary unit) with an **aggregate-first, risk-first,
5
+ cohort-first, drill-down-on-demand** design.
6
+
7
+ ## Mental model → code
8
+
9
+ | Concept | Meaning | Module / storage |
10
+ | --- | --- | --- |
11
+ | Dataset | immutable source population | `population.py` → `runs/megaeval/populations/{pop}/part-*.npz` |
12
+ | Evaluation Run | model+config being evaluated (async job, first-class) | `runs.py` → `runs/megaeval/runs/{run}/run.json` |
13
+ | Population | all objects evaluated in a run | headline row of the metric cube |
14
+ | Cohort | slice by class/weather/lighting/road/scenario/sensor/distance/speed/occlusion | cube cells + `POST /api/evaluations/query` |
15
+ | Container | scene/frame/segment holding related objects | `containers.npz` aggregate table |
16
+ | Annotation | individual object — forensic view only | per-object `objects-part-*.npz`, loaded one partition at a time |
17
+
18
+ ## Data flow
19
+
20
+ ```
21
+ generate population (vectorized numpy, partitioned npz)
22
+ │
23
+ create run (queued) ──► worker pool (4 threads) processes partitions:
24
+ │ model simulation → PARTIAL sufficient statistics
25
+ │ (n, tp, fp, fn, sum_iou, conf sums, histograms …)
26
+ │ grouped by the 9 cube dimensions, per partition
27
+ ▼
28
+ reduce (concat + groupby-sum) ──► materialize METRIC CUBE (cube.npz)
29
+ │ + error index, container table, sketches,
30
+ │ container embeddings
31
+ ▼
32
+ publish ──► queries served by QueryRouter: cache → cube → (rare) record scan
33
+ ```
34
+
35
+ Aggregation **never** scans raw records at query time; queries like
36
+ "pedestrian recall at night in rain for model v42" are cube lookups.
37
+
38
+ ## Single-node stand-ins for distributed infrastructure
39
+
40
+ This repo runs on one laptop; the seams are shaped so each piece maps to the
41
+ production-scale equivalent:
42
+
43
+ | Here (single node) | Production analogue |
44
+ | --- | --- |
45
+ | npz column partitions (`part-0000.npz…`, hash-partitioned by container) | Parquet files in an Iceberg/Delta table, partitioned by scene |
46
+ | `ThreadPoolExecutor` workers over partitions | Spark executors / Flink task slots |
47
+ | partial stats per partition + `reduce_partials` | map-side combine + shuffle reduce |
48
+ | `QueryRouter` (cache → cube → scan) with latency+source reporting | OLAP engine (Druid/Pinot/ClickHouse) over the lakehouse |
49
+ | `QueryCache` keyed by hash(dataset+model+filters+group_by+metrics) | result cache / materialized views |
50
+ | in-process SSE progress events | job-status topic (Kafka) + progress service |
51
+ | numpy cosine similarity over container embeddings | vector DB (hybrid retrieval) |
52
+
53
+ ## Honesty rules encoded in the API
54
+
55
+ - Cube counts and derived ratios are **exact** (`meta.exact = true`).
56
+ - Percentiles/distributions come from fixed-bin quantile histograms;
57
+ cardinalities from HyperLogLog — always labeled approximate.
58
+ - Precision/recall from human review are **sampling estimates with 95% CIs**
59
+ (stratified risk-weighted design: Wilson per stratum, weighted normal-approx
60
+ combination), reported with n reviewed / frame size / method.
61
+ - Every query response reports `source` (cache/cube/scan), `cache_hit`,
62
+ and `latency_ms` so aggregate-first behavior is observable in the UI.
63
+ - Evaluation runs are reproducible: lineage (dataset, model+checkpoint, label
64
+ version, evaluator code version, metric version, thresholds, sampling config,
65
+ seed, hardware) is stored on the run and derives the deterministic seed.
66
+
67
+ ## Performance SLOs (measured locally, 320k-object population, 67k cube cells)
68
+
69
+ | Query | SLO | Measured |
70
+ | --- | --- | --- |
71
+ | aggregate dashboard (group by class) | < 2 s | ~75 ms |
72
+ | filtered cohort query | < 3 s | ~350 ms first / sub-ms cached |
73
+ | cached query | < 100 ms | ~0.1 ms |
74
+ | object investigation (one container, forensic) | < 1 s | ~50–200 ms |
75
+
76
+ The legacy `sensorflow/evaluation/` layer (label-evaluation pipeline, triage,
77
+ HITL, training flywheel) remains unchanged; megaeval integrates with its
78
+ regression tracker and alerting rather than duplicating them.
app_backend.py CHANGED
@@ -1463,6 +1463,10 @@ def get_mitl_queue(sequence_id: str = "seq_001"):
1463
  from sensorflow.evaluation.api import router as labeleval_router
1464
  app.include_router(labeleval_router)
1465
 
 
 
 
 
1466
  # Mount static folder
1467
  static_dir = Path(__file__).parent / "static"
1468
  if static_dir.exists():
 
1463
  from sensorflow.evaluation.api import router as labeleval_router
1464
  app.include_router(labeleval_router)
1465
 
1466
+ # Aggregate-first mega-scale evaluation layer (metric cube, async runs, query API).
1467
+ from sensorflow.megaeval.api import router as megaeval_router
1468
+ app.include_router(megaeval_router)
1469
+
1470
  # Mount static folder
1471
  static_dir = Path(__file__).parent / "static"
1472
  if static_dir.exists():
sensorflow/evaluation/api.py CHANGED
@@ -564,6 +564,18 @@ def copilot_explain_evidence(req: CopilotRequest):
564
  # ------------------------------------------------------------------ SSE stream
565
 
566
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  @router.get("/events/stream")
568
  async def events_stream(ticks: int = 600):
569
  async def gen():
@@ -573,6 +585,7 @@ async def events_stream(ticks: int = 600):
573
  "pipeline": _pipeline_state(),
574
  "training": _active_training_job(),
575
  "alerts_count": len(get_store().all("alerts")),
 
576
  }
577
  yield f"data: {json.dumps(payload)}\n\n"
578
  await asyncio.sleep(1.0)
 
564
  # ------------------------------------------------------------------ SSE stream
565
 
566
 
567
+ def _megaeval_status() -> Dict:
568
+ try:
569
+ from sensorflow.megaeval.runs import get_mega_store
570
+ mstore = get_mega_store()
571
+ active = [r.progress_dict() for r in mstore.runs.values()
572
+ if r.status in ("queued", "running", "reducing", "materializing")]
573
+ return {"active_runs": active, "cache": mstore.router.cache.stats(),
574
+ "total_runs": len(mstore.runs)}
575
+ except Exception:
576
+ return {"active_runs": [], "cache": None, "total_runs": 0}
577
+
578
+
579
  @router.get("/events/stream")
580
  async def events_stream(ticks: int = 600):
581
  async def gen():
 
585
  "pipeline": _pipeline_state(),
586
  "training": _active_training_job(),
587
  "alerts_count": len(get_store().all("alerts")),
588
+ "megaeval": _megaeval_status(),
589
  }
590
  yield f"data: {json.dumps(payload)}\n\n"
591
  await asyncio.sleep(1.0)
sensorflow/megaeval/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """megaeval — aggregate-first, risk-first evaluation layer for mega-scale populations.
2
+
3
+ Mental model (maps 1:1 to modules):
4
+
5
+ Dataset (immutable population) -> population.py (partitioned npz storage)
6
+ Evaluation Run (first-class job) -> runs.py (async workers -> partial stats -> reduce)
7
+ Population / Cohort -> cube.py (metric cube + query router + cache)
8
+ Container -> runs.py (container-level aggregate table)
9
+ Annotation (forensic drill-down) -> runs.py (per-object partitions, loaded on demand)
10
+
11
+ Single-node stand-ins for distributed infra (see ARCHITECTURE.md):
12
+ npz partitions ~ Parquet files in an Iceberg table
13
+ worker thread pool ~ Spark executors / Flink task slots
14
+ partial-stats+reduce~ map-side combine + shuffle reduce
15
+ QueryRouter ~ OLAP engine (Druid/Pinot) in front of the lakehouse
16
+ """
sensorflow/megaeval/analysis.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cross-run analysis: model-vs-model comparison, distribution shift, the "why"
2
+ layer, and hybrid similarity search.
3
+
4
+ All computations run on cubes and the error index — never on raw records.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from typing import Dict, List, Optional
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ from sensorflow.megaeval import cube as cube_mod
17
+ from sensorflow.megaeval import population as pop_mod
18
+ from sensorflow.megaeval.population import DIMENSIONS, DIM_NAMES
19
+ from sensorflow.megaeval.runs import ERROR_TYPES, EvaluationRun, MegaStore, run_dir
20
+
21
+ # ------------------------------------------------------------------ model vs model
22
+
23
+ DEFAULT_PROMOTION_POLICY = {
24
+ "max_recall_drop": 0.010,
25
+ "max_precision_drop": 0.015,
26
+ "max_safety_recall_drop": 0.005,
27
+ "max_cohort_recall_drop": 0.050,
28
+ "min_cohort_support": 200,
29
+ "cohort_dims": ["road_type", "lighting", "class"],
30
+ }
31
+
32
+ _HEADLINE_METRICS = ["precision", "recall", "f1", "mean_iou", "safety_recall", "anomaly_rate"]
33
+
34
+
35
+ def compare_runs(store: MegaStore, candidate: EvaluationRun, baseline: EvaluationRun,
36
+ policy: Optional[Dict] = None) -> Dict:
37
+ policy = {**DEFAULT_PROMOTION_POLICY, **(policy or {})}
38
+ cand_art, base_art = store.artifacts(candidate.run_id), store.artifacts(baseline.run_id)
39
+ cand_cube, base_cube = cand_art["cube"], base_art["cube"]
40
+
41
+ headline = []
42
+ for m in _HEADLINE_METRICS:
43
+ c, b = candidate.headline.get(m), baseline.headline.get(m)
44
+ if c is None or b is None:
45
+ continue
46
+ headline.append({"metric": m, "baseline": round(b, 4), "candidate": round(c, 4),
47
+ "delta": round(c - b, 4)})
48
+
49
+ per_class = []
50
+ for cls in DIMENSIONS["class"]:
51
+ c, b = candidate.per_class.get(cls), baseline.per_class.get(cls)
52
+ if not c or not b:
53
+ continue
54
+ per_class.append({
55
+ "class": cls, "n": c.get("n"),
56
+ "recall_baseline": b.get("recall"), "recall_candidate": c.get("recall"),
57
+ "recall_delta": round((c.get("recall") or 0) - (b.get("recall") or 0), 4),
58
+ "precision_baseline": b.get("precision"), "precision_candidate": c.get("precision"),
59
+ "precision_delta": round((c.get("precision") or 0) - (b.get("precision") or 0), 4),
60
+ })
61
+
62
+ dims = policy["cohort_dims"]
63
+ cand_rows, _ = cube_mod.aggregate(cand_cube, None, dims, ["n", "recall", "precision"], 2000)
64
+ base_rows, _ = cube_mod.aggregate(base_cube, None, dims, ["n", "recall", "precision"], 2000)
65
+ base_map = {tuple(r[d] for d in dims): r for r in base_rows}
66
+ cohorts = []
67
+ for r in cand_rows:
68
+ key = tuple(r[d] for d in dims)
69
+ b = base_map.get(key)
70
+ if not b or r["n"] < policy["min_cohort_support"]:
71
+ continue
72
+ if r.get("recall") is None or b.get("recall") is None:
73
+ continue
74
+ cohorts.append({
75
+ "cohort": "/".join(key), **{d: r[d] for d in dims}, "n": r["n"],
76
+ "recall_baseline": b["recall"], "recall_candidate": r["recall"],
77
+ "recall_delta": round(r["recall"] - b["recall"], 4),
78
+ })
79
+ cohorts.sort(key=lambda x: x["recall_delta"])
80
+ regressions = [c for c in cohorts if c["recall_delta"] < -policy["max_cohort_recall_drop"]]
81
+
82
+ blockers: List[str] = []
83
+ hd = {h["metric"]: h["delta"] for h in headline}
84
+ if hd.get("recall", 0) < -policy["max_recall_drop"]:
85
+ blockers.append(f"headline recall dropped {hd['recall']:+.4f} "
86
+ f"(policy allows -{policy['max_recall_drop']})")
87
+ if hd.get("precision", 0) < -policy["max_precision_drop"]:
88
+ blockers.append(f"headline precision dropped {hd['precision']:+.4f}")
89
+ if hd.get("safety_recall", 0) < -policy["max_safety_recall_drop"]:
90
+ blockers.append(f"safety recall dropped {hd['safety_recall']:+.4f} "
91
+ f"(policy allows -{policy['max_safety_recall_drop']})")
92
+ for c in regressions[:5]:
93
+ blockers.append(
94
+ f"REGRESSION: {c['cohort']} recall "
95
+ f"{c['recall_baseline']*100:.1f}% -> {c['recall_candidate']*100:.1f}% "
96
+ f"(Δ {c['recall_delta']*100:+.1f})")
97
+
98
+ # integrate with the existing regression tracker so the Regression page shows it
99
+ try:
100
+ from sensorflow.evaluation import regression as legacy_regression
101
+ from sensorflow.evaluation.records import get_store as get_legacy_store
102
+ legacy_regression.compare_runs(
103
+ get_legacy_store(),
104
+ current_metrics={"precision": candidate.headline.get("precision"),
105
+ "recall": candidate.headline.get("recall"),
106
+ "safety_critical_recall": candidate.headline.get("safety_recall"),
107
+ "mean_iou_3d": candidate.headline.get("mean_iou")},
108
+ model_version=candidate.model_version,
109
+ dataset_version=candidate.population_id,
110
+ run_id=candidate.run_id,
111
+ baseline_metrics={"precision": baseline.headline.get("precision"),
112
+ "recall": baseline.headline.get("recall"),
113
+ "safety_critical_recall": baseline.headline.get("safety_recall"),
114
+ "mean_iou_3d": baseline.headline.get("mean_iou")},
115
+ baseline_version=baseline.model_version,
116
+ affected_classes=[c["class"] for c in per_class if c["recall_delta"] < -0.03],
117
+ affected_scenarios=sorted({c.get("road_type", "") for c in regressions[:5] if c}),
118
+ )
119
+ except Exception:
120
+ pass
121
+
122
+ return {
123
+ "candidate": {"run_id": candidate.run_id, "model_version": candidate.model_version},
124
+ "baseline": {"run_id": baseline.run_id, "model_version": baseline.model_version},
125
+ "headline_deltas": headline,
126
+ "per_class": per_class,
127
+ "worst_cohorts": cohorts[:15],
128
+ "regressions": regressions[:10],
129
+ "policy": policy,
130
+ "recommendation": "DO_NOT_PROMOTE" if blockers else "PROMOTE",
131
+ "blockers": blockers,
132
+ }
133
+
134
+
135
+ # ------------------------------------------------------------------ distribution shift
136
+
137
+
138
+ def distribution_shift(store: MegaStore, run: EvaluationRun,
139
+ min_eval_count: int = 300, rel_threshold: float = 0.35) -> Dict:
140
+ cache_path = os.path.join(run_dir(run.run_id), "shift.json")
141
+ if os.path.exists(cache_path):
142
+ with open(cache_path) as f:
143
+ return json.load(f)
144
+
145
+ meta = pop_mod.load_meta(run.population_id)
146
+ cube_df = store.artifacts(run.run_id)["cube"]
147
+ overall_rows, _ = cube_mod.aggregate(cube_df, None, None, ["recall"], 1)
148
+ overall_recall = overall_rows[0].get("recall")
149
+
150
+ shifts = []
151
+ for row in meta.get("train_mix", []):
152
+ train, evals = row["train_share"], row["eval_share"]
153
+ if train <= 0 or row["eval_count"] < min_eval_count:
154
+ continue
155
+ rel = (evals - train) / train
156
+ if abs(rel) < rel_threshold:
157
+ continue
158
+ filt = {"class": [row["class"]], "weather": [row["weather"]],
159
+ "lighting": [row["lighting"]]}
160
+ crows, _ = cube_mod.aggregate(cube_df, filt, None, ["n", "recall", "precision"], 1)
161
+ c = crows[0] if crows else {}
162
+ shifts.append({
163
+ "cohort": f"{row['class']}/{row['weather']}/{row['lighting']}",
164
+ "class": row["class"], "weather": row["weather"], "lighting": row["lighting"],
165
+ "train_share": round(train, 5), "eval_share": round(evals, 5),
166
+ "relative_change": round(rel, 4),
167
+ "eval_count": row["eval_count"],
168
+ "cohort_recall": c.get("recall"),
169
+ "overall_recall": overall_recall,
170
+ "recall_gap": (round(c["recall"] - overall_recall, 4)
171
+ if c.get("recall") is not None and overall_recall is not None else None),
172
+ })
173
+ shifts.sort(key=lambda s: abs(s["relative_change"]) * (1 + abs(s.get("recall_gap") or 0) * 5),
174
+ reverse=True)
175
+
176
+ # surface the worst shifts as alerts in the existing alerting system (once per run)
177
+ try:
178
+ from sensorflow.evaluation.records import Alert, get_store as get_legacy_store, new_id
179
+ legacy = get_legacy_store()
180
+ for s in shifts[:3]:
181
+ if s.get("recall_gap") is not None and s["recall_gap"] < -0.03:
182
+ legacy.put("alerts", Alert(
183
+ alert_id=new_id("alert"), kind="distribution_shift",
184
+ severity="critical" if s["recall_gap"] < -0.08 else "warning",
185
+ message=(f"Distribution shift {s['cohort']}: train {s['train_share']*100:.1f}% "
186
+ f"-> eval {s['eval_share']*100:.1f}% "
187
+ f"(rel {s['relative_change']*100:+.0f}%), cohort recall "
188
+ f"{(s['cohort_recall'] or 0)*100:.1f}% vs overall "
189
+ f"{(s['overall_recall'] or 0)*100:.1f}%"),
190
+ evidence_page="command", evidence_id=run.run_id))
191
+ legacy.save()
192
+ except Exception:
193
+ pass
194
+
195
+ result = {"run_id": run.run_id, "shifts": shifts[:20],
196
+ "method": "train mix vs eval mix over (class, weather, lighting); "
197
+ "relative share change with cube recall impact",
198
+ "thresholds": {"min_eval_count": min_eval_count, "rel_threshold": rel_threshold}}
199
+ with open(cache_path, "w") as f:
200
+ json.dump(result, f)
201
+ return result
202
+
203
+
204
+ # ------------------------------------------------------------------ the "why" layer
205
+
206
+
207
+ _FACTOR_ORDER = ["occlusion", "low_illumination", "long_range", "sensor_disagreement", "other"]
208
+
209
+
210
+ def why(store: MegaStore, run: EvaluationRun, filters: Optional[Dict] = None,
211
+ metric: str = "recall") -> Dict:
212
+ """Decompose the failure population of a filtered slice by primary factor."""
213
+ art = store.artifacts(run.run_id)
214
+ errors = art.get("errors")
215
+ cube_df = art["cube"]
216
+ if errors is None or not len(errors):
217
+ return {"factors": [], "top_cohorts": [], "failure_count": 0}
218
+
219
+ mask = np.ones(len(errors), dtype=bool)
220
+ fail_types = [ERROR_TYPES.index(t) for t in ("FN", "FP", "LOCALIZATION")]
221
+ if metric == "recall":
222
+ fail_types = [ERROR_TYPES.index("FN"), ERROR_TYPES.index("LOCALIZATION")]
223
+ elif metric == "precision":
224
+ fail_types = [ERROR_TYPES.index("FP"), ERROR_TYPES.index("LOCALIZATION")]
225
+ mask &= errors["error_type"].isin(fail_types).to_numpy()
226
+ for dim, values in (filters or {}).items():
227
+ if dim not in DIM_NAMES:
228
+ continue
229
+ vocab = DIMENSIONS[dim]
230
+ codes = [vocab.index(v) for v in (values if isinstance(values, list) else [values])
231
+ if v in vocab]
232
+ mask &= errors[dim].isin(codes).to_numpy()
233
+ sub = errors[mask]
234
+ total = len(sub)
235
+ if total == 0:
236
+ return {"factors": [], "top_cohorts": [], "failure_count": 0}
237
+
238
+ occl = sub["occlusion"].to_numpy() >= 1
239
+ night = sub["lighting"].to_numpy() == 1
240
+ far = sub["distance_band"].to_numpy() >= 2
241
+ sensor = sub["sensor_disagree"].to_numpy().astype(bool)
242
+ factor = np.full(total, 4, dtype=np.int8) # other
243
+ factor[sensor] = 3
244
+ factor[far] = 2
245
+ factor[night] = 1
246
+ factor[occl] = 0 # priority: occlusion > low light > range > sensor > other
247
+ counts = np.bincount(factor, minlength=5)
248
+ factors = [{"factor": _FACTOR_ORDER[i], "count": int(counts[i]),
249
+ "share": round(float(counts[i]) / total, 4)}
250
+ for i in range(5) if counts[i] > 0]
251
+ factors.sort(key=lambda x: -x["count"])
252
+
253
+ rows, _ = cube_mod.aggregate(cube_df, filters, ["class", "lighting", "weather"],
254
+ ["n", metric, "fn", "fp"], 2000)
255
+ rows = [r for r in rows if r["n"] >= 150 and r.get(metric) is not None]
256
+ rows.sort(key=lambda r: r[metric])
257
+ top = [{"cohort": f"{r['class']}/{r['lighting']}/{r['weather']}",
258
+ "n": r["n"], metric: r[metric], "fn": r.get("fn"), "fp": r.get("fp")}
259
+ for r in rows[:8]]
260
+
261
+ return {"metric": metric, "filters": filters or {}, "failure_count": int(total),
262
+ "factors": factors, "top_cohorts": top,
263
+ "method": "primary-factor attribution over the error index "
264
+ "(priority: occlusion > low illumination > long range > "
265
+ "sensor disagreement > other)"}
266
+
267
+
268
+ # ------------------------------------------------------------------ hybrid similarity
269
+
270
+
271
+ def similar_containers(store: MegaStore, run: EvaluationRun, container_id: int,
272
+ filters: Optional[Dict] = None, k: int = 12) -> Dict:
273
+ art = store.artifacts(run.run_id)
274
+ ids, emb = art["emb_ids"], art["emb"]
275
+ containers = art["containers"].set_index("container_id")
276
+ pos = np.where(ids == container_id)[0]
277
+ if pos.size == 0:
278
+ raise KeyError(f"Unknown container {container_id}")
279
+ q = emb[pos[0]]
280
+ sims = emb @ q
281
+
282
+ mask = np.ones(ids.shape[0], dtype=bool)
283
+ for dim, values in (filters or {}).items():
284
+ if dim not in pop_mod.CONTAINER_DIMS:
285
+ continue
286
+ vocab = DIMENSIONS[dim]
287
+ codes = [vocab.index(v) for v in (values if isinstance(values, list) else [values])
288
+ if v in vocab]
289
+ col = containers.loc[ids, dim].to_numpy()
290
+ mask &= np.isin(col, codes)
291
+ mask[pos[0]] = False
292
+ sims = np.where(mask, sims, -np.inf)
293
+
294
+ kk = min(k, int(mask.sum()))
295
+ if kk <= 0:
296
+ return {"query_container": container_id, "results": []}
297
+ top = np.argpartition(-sims, kk - 1)[:kk]
298
+ top = top[np.argsort(-sims[top])]
299
+ results = []
300
+ for i in top.tolist():
301
+ cid = int(ids[i])
302
+ row = containers.loc[cid]
303
+ results.append({
304
+ "container_id": cid,
305
+ "similarity": round(float(sims[i]), 4),
306
+ **{dim: DIMENSIONS[dim][int(row[dim])] for dim in pop_mod.CONTAINER_DIMS},
307
+ "n_objects": int(row["n_objects"]),
308
+ "fn": int(row["fn"]), "fp": int(row["fp"]),
309
+ "anomalies": int(row["anomalies"]),
310
+ "risk_score": round(float(row["risk_score"]), 4),
311
+ })
312
+ return {"query_container": container_id,
313
+ "retrieval": "cosine over 32-d structural embeddings + structured filters",
314
+ "results": results}
sensorflow/megaeval/api.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI router for the aggregate-first mega-scale evaluation layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, List, Optional
6
+
7
+ import numpy as np
8
+ from fastapi import APIRouter, HTTPException
9
+ from pydantic import BaseModel, Field
10
+
11
+ from sensorflow.megaeval import analysis, errors as errors_mod, population as pop_mod
12
+ from sensorflow.megaeval import sampling
13
+ from sensorflow.megaeval.cube import SKETCH_METRICS
14
+ from sensorflow.megaeval.population import DIMENSIONS
15
+ from sensorflow.megaeval.runs import CONTAINER_STATUS, EvaluationRun, get_mega_store
16
+
17
+ router = APIRouter(prefix="/api")
18
+
19
+
20
+ def _run_or_404(run_id: str) -> EvaluationRun:
21
+ run = get_mega_store().runs.get(run_id)
22
+ if run is None:
23
+ raise HTTPException(404, f"Unknown evaluation run {run_id}")
24
+ return run
25
+
26
+
27
+ def _published_or_409(run_id: str) -> EvaluationRun:
28
+ run = _run_or_404(run_id)
29
+ if run.status != "published":
30
+ raise HTTPException(409, f"Run {run_id} is {run.status}; results are available "
31
+ "once it is published")
32
+ return run
33
+
34
+
35
+ # ------------------------------------------------------------------ requests
36
+
37
+
38
+ class GeneratePopulationRequest(BaseModel):
39
+ name: str = "mega-perception"
40
+ num_objects: int = Field(320_000, ge=1_000, le=1_200_000)
41
+ seed: int = 42
42
+
43
+
44
+ class CreateRunRequest(BaseModel):
45
+ population_id: str
46
+ model_version: str = "model-v42"
47
+ overrides: Dict = Field(default_factory=dict)
48
+ worker_delay_s: float = Field(0.5, ge=0.0, le=5.0)
49
+ workers: int = Field(4, ge=1, le=8)
50
+ seed: Optional[int] = None
51
+ label_version: str = "labels-v1"
52
+
53
+
54
+ class EvaluationQueryRequest(BaseModel):
55
+ evaluation_id: str
56
+ filters: Dict[str, List[str]] = Field(default_factory=dict)
57
+ metrics: List[str] = Field(default_factory=list)
58
+ group_by: List[str] = Field(default_factory=list)
59
+ limit: int = Field(200, ge=1, le=2000)
60
+
61
+
62
+ class ErrorSearchRequest(BaseModel):
63
+ run_id: str
64
+ error_types: List[str] = Field(default_factory=list)
65
+ filters: Dict[str, List[str]] = Field(default_factory=dict)
66
+ confidence_max: Optional[float] = None
67
+ confidence_min: Optional[float] = None
68
+ risk_min: Optional[float] = None
69
+ severity_min: Optional[float] = None
70
+ safety_only: bool = False
71
+ limit_containers: int = Field(25, ge=1, le=100)
72
+
73
+
74
+ class CompareRequest(BaseModel):
75
+ candidate_run_id: str
76
+ baseline_run_id: str
77
+ policy: Dict = Field(default_factory=dict)
78
+
79
+
80
+ class WhyRequest(BaseModel):
81
+ run_id: str
82
+ filters: Dict[str, List[str]] = Field(default_factory=dict)
83
+ metric: str = "recall"
84
+
85
+
86
+ class SimilarityRequest(BaseModel):
87
+ run_id: str
88
+ container_id: int
89
+ filters: Dict[str, List[str]] = Field(default_factory=dict)
90
+ k: int = Field(12, ge=1, le=50)
91
+
92
+
93
+ class ReviewPlanRequest(BaseModel):
94
+ target_n: Optional[int] = Field(None, ge=100, le=20_000)
95
+
96
+
97
+ # ------------------------------------------------------------------ populations
98
+
99
+
100
+ @router.post("/megaeval/population/generate")
101
+ def generate_population(req: GeneratePopulationRequest):
102
+ meta = pop_mod.generate_population(name=req.name, num_objects=req.num_objects,
103
+ seed=req.seed)
104
+ return meta
105
+
106
+
107
+ @router.get("/megaeval/populations")
108
+ def list_populations():
109
+ return {"populations": pop_mod.list_populations()}
110
+
111
+
112
+ @router.get("/megaeval/dimensions")
113
+ def dimensions():
114
+ return {"dimensions": DIMENSIONS}
115
+
116
+
117
+ # ------------------------------------------------------------------ runs (async jobs)
118
+
119
+
120
+ @router.post("/megaeval/runs")
121
+ def create_run(req: CreateRunRequest):
122
+ store = get_mega_store()
123
+ try:
124
+ run = store.create_run(population_id=req.population_id,
125
+ model_version=req.model_version,
126
+ overrides=req.overrides,
127
+ seed=req.seed,
128
+ worker_delay_s=req.worker_delay_s,
129
+ workers=req.workers,
130
+ label_version=req.label_version)
131
+ except KeyError as e:
132
+ raise HTTPException(404, str(e))
133
+ store.start_async(run)
134
+ return run.to_dict()
135
+
136
+
137
+ @router.get("/megaeval/runs")
138
+ def list_runs():
139
+ store = get_mega_store()
140
+ runs = sorted(store.runs.values(), key=lambda r: r.created_at, reverse=True)
141
+ return {"runs": [r.to_dict() for r in runs]}
142
+
143
+
144
+ @router.get("/megaeval/runs/{run_id}")
145
+ def get_run(run_id: str):
146
+ return _run_or_404(run_id).to_dict()
147
+
148
+
149
+ @router.get("/megaeval/status")
150
+ def megaeval_status():
151
+ store = get_mega_store()
152
+ active = [r.progress_dict() for r in store.runs.values()
153
+ if r.status in ("queued", "running", "reducing", "materializing")]
154
+ return {"active_runs": active, "cache": store.router.cache.stats(),
155
+ "total_runs": len(store.runs)}
156
+
157
+
158
+ # ------------------------------------------------------------------ THE query API
159
+
160
+
161
+ @router.post("/evaluations/query")
162
+ def evaluations_query(req: EvaluationQueryRequest):
163
+ """Aggregate-first query endpoint. Routed: cache -> cube -> record scan."""
164
+ store = get_mega_store()
165
+ run = _published_or_409(req.evaluation_id)
166
+ art = store.artifacts(run.run_id)
167
+
168
+ def scan_fn(filters, group_by, metrics, limit):
169
+ return store.scan_records(run, filters, group_by, metrics, limit)
170
+
171
+ return store.router.query(run, art["cube"], art.get("sketches", {}),
172
+ req.filters, req.metrics, req.group_by,
173
+ req.limit, scan_fn=scan_fn)
174
+
175
+
176
+ @router.get("/megaeval/cache")
177
+ def cache_stats():
178
+ return get_mega_store().router.cache.stats()
179
+
180
+
181
+ # ------------------------------------------------------------------ quality funnel
182
+
183
+
184
+ @router.get("/megaeval/runs/{run_id}/funnel")
185
+ def quality_funnel(run_id: str):
186
+ store = get_mega_store()
187
+ run = _published_or_409(run_id)
188
+ art = store.artifacts(run_id)
189
+ errs = art.get("errors")
190
+ review = art.get("review") or {}
191
+ n_err = int(len(errs)) if errs is not None else 0
192
+ n_high = int((errs["risk_score"] >= 0.6).sum()) if errs is not None and len(errs) else 0
193
+ results = (review.get("results") or {})
194
+ prec = results.get("precision") or {}
195
+ stages = [
196
+ {"stage": "Objects Evaluated", "count": int(run.objects_total)},
197
+ {"stage": "Containers", "count": int(run.headline.get("containers", 0))},
198
+ {"stage": "Potential Errors", "count": n_err},
199
+ {"stage": "High-Confidence Errors", "count": n_high},
200
+ {"stage": "Review Sample", "count": int((review.get("funnel") or {}).get("statistically_selected", 0))},
201
+ {"stage": "Human Verified", "count": int((review.get("funnel") or {}).get("reviewed", 0))},
202
+ ]
203
+ base = stages[0]["count"] or 1
204
+ for s in stages:
205
+ s["pct_of_population"] = round(s["count"] / base * 100, 3)
206
+ return {"stages": stages,
207
+ "estimated_precision": ({"estimate": prec.get("estimate"),
208
+ "ci_low": prec.get("ci_low"),
209
+ "ci_high": prec.get("ci_high"),
210
+ "n_reviewed": prec.get("n_reviewed")}
211
+ if prec else None)}
212
+
213
+
214
+ # ------------------------------------------------------------------ containers
215
+
216
+
217
+ _SORT_PRESETS = {
218
+ "worst_recall": ("recall", True),
219
+ "worst_precision": ("precision", True),
220
+ "worst_iou": ("mean_iou", True),
221
+ "most_anomalies": ("anomalies", False),
222
+ "least_verified": ("reviewed", True),
223
+ "highest_risk": ("risk_score", False),
224
+ }
225
+
226
+
227
+ @router.get("/megaeval/runs/{run_id}/containers")
228
+ def containers(run_id: str, sort: str = "highest_risk", limit: int = 50, offset: int = 0):
229
+ store = get_mega_store()
230
+ _published_or_409(run_id)
231
+ art = store.artifacts(run_id)
232
+ df = art["containers"].copy()
233
+ tp, fp, fn = df["tp"], df["fp"], df["fn"]
234
+ with np.errstate(divide="ignore", invalid="ignore"):
235
+ df["recall"] = np.where((tp + fn) > 0, tp / (tp + fn), np.nan)
236
+ df["precision"] = np.where((tp + fp) > 0, tp / (tp + fp), np.nan)
237
+ df["mean_iou"] = np.where(tp > 0, df["sum_iou"] / tp, np.nan)
238
+ metric, ascending = _SORT_PRESETS.get(sort, _SORT_PRESETS["highest_risk"])
239
+ df = df.sort_values(metric, ascending=ascending, na_position="last")
240
+ total = len(df)
241
+ page = df.iloc[offset:offset + max(1, min(limit, 500))]
242
+ rows = []
243
+ for _, r in page.iterrows():
244
+ rows.append({
245
+ "container_id": int(r["container_id"]),
246
+ **{dim: DIMENSIONS[dim][int(r[dim])] for dim in pop_mod.CONTAINER_DIMS},
247
+ "n_objects": int(r["n_objects"]),
248
+ "tp": int(r["tp"]), "fp": int(r["fp"]), "fn": int(r["fn"]),
249
+ "anomalies": int(r["anomalies"]),
250
+ "reviewed": int(r["reviewed"]), "verified": int(r["verified"]),
251
+ "recall": None if np.isnan(r["recall"]) else round(float(r["recall"]), 4),
252
+ "precision": None if np.isnan(r["precision"]) else round(float(r["precision"]), 4),
253
+ "mean_iou": None if np.isnan(r["mean_iou"]) else round(float(r["mean_iou"]), 4),
254
+ "risk_score": round(float(r["risk_score"]), 4),
255
+ "status": CONTAINER_STATUS[int(r["status"])],
256
+ })
257
+ return {"total": total, "sort": sort, "rows": rows}
258
+
259
+
260
+ @router.get("/megaeval/runs/{run_id}/containers/{container_id}/objects")
261
+ def container_objects(run_id: str, container_id: int):
262
+ store = get_mega_store()
263
+ run = _published_or_409(run_id)
264
+ try:
265
+ rows = store.container_objects(run, container_id)
266
+ except FileNotFoundError:
267
+ raise HTTPException(404, f"No forensic data for container {container_id}")
268
+ if not rows:
269
+ raise HTTPException(404, f"Unknown container {container_id}")
270
+ return {"container_id": container_id, "objects": rows}
271
+
272
+
273
+ # ------------------------------------------------------------------ error index
274
+
275
+
276
+ @router.post("/megaeval/errors/search")
277
+ def error_search(req: ErrorSearchRequest):
278
+ store = get_mega_store()
279
+ _published_or_409(req.run_id)
280
+ art = store.artifacts(req.run_id)
281
+ return errors_mod.search_errors(
282
+ art.get("errors"), art.get("containers"),
283
+ error_types=req.error_types or None, filters=req.filters or None,
284
+ confidence_max=req.confidence_max, confidence_min=req.confidence_min,
285
+ risk_min=req.risk_min, severity_min=req.severity_min,
286
+ safety_only=req.safety_only, limit_containers=req.limit_containers)
287
+
288
+
289
+ # ------------------------------------------------------------------ compare / shift / why / similarity
290
+
291
+
292
+ @router.post("/megaeval/compare")
293
+ def compare(req: CompareRequest):
294
+ store = get_mega_store()
295
+ cand = _published_or_409(req.candidate_run_id)
296
+ base = _published_or_409(req.baseline_run_id)
297
+ return analysis.compare_runs(store, cand, base, req.policy or None)
298
+
299
+
300
+ @router.get("/megaeval/runs/{run_id}/shift")
301
+ def shift(run_id: str):
302
+ return analysis.distribution_shift(get_mega_store(), _published_or_409(run_id))
303
+
304
+
305
+ @router.post("/megaeval/why")
306
+ def why(req: WhyRequest):
307
+ return analysis.why(get_mega_store(), _published_or_409(req.run_id),
308
+ req.filters or None, req.metric)
309
+
310
+
311
+ @router.post("/megaeval/similarity")
312
+ def similarity(req: SimilarityRequest):
313
+ try:
314
+ return analysis.similar_containers(get_mega_store(), _published_or_409(req.run_id),
315
+ req.container_id, req.filters or None, req.k)
316
+ except KeyError as e:
317
+ raise HTTPException(404, str(e))
318
+
319
+
320
+ # ------------------------------------------------------------------ review sampling
321
+
322
+
323
+ @router.get("/megaeval/runs/{run_id}/review")
324
+ def review_state(run_id: str):
325
+ store = get_mega_store()
326
+ _published_or_409(run_id)
327
+ art = store.artifacts(run_id)
328
+ review = art.get("review")
329
+ if review is None:
330
+ return {"run_id": run_id, "planned": False, "executed": False}
331
+ slim = {k: v for k, v in review.items() if k != "plans"}
332
+ slim["planned"] = True
333
+ slim["strata"] = {name: plan["strata"] for name, plan in review["plans"].items()}
334
+ return slim
335
+
336
+
337
+ @router.post("/megaeval/runs/{run_id}/review/plan")
338
+ def review_plan(run_id: str, req: ReviewPlanRequest):
339
+ store = get_mega_store()
340
+ run = _published_or_409(run_id)
341
+ review = sampling.build_review_plan(store, run, target_n=req.target_n)
342
+ slim = {k: v for k, v in review.items() if k != "plans"}
343
+ slim["planned"] = True
344
+ slim["strata"] = {name: plan["strata"] for name, plan in review["plans"].items()}
345
+ return slim
346
+
347
+
348
+ @router.post("/megaeval/runs/{run_id}/review/execute")
349
+ def review_execute(run_id: str):
350
+ store = get_mega_store()
351
+ run = _published_or_409(run_id)
352
+ art = store.artifacts(run_id)
353
+ if art.get("review") is None:
354
+ sampling.build_review_plan(store, run)
355
+ review = sampling.execute_reviews(store, run)
356
+ slim = {k: v for k, v in review.items() if k != "plans"}
357
+ slim["planned"] = True
358
+ slim["strata"] = {name: plan["strata"] for name, plan in review["plans"].items()}
359
+ return slim
360
+
361
+
362
+ # ------------------------------------------------------------------ distributions (sketches)
363
+
364
+
365
+ @router.get("/megaeval/runs/{run_id}/distributions")
366
+ def distributions(run_id: str):
367
+ store = get_mega_store()
368
+ run = _published_or_409(run_id)
369
+ art = store.artifacts(run_id)
370
+ sk = art.get("sketches", {})
371
+ out = {"run_id": run_id, "exact": False,
372
+ "note": "distributions are sketch-based (fixed-bin quantile histograms); "
373
+ "counts in the cube are exact"}
374
+ for name in ("confidence", "iou"):
375
+ hist = sk.get(name)
376
+ if hist is None:
377
+ continue
378
+ out[name] = {
379
+ "bins": hist.bins, "lo": hist.lo, "hi": hist.hi,
380
+ "counts": hist.counts.tolist(),
381
+ "percentiles": {q: round(hist.percentile(float(q)), 4)
382
+ for q in ("10", "25", "50", "75", "90", "99")},
383
+ }
384
+ out["containers_hll_estimate"] = sk.get("container_hll_estimate")
385
+ out["containers_exact"] = run.headline.get("containers")
386
+ out["available_sketch_metrics"] = SKETCH_METRICS
387
+ return out
sensorflow/megaeval/cube.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Metric cube: partial sufficient statistics -> reduce -> aggregate queries.
2
+
3
+ The cube is the heart of aggregate-first evaluation. During a run, each worker
4
+ computes *partial* additive statistics per partition, grouped by the 9 cube
5
+ dimensions. Reducing = concatenating partials and summing per cell (map-side
6
+ combine + shuffle reduce in Spark terms). Queries NEVER scan raw records unless
7
+ they ask for something outside the cube (then the router honestly reports
8
+ source="scan").
9
+
10
+ Cell key: class x weather x lighting x road_type x scenario x sensor x
11
+ distance_band x speed_band x occlusion (per evaluation run)
12
+ Cell stats (all additive): n, tp, fp, fn, loc_err, anomalies, sum_iou,
13
+ sum_conf, sum_conf2, safety_n, safety_tp, reviewed, verified
14
+ Derived on demand: precision, recall, f1, mean_iou, anomaly_rate,
15
+ conf_mean, conf_std, safety_recall, review_coverage, verified_rate
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import threading
24
+ import time
25
+ from typing import Dict, List, Optional, Tuple
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+ from sensorflow.megaeval.population import DIMENSIONS, DIM_NAMES
31
+
32
+ STAT_COLS = ["n", "tp", "fp", "fn", "loc_err", "anomalies", "sum_iou",
33
+ "sum_conf", "sum_conf2", "safety_n", "safety_tp", "reviewed", "verified"]
34
+
35
+ DERIVED_METRICS = ["precision", "recall", "f1", "mean_iou", "anomaly_rate",
36
+ "conf_mean", "conf_std", "safety_recall", "review_coverage",
37
+ "verified_rate", "error_rate"]
38
+ COUNT_METRICS = ["n", "tp", "fp", "fn", "loc_err", "anomalies", "reviewed", "verified",
39
+ "safety_n", "safety_tp"]
40
+ SKETCH_METRICS = ["confidence_p50", "confidence_p10", "confidence_p90",
41
+ "iou_p50", "iou_p10", "iou_p90"]
42
+ ALL_METRICS = COUNT_METRICS + DERIVED_METRICS + SKETCH_METRICS
43
+
44
+
45
+ # ------------------------------------------------------------------ partial stats
46
+
47
+
48
+ def partial_stats(rows: pd.DataFrame) -> pd.DataFrame:
49
+ """Group one partition's contribution rows by the 9 dims and sum stat columns.
50
+
51
+ `rows` must contain DIM_NAMES code columns plus STAT_COLS contribution columns
52
+ (e.g. a TP object contributes n=1, tp=1, sum_iou=iou; an FP row contributes
53
+ n=0, fp=1, sum_conf=conf).
54
+ """
55
+ return rows.groupby(DIM_NAMES, as_index=False, observed=True, sort=False)[STAT_COLS].sum()
56
+
57
+
58
+ def reduce_partials(partials: List[pd.DataFrame]) -> pd.DataFrame:
59
+ """Reduce partial stats into the final cube (concat + groupby-sum)."""
60
+ if not partials:
61
+ return pd.DataFrame(columns=DIM_NAMES + STAT_COLS)
62
+ return (pd.concat(partials, ignore_index=True)
63
+ .groupby(DIM_NAMES, as_index=False, observed=True, sort=False)[STAT_COLS]
64
+ .sum())
65
+
66
+
67
+ def derive_metrics(agg: pd.DataFrame) -> pd.DataFrame:
68
+ """Attach derived ratio metrics to an aggregated (already summed) frame."""
69
+ out = agg.copy()
70
+ tp, fp, fn = out["tp"], out["fp"], out["fn"]
71
+ pp = tp + fp
72
+ with np.errstate(divide="ignore", invalid="ignore"):
73
+ out["precision"] = np.where(pp > 0, tp / pp, np.nan)
74
+ out["recall"] = np.where((tp + fn) > 0, tp / (tp + fn), np.nan)
75
+ p, r = out["precision"], out["recall"]
76
+ out["f1"] = np.where((p + r) > 0, 2 * p * r / (p + r), np.nan)
77
+ out["mean_iou"] = np.where(tp > 0, out["sum_iou"] / tp, np.nan)
78
+ out["anomaly_rate"] = np.where(out["n"] > 0, out["anomalies"] / out["n"], np.nan)
79
+ out["conf_mean"] = np.where(pp > 0, out["sum_conf"] / pp, np.nan)
80
+ var = np.where(pp > 1,
81
+ (out["sum_conf2"] - out["sum_conf"] ** 2 / pp.replace(0, 1)) / (pp - 1).replace(0, 1),
82
+ np.nan)
83
+ out["conf_std"] = np.sqrt(np.clip(var, 0, None))
84
+ out["safety_recall"] = np.where(out["safety_n"] > 0, out["safety_tp"] / out["safety_n"], np.nan)
85
+ out["review_coverage"] = np.where(out["n"] > 0, out["reviewed"] / out["n"], np.nan)
86
+ out["verified_rate"] = np.where(out["reviewed"] > 0, out["verified"] / out["reviewed"], np.nan)
87
+ out["error_rate"] = np.where(out["n"] > 0, (fn + fp + out["loc_err"]) / out["n"], np.nan)
88
+ return out
89
+
90
+
91
+ # ------------------------------------------------------------------ persistence
92
+
93
+
94
+ def save_cube(path: str, cube: pd.DataFrame) -> None:
95
+ np.savez_compressed(path, **{c: cube[c].to_numpy() for c in cube.columns})
96
+
97
+
98
+ def load_cube(path: str) -> pd.DataFrame:
99
+ with np.load(path) as z:
100
+ return pd.DataFrame({k: z[k] for k in z.files})
101
+
102
+
103
+ # ------------------------------------------------------------------ query router
104
+
105
+
106
+ class QueryCache:
107
+ """Evaluation cache: key = hash(dataset+model+filters+group_by+metrics)."""
108
+
109
+ def __init__(self, max_entries: int = 512):
110
+ self._data: Dict[str, Dict] = {}
111
+ self._lock = threading.Lock()
112
+ self.max_entries = max_entries
113
+ self.hits = 0
114
+ self.misses = 0
115
+
116
+ @staticmethod
117
+ def key(dataset_version: str, model_version: str, filters: Dict,
118
+ group_by: List[str], metrics: List[str]) -> str:
119
+ blob = json.dumps({
120
+ "d": dataset_version, "m": model_version,
121
+ "f": {k: sorted(v) for k, v in sorted((filters or {}).items())},
122
+ "g": list(group_by or []), "x": sorted(metrics or []),
123
+ }, sort_keys=True)
124
+ return hashlib.sha256(blob.encode()).hexdigest()[:24]
125
+
126
+ def get(self, key: str) -> Optional[Dict]:
127
+ with self._lock:
128
+ hit = self._data.get(key)
129
+ if hit is not None:
130
+ self.hits += 1
131
+ else:
132
+ self.misses += 1
133
+ return hit
134
+
135
+ def put(self, key: str, value: Dict) -> None:
136
+ with self._lock:
137
+ if len(self._data) >= self.max_entries:
138
+ self._data.pop(next(iter(self._data)))
139
+ self._data[key] = value
140
+
141
+ def invalidate(self) -> None:
142
+ with self._lock:
143
+ self._data.clear()
144
+
145
+ def stats(self) -> Dict:
146
+ with self._lock:
147
+ total = self.hits + self.misses
148
+ return {"entries": len(self._data), "hits": self.hits, "misses": self.misses,
149
+ "hit_rate": round(self.hits / total, 4) if total else None}
150
+
151
+
152
+ def _codes_for(dim: str, values: List[str]) -> List[int]:
153
+ vocab = DIMENSIONS[dim]
154
+ out = []
155
+ for v in values:
156
+ if v in vocab:
157
+ out.append(vocab.index(v))
158
+ return out
159
+
160
+
161
+ def apply_filters(cube: pd.DataFrame, filters: Optional[Dict[str, List[str]]]) -> pd.DataFrame:
162
+ if not filters:
163
+ return cube
164
+ mask = np.ones(len(cube), dtype=bool)
165
+ for dim, values in filters.items():
166
+ if dim not in DIM_NAMES:
167
+ continue
168
+ codes = _codes_for(dim, values if isinstance(values, list) else [values])
169
+ mask &= cube[dim].isin(codes).to_numpy()
170
+ return cube[mask]
171
+
172
+
173
+ def aggregate(cube: pd.DataFrame, filters: Optional[Dict], group_by: Optional[List[str]],
174
+ metrics: Optional[List[str]], limit: int = 200) -> Tuple[List[Dict], int]:
175
+ """Aggregate-lookup path: filter cells, group, sum, derive. Never touches records."""
176
+ sub = apply_filters(cube, filters)
177
+ group_by = [g for g in (group_by or []) if g in DIM_NAMES]
178
+ metrics = [m for m in (metrics or []) if m in ALL_METRICS and m not in SKETCH_METRICS] \
179
+ or ["n", "precision", "recall", "f1", "mean_iou", "anomaly_rate"]
180
+
181
+ if group_by:
182
+ agg = sub.groupby(group_by, as_index=False, observed=True, sort=False)[STAT_COLS].sum()
183
+ else:
184
+ sums = sub[STAT_COLS].sum()
185
+ agg = pd.DataFrame([sums])
186
+ agg = derive_metrics(agg)
187
+
188
+ cell_count = len(sub)
189
+ sort_metric = next((m for m in metrics if m in agg.columns), None)
190
+ if group_by and sort_metric:
191
+ agg = agg.sort_values("n", ascending=False)
192
+ agg = agg.head(max(1, min(limit, 2000)))
193
+
194
+ rows: List[Dict] = []
195
+ for _, r in agg.iterrows():
196
+ row: Dict = {}
197
+ for g in group_by:
198
+ row[g] = DIMENSIONS[g][int(r[g])]
199
+ row["n"] = int(r["n"])
200
+ for m in metrics:
201
+ if m == "n":
202
+ continue
203
+ v = r.get(m)
204
+ if v is None or (isinstance(v, float) and np.isnan(v)):
205
+ row[m] = None
206
+ elif m in COUNT_METRICS:
207
+ row[m] = int(v)
208
+ else:
209
+ row[m] = round(float(v), 6)
210
+ rows.append(row)
211
+ return rows, cell_count
212
+
213
+
214
+ class QueryRouter:
215
+ """Routes a query: cache -> cube aggregate -> (only if unavoidable) record scan.
216
+
217
+ This is the OLAP-engine seam: callers never know how data is stored.
218
+ """
219
+
220
+ def __init__(self):
221
+ self.cache = QueryCache()
222
+
223
+ def query(self, run: "object", cube: pd.DataFrame, sketches: Dict,
224
+ filters: Optional[Dict], metrics: Optional[List[str]],
225
+ group_by: Optional[List[str]], limit: int = 200,
226
+ scan_fn=None) -> Dict:
227
+ t0 = time.perf_counter()
228
+ metrics = metrics or []
229
+ key = QueryCache.key(run.population_id, run.model_version,
230
+ filters or {}, group_by or [], metrics)
231
+ cached = self.cache.get(key)
232
+ if cached is not None:
233
+ return {**cached,
234
+ "meta": {**cached["meta"], "cache_hit": True, "source": "cache",
235
+ "latency_ms": round((time.perf_counter() - t0) * 1000, 3)}}
236
+
237
+ sketch_wanted = [m for m in metrics if m in SKETCH_METRICS]
238
+ needs_scan = any(g not in DIM_NAMES for g in (group_by or []))
239
+
240
+ if needs_scan and scan_fn is not None:
241
+ rows, cell_count = scan_fn(filters, group_by, metrics, limit)
242
+ source, exact = "scan", True
243
+ else:
244
+ rows, cell_count = aggregate(cube, filters, group_by, metrics, limit)
245
+ source, exact = "cube", True
246
+ if sketch_wanted and not group_by and not filters:
247
+ # population-level percentile questions answered from sketches
248
+ for m in sketch_wanted:
249
+ metric_name, q = m.rsplit("_p", 1)
250
+ hist = sketches.get(metric_name)
251
+ if hist is not None and rows:
252
+ rows[0][m] = round(float(hist.percentile(float(q))), 6)
253
+ exact = False # percentiles are approximate
254
+ elif sketch_wanted:
255
+ exact = True # sketch metrics ignored on grouped/filtered queries
256
+
257
+ latency = round((time.perf_counter() - t0) * 1000, 3)
258
+ result = {
259
+ "rows": rows,
260
+ "meta": {
261
+ "source": source,
262
+ "cache_hit": False,
263
+ "latency_ms": latency,
264
+ "cells_touched": cell_count,
265
+ "exact": exact,
266
+ "approximate_fields": sketch_wanted if not exact else [],
267
+ },
268
+ }
269
+ self.cache.put(key, result)
270
+ return result
sensorflow/megaeval/errors.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Error index: fast multi-criteria search over evaluation errors.
2
+
3
+ The index is a columnar table (one row per error) built during the run:
4
+ error_id, object_id, container_id, error_type, severity, confidence,
5
+ risk_score, safety_critical, sensor_disagree, + the 9 cube dimensions.
6
+
7
+ Searches return the *worst-N containers* (aggregated), never raw record dumps —
8
+ this backs the Investigation UI. Individual exemplar errors are capped.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Dict, List, Optional
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ from sensorflow.megaeval.population import DIMENSIONS, DIM_NAMES
19
+ from sensorflow.megaeval.runs import ERROR_TYPES
20
+
21
+
22
+ def search_errors(
23
+ errors: pd.DataFrame,
24
+ containers: pd.DataFrame,
25
+ error_types: Optional[List[str]] = None,
26
+ filters: Optional[Dict[str, List[str]]] = None,
27
+ confidence_max: Optional[float] = None,
28
+ confidence_min: Optional[float] = None,
29
+ risk_min: Optional[float] = None,
30
+ severity_min: Optional[float] = None,
31
+ safety_only: bool = False,
32
+ limit_containers: int = 25,
33
+ limit_examples: int = 50,
34
+ ) -> Dict:
35
+ if errors is None or not len(errors):
36
+ return {"matched_errors": 0, "worst_containers": [], "examples": [],
37
+ "by_type": {}}
38
+ mask = np.ones(len(errors), dtype=bool)
39
+ if error_types:
40
+ codes = [ERROR_TYPES.index(t) for t in error_types if t in ERROR_TYPES]
41
+ mask &= errors["error_type"].isin(codes).to_numpy()
42
+ for dim, values in (filters or {}).items():
43
+ if dim not in DIM_NAMES:
44
+ continue
45
+ vocab = DIMENSIONS[dim]
46
+ codes = [vocab.index(v) for v in (values if isinstance(values, list) else [values])
47
+ if v in vocab]
48
+ mask &= errors[dim].isin(codes).to_numpy()
49
+ if confidence_max is not None:
50
+ mask &= errors["confidence"].to_numpy() <= confidence_max
51
+ if confidence_min is not None:
52
+ mask &= errors["confidence"].to_numpy() >= confidence_min
53
+ if risk_min is not None:
54
+ mask &= errors["risk_score"].to_numpy() >= risk_min
55
+ if severity_min is not None:
56
+ mask &= errors["severity"].to_numpy() >= severity_min
57
+ if safety_only:
58
+ mask &= errors["safety_critical"].to_numpy().astype(bool)
59
+
60
+ sub = errors[mask]
61
+ by_type = {ERROR_TYPES[int(c)]: int(n)
62
+ for c, n in sub["error_type"].value_counts().items()}
63
+
64
+ # aggregate to worst containers: rank by count x mean risk
65
+ grp = sub.groupby("container_id", as_index=False).agg(
66
+ error_count=("error_id", "size"), mean_risk=("risk_score", "mean"),
67
+ max_severity=("severity", "max"), safety_hits=("safety_critical", "sum"))
68
+ grp["rank_score"] = grp["error_count"] * grp["mean_risk"]
69
+ grp = grp.sort_values("rank_score", ascending=False).head(limit_containers)
70
+ cmeta = containers.set_index("container_id") if containers is not None else None
71
+ worst = []
72
+ for _, r in grp.iterrows():
73
+ cid = int(r["container_id"])
74
+ item = {"container_id": cid, "error_count": int(r["error_count"]),
75
+ "mean_risk": round(float(r["mean_risk"]), 4),
76
+ "max_severity": round(float(r["max_severity"]), 4),
77
+ "safety_hits": int(r["safety_hits"])}
78
+ if cmeta is not None and cid in cmeta.index:
79
+ row = cmeta.loc[cid]
80
+ for dim in ("weather", "lighting", "road_type", "scenario"):
81
+ item[dim] = DIMENSIONS[dim][int(row[dim])]
82
+ item["n_objects"] = int(row["n_objects"])
83
+ item["risk_score"] = round(float(row["risk_score"]), 4)
84
+ worst.append(item)
85
+
86
+ ex = sub.sort_values("risk_score", ascending=False).head(limit_examples)
87
+ examples = []
88
+ for _, r in ex.iterrows():
89
+ examples.append({
90
+ "error_id": int(r["error_id"]),
91
+ "annotation_id": (f"obj-{int(r['object_id'])}" if r["object_id"] >= 0
92
+ else f"fp-{int(r['error_id'])}"),
93
+ "container_id": int(r["container_id"]),
94
+ "error_type": ERROR_TYPES[int(r["error_type"])],
95
+ "class": DIMENSIONS["class"][int(r["class"])],
96
+ "severity": round(float(r["severity"]), 4),
97
+ "confidence": round(float(r["confidence"]), 4),
98
+ "risk_score": round(float(r["risk_score"]), 4),
99
+ "safety_critical": bool(r["safety_critical"]),
100
+ "scenario": DIMENSIONS["scenario"][int(r["scenario"])],
101
+ "lighting": DIMENSIONS["lighting"][int(r["lighting"])],
102
+ "weather": DIMENSIONS["weather"][int(r["weather"])],
103
+ })
104
+
105
+ return {"matched_errors": int(len(sub)), "by_type": by_type,
106
+ "worst_containers": worst, "examples": examples}
sensorflow/megaeval/population.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Immutable synthetic populations, generated vectorized and stored partitioned.
2
+
3
+ Storage layout (single-node stand-in for an Iceberg/parquet lakehouse — no parquet
4
+ engine is installed in this venv, so partitions are compressed npz column files):
5
+
6
+ runs/megaeval/populations/{pop_id}/
7
+ meta.json dataset card: dims vocab, counts (exact), train mix, lineage
8
+ part-0000.npz ... column arrays partitioned by container_id % NUM_PARTITIONS
9
+
10
+ Every object row carries:
11
+ - 9 low-cardinality dimension codes (the cube dimensions)
12
+ - a model-independent `difficulty` in [0,1] (occlusion/distance/lighting/weather/size)
13
+ - `safety_critical` flag (VRUs in high-risk scenarios)
14
+
15
+ The "training mix" is a deliberately shifted joint distribution over
16
+ (class, weather, lighting) used by shift detection: training saw less
17
+ night/rain/fog than this evaluation population.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import threading
25
+ import uuid
26
+ from datetime import datetime, timezone
27
+ from typing import Dict, List, Optional, Tuple
28
+
29
+ import numpy as np
30
+
31
+ MEGA_ROOT = os.path.join("runs", "megaeval")
32
+ NUM_PARTITIONS = 32
33
+
34
+ DIMENSIONS: Dict[str, List[str]] = {
35
+ "class": ["vehicle", "pedestrian", "cyclist", "motorcycle", "truck", "bus"],
36
+ "weather": ["clear", "rain", "fog", "snow"],
37
+ "lighting": ["day", "night", "dusk"],
38
+ "road_type": ["highway", "urban", "rural", "intersection"],
39
+ "scenario": ["nominal", "near_miss", "occlusion_heavy", "night_glare",
40
+ "extreme_ttc", "sensor_degraded"],
41
+ "sensor": ["lidar", "camera", "fused"],
42
+ "distance_band": ["0-20m", "20-50m", "50-100m", "100m+"],
43
+ "speed_band": ["stationary", "slow", "medium", "fast"],
44
+ "occlusion": ["none", "partial", "heavy"],
45
+ }
46
+ DIM_NAMES = list(DIMENSIONS.keys())
47
+ CONTAINER_DIMS = ["weather", "lighting", "road_type", "scenario"]
48
+ OBJECT_DIMS = ["class", "sensor", "distance_band", "speed_band", "occlusion"]
49
+
50
+
51
+ def _now() -> str:
52
+ return datetime.now(timezone.utc).isoformat()
53
+
54
+
55
+ def pop_dir(pop_id: str) -> str:
56
+ return os.path.join(MEGA_ROOT, "populations", pop_id)
57
+
58
+
59
+ def set_mega_root(path: str) -> None:
60
+ """Test hook: relocate all megaeval storage (and clear partition caches)."""
61
+ global MEGA_ROOT
62
+ MEGA_ROOT = str(path)
63
+ with _CACHE_LOCK:
64
+ _PART_CACHE.clear()
65
+
66
+
67
+ # ------------------------------------------------------------------ generation
68
+
69
+
70
+ def _sample_container_dims(rng: np.random.Generator, n: int) -> Dict[str, np.ndarray]:
71
+ """Correlated container-level context (weather/lighting/road/scenario)."""
72
+ weather = rng.choice(4, size=n, p=[0.62, 0.20, 0.11, 0.07]).astype(np.int8)
73
+ lighting = rng.choice(3, size=n, p=[0.60, 0.28, 0.12]).astype(np.int8)
74
+ road = rng.choice(4, size=n, p=[0.30, 0.38, 0.14, 0.18]).astype(np.int8)
75
+ scenario = rng.choice(6, size=n, p=[0.66, 0.09, 0.09, 0.06, 0.05, 0.05]).astype(np.int8)
76
+ # correlations: night_glare scenario forces night; fog/snow raise sensor_degraded odds
77
+ scenario_names = DIMENSIONS["scenario"]
78
+ lighting[scenario == scenario_names.index("night_glare")] = 1 # night
79
+ bad_wx = (weather >= 2)
80
+ flip = bad_wx & (rng.random(n) < 0.18)
81
+ scenario[flip] = scenario_names.index("sensor_degraded")
82
+ return {"weather": weather, "lighting": lighting, "road_type": road, "scenario": scenario}
83
+
84
+
85
+ _CLASS_MIX_BY_ROAD = np.array([
86
+ # vehicle, pedestrian, cyclist, motorcycle, truck, bus
87
+ [0.62, 0.02, 0.01, 0.05, 0.24, 0.06], # highway
88
+ [0.44, 0.26, 0.12, 0.06, 0.07, 0.05], # urban
89
+ [0.55, 0.08, 0.06, 0.08, 0.18, 0.05], # rural
90
+ [0.40, 0.30, 0.14, 0.05, 0.06, 0.05], # intersection
91
+ ])
92
+
93
+ _SPEED_MIX_BY_ROAD = np.array([
94
+ [0.02, 0.08, 0.35, 0.55], # highway
95
+ [0.22, 0.44, 0.28, 0.06], # urban
96
+ [0.10, 0.30, 0.40, 0.20], # rural
97
+ [0.30, 0.45, 0.20, 0.05], # intersection
98
+ ])
99
+
100
+
101
+ def _object_difficulty(codes: Dict[str, np.ndarray], rng: np.random.Generator) -> np.ndarray:
102
+ """Model-independent difficulty in [0,1] — the physical hardness of each object."""
103
+ n = codes["class"].shape[0]
104
+ d = np.full(n, 0.12, dtype=np.float32)
105
+ d += np.array([0.00, 0.14, 0.12, 0.10, 0.02, 0.02], dtype=np.float32)[codes["class"]]
106
+ d += np.array([0.00, 0.08, 0.12, 0.10], dtype=np.float32)[codes["weather"]]
107
+ d += np.array([0.00, 0.12, 0.05], dtype=np.float32)[codes["lighting"]]
108
+ d += np.array([0.00, 0.10, 0.24], dtype=np.float32)[codes["occlusion"]]
109
+ d += np.array([0.00, 0.05, 0.14, 0.26], dtype=np.float32)[codes["distance_band"]]
110
+ d += np.array([0.00, 0.04, 0.08, 0.06, 0.07, 0.12], dtype=np.float32)[codes["scenario"]]
111
+ d += rng.normal(0.0, 0.05, size=n).astype(np.float32)
112
+ return np.clip(d, 0.01, 0.99)
113
+
114
+
115
+ def generate_population(
116
+ name: str = "mega-perception",
117
+ num_objects: int = 320_000,
118
+ seed: int = 42,
119
+ avg_objects_per_container: float = 12.0,
120
+ ) -> Dict:
121
+ """Generate + persist a partitioned population. Vectorized; ~seconds for 320k."""
122
+ rng = np.random.default_rng(seed)
123
+ num_containers = max(64, int(num_objects / avg_objects_per_container))
124
+
125
+ cdims = _sample_container_dims(rng, num_containers)
126
+ per_container = np.clip(rng.poisson(avg_objects_per_container, size=num_containers),
127
+ 3, 48).astype(np.int64)
128
+ total = int(per_container.sum())
129
+ container_id = np.repeat(np.arange(num_containers, dtype=np.int64), per_container)
130
+
131
+ codes: Dict[str, np.ndarray] = {k: cdims[k][container_id] for k in CONTAINER_DIMS}
132
+
133
+ road_obj = codes["road_type"].astype(np.int64)
134
+ u = rng.random(total)
135
+ cls_cdf = np.cumsum(_CLASS_MIX_BY_ROAD, axis=1)[road_obj]
136
+ codes["class"] = (u[:, None] > cls_cdf).sum(axis=1).astype(np.int8)
137
+ u2 = rng.random(total)
138
+ spd_cdf = np.cumsum(_SPEED_MIX_BY_ROAD, axis=1)[road_obj]
139
+ codes["speed_band"] = (u2[:, None] > spd_cdf).sum(axis=1).astype(np.int8)
140
+ codes["sensor"] = rng.choice(3, size=total, p=[0.38, 0.22, 0.40]).astype(np.int8)
141
+ codes["distance_band"] = rng.choice(4, size=total, p=[0.24, 0.40, 0.25, 0.11]).astype(np.int8)
142
+
143
+ occ_p = np.tile(np.array([0.62, 0.28, 0.10]), (total, 1))
144
+ heavy_scene = codes["scenario"] == DIMENSIONS["scenario"].index("occlusion_heavy")
145
+ occ_p[heavy_scene] = [0.20, 0.42, 0.38]
146
+ occ_cdf = np.cumsum(occ_p, axis=1)
147
+ codes["occlusion"] = (rng.random(total)[:, None] > occ_cdf).sum(axis=1).astype(np.int8)
148
+
149
+ difficulty = _object_difficulty(codes, rng)
150
+ vru = np.isin(codes["class"], [1, 2, 3])
151
+ risky_scene = np.isin(codes["scenario"], [1, 4]) # near_miss, extreme_ttc
152
+ near = codes["distance_band"] <= 1
153
+ safety_critical = (vru & (risky_scene | (near & (rng.random(total) < 0.15)))).astype(np.uint8)
154
+
155
+ object_id = np.arange(total, dtype=np.int64)
156
+ pop_id = f"pop-{uuid.uuid4().hex[:10]}"
157
+ d = pop_dir(pop_id)
158
+ os.makedirs(d, exist_ok=True)
159
+
160
+ part_of = (container_id % NUM_PARTITIONS).astype(np.int64)
161
+ order = np.argsort(part_of, kind="stable")
162
+ part_sorted = part_of[order]
163
+ bounds = np.searchsorted(part_sorted, np.arange(NUM_PARTITIONS + 1))
164
+ partition_meta = []
165
+ for p in range(NUM_PARTITIONS):
166
+ sl = order[bounds[p]:bounds[p + 1]]
167
+ cols = {k: codes[k][sl] for k in DIM_NAMES}
168
+ cols["object_id"] = object_id[sl]
169
+ cols["container_id"] = container_id[sl]
170
+ cols["difficulty"] = difficulty[sl]
171
+ cols["safety_critical"] = safety_critical[sl]
172
+ np.savez_compressed(os.path.join(d, f"part-{p:04d}.npz"), **cols)
173
+ partition_meta.append({"partition": p, "rows": int(sl.size)})
174
+
175
+ # exact per-dimension counts (dataset card)
176
+ dim_counts = {
177
+ k: {DIMENSIONS[k][i]: int(c) for i, c in enumerate(np.bincount(codes[k], minlength=len(DIMENSIONS[k])))}
178
+ for k in DIM_NAMES
179
+ }
180
+
181
+ train_mix = _training_mix(codes, rng)
182
+
183
+ meta = {
184
+ "population_id": pop_id,
185
+ "name": name,
186
+ "created_at": _now(),
187
+ "seed": seed,
188
+ "num_objects": total,
189
+ "num_containers": num_containers,
190
+ "num_partitions": NUM_PARTITIONS,
191
+ "avg_objects_per_container": round(total / num_containers, 2),
192
+ "dimensions": DIMENSIONS,
193
+ "dim_counts": dim_counts,
194
+ "partitions": partition_meta,
195
+ "safety_critical_count": int(safety_critical.sum()),
196
+ "train_mix": train_mix,
197
+ }
198
+ with open(os.path.join(d, "meta.json"), "w") as f:
199
+ json.dump(meta, f)
200
+ return meta
201
+
202
+
203
+ def _training_mix(codes: Dict[str, np.ndarray], rng: np.random.Generator) -> List[Dict]:
204
+ """Joint (class, weather, lighting) proportions the *training* set had.
205
+
206
+ Derived from the eval mix with deliberate, deterministic under-representation of
207
+ hard conditions (night, rain/fog/snow, VRU at night) so shift detection has
208
+ honest signal: shift = eval share vs train share of the same cohort.
209
+ """
210
+ n = codes["class"].shape[0]
211
+ key = (codes["class"].astype(np.int64) * 100
212
+ + codes["weather"].astype(np.int64) * 10
213
+ + codes["lighting"].astype(np.int64))
214
+ uniq, counts = np.unique(key, return_counts=True)
215
+ eval_share = counts / n
216
+
217
+ cls, wx, lt = uniq // 100, (uniq // 10) % 10, uniq % 10
218
+ factor = np.ones(uniq.shape)
219
+ factor[lt == 1] *= 0.55 # trained on less night
220
+ factor[wx >= 1] *= 0.70 # less rain/fog/snow
221
+ factor[(np.isin(cls, [1, 2])) & (lt == 1)] *= 0.75 # far fewer night VRUs
222
+ train_share = eval_share * factor
223
+ train_share = train_share / train_share.sum()
224
+
225
+ out = []
226
+ for i in range(uniq.size):
227
+ out.append({
228
+ "class": DIMENSIONS["class"][int(cls[i])],
229
+ "weather": DIMENSIONS["weather"][int(wx[i])],
230
+ "lighting": DIMENSIONS["lighting"][int(lt[i])],
231
+ "train_share": float(train_share[i]),
232
+ "eval_share": float(eval_share[i]),
233
+ "eval_count": int(counts[i]),
234
+ })
235
+ return out
236
+
237
+
238
+ # ------------------------------------------------------------------ loading
239
+
240
+
241
+ _PART_CACHE: Dict[Tuple[str, int], Dict[str, np.ndarray]] = {}
242
+ _CACHE_LOCK = threading.Lock()
243
+
244
+
245
+ def load_meta(pop_id: str) -> Optional[Dict]:
246
+ path = os.path.join(pop_dir(pop_id), "meta.json")
247
+ if not os.path.exists(path):
248
+ return None
249
+ with open(path) as f:
250
+ return json.load(f)
251
+
252
+
253
+ def list_populations() -> List[Dict]:
254
+ base = os.path.join(MEGA_ROOT, "populations")
255
+ if not os.path.isdir(base):
256
+ return []
257
+ metas = []
258
+ for pid in sorted(os.listdir(base)):
259
+ m = load_meta(pid)
260
+ if m:
261
+ metas.append(m)
262
+ return metas
263
+
264
+
265
+ def load_partition(pop_id: str, partition: int) -> Dict[str, np.ndarray]:
266
+ key = (pop_id, partition)
267
+ with _CACHE_LOCK:
268
+ if key in _PART_CACHE:
269
+ return _PART_CACHE[key]
270
+ path = os.path.join(pop_dir(pop_id), f"part-{partition:04d}.npz")
271
+ with np.load(path) as z:
272
+ cols = {k: z[k] for k in z.files}
273
+ with _CACHE_LOCK:
274
+ if len(_PART_CACHE) > 96: # bounded partition cache
275
+ _PART_CACHE.clear()
276
+ _PART_CACHE[key] = cols
277
+ return cols
278
+
279
+
280
+ def partition_of_container(container_id: int) -> int:
281
+ return int(container_id) % NUM_PARTITIONS
sensorflow/megaeval/runs.py ADDED
@@ -0,0 +1,704 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluation runs: first-class async jobs producing cubes, error indexes and sketches.
2
+
3
+ Lifecycle: create -> queued -> running (workers over partitions, partial stats)
4
+ -> reducing -> materializing -> published (or failed)
5
+
6
+ Each worker processes one population partition (Spark-executor stand-in):
7
+ 1. vectorized model simulation (deterministic given lineage seed)
8
+ 2. partial sufficient statistics grouped by the 9 cube dims
9
+ 3. per-object outputs persisted for forensic drill-down / honest scan fallback
10
+ 4. error rows + sketch updates + container partial aggregates
11
+
12
+ Reduce = concat partials + groupby-sum (cube.reduce_partials).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import os
20
+ import platform
21
+ import threading
22
+ import time
23
+ import uuid
24
+ from concurrent.futures import ThreadPoolExecutor
25
+ from datetime import datetime, timezone
26
+ from typing import Dict, List, Optional
27
+
28
+ import numpy as np
29
+ import pandas as pd
30
+
31
+ from sensorflow.megaeval import cube as cube_mod
32
+ from sensorflow.megaeval import population as pop_mod
33
+ from sensorflow.megaeval.population import DIMENSIONS, DIM_NAMES
34
+ from sensorflow.megaeval.sketches import HyperLogLog, QuantileHistogram, Reservoir
35
+
36
+ ERROR_TYPES = ["FN", "FP", "LOCALIZATION", "ANOMALY", "LOW_CONF"]
37
+ CONTAINER_STATUS = ["ok", "warn", "critical"]
38
+ IOU_MATCH_THRESHOLD = 0.5
39
+ EVALUATOR_CODE_VERSION = "megaeval-1.0.0"
40
+ METRIC_VERSION = "metrics-1.0"
41
+
42
+
43
+ def _now() -> str:
44
+ return datetime.now(timezone.utc).isoformat()
45
+
46
+
47
+ def run_dir(run_id: str) -> str:
48
+ return os.path.join(pop_mod.MEGA_ROOT, "runs", run_id)
49
+
50
+
51
+ # ------------------------------------------------------------------ model profiles
52
+
53
+
54
+ def model_params(model_version: str, overrides: Optional[Dict] = None) -> Dict:
55
+ """Deterministic model skill profile derived from the version string.
56
+
57
+ Overrides let tests/demos inject regressions (e.g. worse night detection).
58
+ """
59
+ h = int(hashlib.sha256(model_version.encode()).hexdigest()[:8], 16)
60
+ r = np.random.default_rng(h)
61
+ params = {
62
+ "base_detect": 0.972 + float(r.uniform(-0.01, 0.015)),
63
+ "difficulty_weight": 0.18,
64
+ "night_penalty": 0.030 + float(r.uniform(0, 0.02)),
65
+ "rain_penalty": 0.020 + float(r.uniform(0, 0.015)),
66
+ "occlusion_penalty": 0.050,
67
+ "vru_penalty": 0.020 + float(r.uniform(0, 0.015)),
68
+ "far_penalty": 0.045,
69
+ "iou_base": 0.875 + float(r.uniform(-0.01, 0.015)),
70
+ "iou_difficulty": 0.28,
71
+ "fp_per_container": 0.30 + float(r.uniform(0, 0.08)),
72
+ "anomaly_base": 0.018,
73
+ "low_conf_threshold": 0.45,
74
+ }
75
+ params.update(overrides or {})
76
+ return params
77
+
78
+
79
+ # ------------------------------------------------------------------ run entity
80
+
81
+
82
+ class EvaluationRun:
83
+ def __init__(self, population_id: str, model_version: str,
84
+ overrides: Optional[Dict] = None, seed: Optional[int] = None,
85
+ worker_delay_s: float = 0.0, workers: int = 4,
86
+ label_version: str = "labels-v1",
87
+ threshold_config: Optional[Dict] = None,
88
+ sampling_config: Optional[Dict] = None):
89
+ self.run_id = f"eval-{uuid.uuid4().hex[:10]}"
90
+ self.population_id = population_id
91
+ self.model_version = model_version
92
+ self.overrides = overrides or {}
93
+ self.seed = seed if seed is not None else int(
94
+ hashlib.sha256(f"{population_id}|{model_version}|{label_version}".encode()
95
+ ).hexdigest()[:8], 16)
96
+ self.worker_delay_s = worker_delay_s
97
+ self.workers = workers
98
+ self.status = "created"
99
+ self.error: Optional[str] = None
100
+ self.created_at = _now()
101
+ self.started_at: Optional[str] = None
102
+ self.published_at: Optional[str] = None
103
+ self.partitions_total = 0
104
+ self.partitions_done = 0
105
+ self.objects_total = 0
106
+ self.objects_processed = 0
107
+ self.throughput_objs_per_s = 0.0
108
+ self.eta_s: Optional[float] = None
109
+ self.headline: Dict = {}
110
+ self.per_class: Dict = {}
111
+ self.lineage = {
112
+ "evaluation_id": self.run_id,
113
+ "dataset_version": population_id,
114
+ "model_version": model_version,
115
+ "model_checkpoint": f"ckpt-{hashlib.sha256(model_version.encode()).hexdigest()[:10]}",
116
+ "label_version": label_version,
117
+ "evaluator_code_version": EVALUATOR_CODE_VERSION,
118
+ "metric_version": METRIC_VERSION,
119
+ "threshold_config": {"iou_match": IOU_MATCH_THRESHOLD,
120
+ **(threshold_config or {})},
121
+ "sampling_config": sampling_config or {"method": "stratified_risk_weighted",
122
+ "target_n": 1500},
123
+ "seed": self.seed,
124
+ "hardware": f"{platform.machine()}/{platform.system()} single-node",
125
+ "timestamp": self.created_at,
126
+ }
127
+
128
+ def progress_dict(self) -> Dict:
129
+ pct = (self.partitions_done / self.partitions_total * 100) if self.partitions_total else 0.0
130
+ return {
131
+ "run_id": self.run_id,
132
+ "population_id": self.population_id,
133
+ "model_version": self.model_version,
134
+ "status": self.status,
135
+ "percent": round(pct, 2),
136
+ "partitions_done": self.partitions_done,
137
+ "partitions_total": self.partitions_total,
138
+ "objects_processed": int(self.objects_processed),
139
+ "objects_total": int(self.objects_total),
140
+ "workers": self.workers,
141
+ "throughput_objs_per_s": round(self.throughput_objs_per_s, 1),
142
+ "eta_s": round(self.eta_s, 1) if self.eta_s is not None else None,
143
+ "error": self.error,
144
+ }
145
+
146
+ def to_dict(self) -> Dict:
147
+ return {
148
+ **self.progress_dict(),
149
+ "created_at": self.created_at,
150
+ "started_at": self.started_at,
151
+ "published_at": self.published_at,
152
+ "lineage": self.lineage,
153
+ "headline": self.headline,
154
+ "per_class": self.per_class,
155
+ "overrides": self.overrides,
156
+ }
157
+
158
+
159
+ # ------------------------------------------------------------------ partition evaluation
160
+
161
+
162
+ def evaluate_partition(pop_id: str, partition: int, params: Dict, seed: int) -> Dict:
163
+ """Simulate the model on one partition; return partials + per-object outputs.
164
+
165
+ Fully vectorized and deterministic for (lineage seed, partition).
166
+ """
167
+ cols = pop_mod.load_partition(pop_id, partition)
168
+ n = cols["object_id"].shape[0]
169
+ rng = np.random.default_rng((seed * 1_000_003 + partition) % (2**63))
170
+
171
+ difficulty = cols["difficulty"].astype(np.float64)
172
+ p_det = params["base_detect"] - difficulty * params["difficulty_weight"]
173
+ p_det -= params["night_penalty"] * (cols["lighting"] == 1)
174
+ p_det -= params["rain_penalty"] * (cols["weather"] >= 1)
175
+ p_det -= params["occlusion_penalty"] * (cols["occlusion"] == 2)
176
+ p_det -= params["vru_penalty"] * np.isin(cols["class"], [1, 2, 3])
177
+ p_det -= params["far_penalty"] * (cols["distance_band"] == 3)
178
+ p_det = np.clip(p_det, 0.02, 0.995)
179
+ detected = rng.random(n) < p_det
180
+
181
+ iou = np.zeros(n, dtype=np.float64)
182
+ iou[detected] = np.clip(
183
+ rng.normal(params["iou_base"] - difficulty[detected] * params["iou_difficulty"], 0.07),
184
+ 0.02, 0.985)
185
+ tp = detected & (iou >= IOU_MATCH_THRESHOLD)
186
+ loc = detected & ~tp
187
+
188
+ conf = np.zeros(n, dtype=np.float64)
189
+ conf[detected] = np.clip(
190
+ 0.55 * iou[detected] + 0.25 * (1 - difficulty[detected])
191
+ + rng.normal(0.05, 0.08, size=int(detected.sum())), 0.01, 0.999)
192
+
193
+ anomaly_p = params["anomaly_base"] * (1 + 2.0 * difficulty
194
+ + 0.8 * (cols["scenario"] >= 4))
195
+ anomaly = rng.random(n) < anomaly_p
196
+ sensor_disagree = rng.random(n) < (0.04 + 0.25 * (cols["scenario"] == 5))
197
+
198
+ safety = cols["safety_critical"].astype(bool)
199
+
200
+ # ---- spurious FPs per container
201
+ uniq_containers, first_idx = np.unique(cols["container_id"], return_index=True)
202
+ ncont = uniq_containers.size
203
+ wx_c = cols["weather"][first_idx]
204
+ lt_c = cols["lighting"][first_idx]
205
+ lam = params["fp_per_container"] * (1 + 0.5 * (wx_c >= 1) + 0.6 * (lt_c == 1))
206
+ fp_counts = rng.poisson(lam)
207
+ total_fp = int(fp_counts.sum())
208
+ fp_container = np.repeat(uniq_containers, fp_counts)
209
+ fp_src = np.repeat(first_idx, fp_counts)
210
+ fp_cols = {k: cols[k][fp_src] for k in pop_mod.CONTAINER_DIMS}
211
+ fp_cols["class"] = rng.choice(6, size=total_fp,
212
+ p=[0.42, 0.18, 0.10, 0.08, 0.14, 0.08]).astype(np.int8)
213
+ fp_cols["sensor"] = rng.choice(3, size=total_fp).astype(np.int8)
214
+ fp_cols["distance_band"] = rng.choice(4, size=total_fp, p=[0.15, 0.35, 0.30, 0.20]).astype(np.int8)
215
+ fp_cols["speed_band"] = rng.choice(4, size=total_fp).astype(np.int8)
216
+ fp_cols["occlusion"] = rng.choice(3, size=total_fp, p=[0.5, 0.3, 0.2]).astype(np.int8)
217
+ fp_conf = np.clip(rng.beta(2.0, 4.5, size=total_fp) + 0.05, 0.01, 0.99)
218
+
219
+ # ---- contribution rows -> partial stats
220
+ gt = pd.DataFrame({k: cols[k] for k in DIM_NAMES})
221
+ gt["n"] = 1
222
+ gt["tp"] = tp.astype(np.int64)
223
+ gt["fp"] = loc.astype(np.int64) # badly localized box counts as FP too
224
+ gt["fn"] = (~tp).astype(np.int64)
225
+ gt["loc_err"] = loc.astype(np.int64)
226
+ gt["anomalies"] = anomaly.astype(np.int64)
227
+ gt["sum_iou"] = np.where(tp, iou, 0.0)
228
+ gt["sum_conf"] = np.where(detected, conf, 0.0)
229
+ gt["sum_conf2"] = np.where(detected, conf**2, 0.0)
230
+ gt["safety_n"] = safety.astype(np.int64)
231
+ gt["safety_tp"] = (safety & tp).astype(np.int64)
232
+ gt["reviewed"] = 0
233
+ gt["verified"] = 0
234
+
235
+ fpf = pd.DataFrame({k: fp_cols[k] for k in DIM_NAMES})
236
+ fpf["n"] = 0
237
+ fpf["tp"] = 0
238
+ fpf["fp"] = 1
239
+ fpf["fn"] = 0
240
+ fpf["loc_err"] = 0
241
+ fpf["anomalies"] = 0
242
+ fpf["sum_iou"] = 0.0
243
+ fpf["sum_conf"] = fp_conf
244
+ fpf["sum_conf2"] = fp_conf**2
245
+ fpf["safety_n"] = 0
246
+ fpf["safety_tp"] = 0
247
+ fpf["reviewed"] = 0
248
+ fpf["verified"] = 0
249
+
250
+ partial = cube_mod.partial_stats(pd.concat([gt, fpf], ignore_index=True))
251
+
252
+ # ---- error rows (error index)
253
+ err_frames = []
254
+ fn_mask = ~detected
255
+ for etype, mask, sev_base in (("FN", fn_mask, 0.75), ("LOCALIZATION", loc, 0.55)):
256
+ idx = np.where(mask)[0]
257
+ if idx.size == 0:
258
+ continue
259
+ code = ERROR_TYPES.index(etype)
260
+ err_frames.append(pd.DataFrame({
261
+ "object_id": cols["object_id"][idx],
262
+ "container_id": cols["container_id"][idx],
263
+ "error_type": np.full(idx.size, code, dtype=np.int8),
264
+ "severity": np.clip(sev_base + 0.25 * safety[idx] + 0.15 * difficulty[idx], 0, 1),
265
+ "confidence": conf[idx],
266
+ "safety_critical": safety[idx].astype(np.uint8),
267
+ "sensor_disagree": sensor_disagree[idx].astype(np.uint8),
268
+ **{k: cols[k][idx] for k in DIM_NAMES},
269
+ }))
270
+ an_idx = np.where(anomaly & tp)[0]
271
+ if an_idx.size:
272
+ err_frames.append(pd.DataFrame({
273
+ "object_id": cols["object_id"][an_idx],
274
+ "container_id": cols["container_id"][an_idx],
275
+ "error_type": np.full(an_idx.size, ERROR_TYPES.index("ANOMALY"), dtype=np.int8),
276
+ "severity": np.clip(0.4 + 0.3 * difficulty[an_idx], 0, 1),
277
+ "confidence": conf[an_idx],
278
+ "safety_critical": safety[an_idx].astype(np.uint8),
279
+ "sensor_disagree": sensor_disagree[an_idx].astype(np.uint8),
280
+ **{k: cols[k][an_idx] for k in DIM_NAMES},
281
+ }))
282
+ lc_idx = np.where(tp & (conf < params["low_conf_threshold"]))[0]
283
+ if lc_idx.size:
284
+ err_frames.append(pd.DataFrame({
285
+ "object_id": cols["object_id"][lc_idx],
286
+ "container_id": cols["container_id"][lc_idx],
287
+ "error_type": np.full(lc_idx.size, ERROR_TYPES.index("LOW_CONF"), dtype=np.int8),
288
+ "severity": np.clip(0.3 + 0.2 * difficulty[lc_idx], 0, 1),
289
+ "confidence": conf[lc_idx],
290
+ "safety_critical": safety[lc_idx].astype(np.uint8),
291
+ "sensor_disagree": sensor_disagree[lc_idx].astype(np.uint8),
292
+ **{k: cols[k][lc_idx] for k in DIM_NAMES},
293
+ }))
294
+ if total_fp:
295
+ err_frames.append(pd.DataFrame({
296
+ "object_id": np.full(total_fp, -1, dtype=np.int64),
297
+ "container_id": fp_container,
298
+ "error_type": np.full(total_fp, ERROR_TYPES.index("FP"), dtype=np.int8),
299
+ "severity": np.clip(0.45 + 0.4 * fp_conf, 0, 1),
300
+ "confidence": fp_conf,
301
+ "safety_critical": np.zeros(total_fp, dtype=np.uint8),
302
+ "sensor_disagree": (rng.random(total_fp) < 0.08).astype(np.uint8),
303
+ **{k: fp_cols[k] for k in DIM_NAMES},
304
+ }))
305
+ errors = (pd.concat(err_frames, ignore_index=True) if err_frames
306
+ else pd.DataFrame())
307
+
308
+ # ---- container partial aggregates
309
+ cdf = pd.DataFrame({
310
+ "container_id": cols["container_id"],
311
+ **{k: cols[k] for k in pop_mod.CONTAINER_DIMS},
312
+ "n_objects": 1,
313
+ "tp": tp.astype(np.int64),
314
+ "fp": loc.astype(np.int64),
315
+ "fn": (~tp).astype(np.int64),
316
+ "anomalies": anomaly.astype(np.int64),
317
+ "sum_iou": np.where(tp, iou, 0.0),
318
+ "safety_n": safety.astype(np.int64),
319
+ })
320
+ containers = cdf.groupby(["container_id"] + pop_mod.CONTAINER_DIMS,
321
+ as_index=False, observed=True).sum()
322
+ if total_fp:
323
+ fp_per = pd.Series(fp_container).value_counts()
324
+ containers["fp"] = containers["fp"] + containers["container_id"].map(fp_per).fillna(0).astype(np.int64)
325
+
326
+ # ---- sketches (per partition, merged at reduce)
327
+ conf_hist = QuantileHistogram(0, 1, 64)
328
+ conf_hist.add(np.concatenate([conf[detected], fp_conf]))
329
+ iou_hist = QuantileHistogram(0, 1, 64)
330
+ iou_hist.add(iou[tp])
331
+ hll = HyperLogLog(12)
332
+ hll.add_ids(uniq_containers)
333
+ reservoir = Reservoir(k=100, seed=partition)
334
+ if len(errors):
335
+ reservoir.add_ids(errors["object_id"].to_numpy()[errors["object_id"].to_numpy() >= 0][:2000])
336
+
337
+ # ---- per-object outputs (forensic view + scan fallback + brute-force tests)
338
+ obj_out = {
339
+ "object_id": cols["object_id"],
340
+ "container_id": cols["container_id"],
341
+ "detected": detected.astype(np.uint8),
342
+ "tp": tp.astype(np.uint8),
343
+ "iou": iou.astype(np.float32),
344
+ "confidence": conf.astype(np.float32),
345
+ "anomaly": anomaly.astype(np.uint8),
346
+ "sensor_disagree": sensor_disagree.astype(np.uint8),
347
+ }
348
+ fp_out = {
349
+ "container_id": fp_container,
350
+ "confidence": fp_conf.astype(np.float32),
351
+ **{k: fp_cols[k] for k in DIM_NAMES},
352
+ }
353
+
354
+ return {"partial": partial, "errors": errors, "containers": containers,
355
+ "conf_hist": conf_hist, "iou_hist": iou_hist, "hll": hll,
356
+ "reservoir": reservoir, "objects": obj_out, "fps": fp_out,
357
+ "rows": n}
358
+
359
+
360
+ # ------------------------------------------------------------------ store
361
+
362
+
363
+ class MegaStore:
364
+ """Registry of populations + runs, with lazily-loaded run artifacts."""
365
+
366
+ def __init__(self):
367
+ self.lock = threading.RLock()
368
+ self.runs: Dict[str, EvaluationRun] = {}
369
+ self._artifacts: Dict[str, Dict] = {}
370
+ self.router = cube_mod.QueryRouter()
371
+ self._load_persisted_runs()
372
+
373
+ def _load_persisted_runs(self) -> None:
374
+ base = os.path.join(pop_mod.MEGA_ROOT, "runs")
375
+ if not os.path.isdir(base):
376
+ return
377
+ for rid in sorted(os.listdir(base)):
378
+ meta_path = os.path.join(base, rid, "run.json")
379
+ if not os.path.exists(meta_path):
380
+ continue
381
+ try:
382
+ with open(meta_path) as f:
383
+ d = json.load(f)
384
+ run = EvaluationRun.__new__(EvaluationRun)
385
+ run.__dict__.update(d["state"])
386
+ self.runs[run.run_id] = run
387
+ except Exception:
388
+ continue
389
+
390
+ def save_run(self, run: EvaluationRun) -> None:
391
+ d = run_dir(run.run_id)
392
+ os.makedirs(d, exist_ok=True)
393
+ state = {k: v for k, v in run.__dict__.items() if not k.startswith("_")}
394
+ with open(os.path.join(d, "run.json"), "w") as f:
395
+ json.dump({"state": state}, f)
396
+
397
+ # ---- artifacts
398
+
399
+ def artifacts(self, run_id: str) -> Dict:
400
+ with self.lock:
401
+ if run_id in self._artifacts:
402
+ return self._artifacts[run_id]
403
+ d = run_dir(run_id)
404
+ art: Dict = {}
405
+ cube_path = os.path.join(d, "cube.npz")
406
+ if os.path.exists(cube_path):
407
+ art["cube"] = cube_mod.load_cube(cube_path)
408
+ err_path = os.path.join(d, "errors.npz")
409
+ if os.path.exists(err_path):
410
+ with np.load(err_path) as z:
411
+ art["errors"] = pd.DataFrame({k: z[k] for k in z.files})
412
+ cont_path = os.path.join(d, "containers.npz")
413
+ if os.path.exists(cont_path):
414
+ with np.load(cont_path) as z:
415
+ art["containers"] = pd.DataFrame({k: z[k] for k in z.files})
416
+ emb_path = os.path.join(d, "embeddings.npz")
417
+ if os.path.exists(emb_path):
418
+ with np.load(emb_path) as z:
419
+ art["emb_ids"] = z["container_id"]
420
+ art["emb"] = z["emb"]
421
+ sk_path = os.path.join(d, "sketches.npz")
422
+ if os.path.exists(sk_path):
423
+ with np.load(sk_path) as z:
424
+ art["sketches"] = {
425
+ "confidence": QuantileHistogram.from_arrays(0, 1, z["conf_hist"]),
426
+ "iou": QuantileHistogram.from_arrays(0, 1, z["iou_hist"]),
427
+ "container_hll_estimate": float(z["hll_estimate"]),
428
+ "exemplar_ids": z["exemplars"].tolist(),
429
+ }
430
+ rev_path = os.path.join(d, "review.json")
431
+ if os.path.exists(rev_path):
432
+ with open(rev_path) as f:
433
+ art["review"] = json.load(f)
434
+ with self.lock:
435
+ if len(self._artifacts) > 6:
436
+ self._artifacts.pop(next(iter(self._artifacts)))
437
+ self._artifacts[run_id] = art
438
+ return art
439
+
440
+ def invalidate_artifacts(self, run_id: str) -> None:
441
+ with self.lock:
442
+ self._artifacts.pop(run_id, None)
443
+ self.router.cache.invalidate()
444
+
445
+ # ---- lifecycle
446
+
447
+ def create_run(self, **kwargs) -> EvaluationRun:
448
+ run = EvaluationRun(**kwargs)
449
+ meta = pop_mod.load_meta(run.population_id)
450
+ if meta is None:
451
+ raise KeyError(f"Unknown population {run.population_id}")
452
+ run.partitions_total = meta["num_partitions"]
453
+ run.objects_total = meta["num_objects"]
454
+ run.status = "queued"
455
+ with self.lock:
456
+ self.runs[run.run_id] = run
457
+ self.save_run(run)
458
+ return run
459
+
460
+ def start_async(self, run: EvaluationRun) -> None:
461
+ t = threading.Thread(target=self._execute, args=(run,), daemon=True)
462
+ t.start()
463
+
464
+ def execute_sync(self, run: EvaluationRun) -> None:
465
+ self._execute(run)
466
+
467
+ def _execute(self, run: EvaluationRun) -> None:
468
+ try:
469
+ run.status = "running"
470
+ run.started_at = _now()
471
+ t_start = time.perf_counter()
472
+ params = model_params(run.model_version, run.overrides)
473
+ d = run_dir(run.run_id)
474
+ os.makedirs(d, exist_ok=True)
475
+
476
+ partials: List[pd.DataFrame] = []
477
+ error_frames: List[pd.DataFrame] = []
478
+ container_frames: List[pd.DataFrame] = []
479
+ conf_hist = QuantileHistogram(0, 1, 64)
480
+ iou_hist = QuantileHistogram(0, 1, 64)
481
+ hll = HyperLogLog(12)
482
+ reservoir = Reservoir(k=200, seed=run.seed)
483
+ agg_lock = threading.Lock()
484
+
485
+ def work(p: int) -> None:
486
+ res = evaluate_partition(run.population_id, p, params, run.seed)
487
+ if run.worker_delay_s:
488
+ time.sleep(run.worker_delay_s) # simulated distributed I/O latency
489
+ # uncompressed npz: these are hot-path worker writes (few MB total)
490
+ np.savez(os.path.join(d, f"objects-part-{p:04d}.npz"), **res["objects"])
491
+ np.savez(os.path.join(d, f"fp-part-{p:04d}.npz"), **res["fps"])
492
+ nonlocal conf_hist, iou_hist, hll, reservoir
493
+ with agg_lock:
494
+ partials.append(res["partial"])
495
+ if len(res["errors"]):
496
+ error_frames.append(res["errors"])
497
+ container_frames.append(res["containers"])
498
+ conf_hist = conf_hist.merge(res["conf_hist"])
499
+ iou_hist = iou_hist.merge(res["iou_hist"])
500
+ hll = hll.merge(res["hll"])
501
+ reservoir = reservoir.merge(res["reservoir"])
502
+ run.partitions_done += 1
503
+ run.objects_processed += res["rows"]
504
+ elapsed = time.perf_counter() - t_start
505
+ run.throughput_objs_per_s = run.objects_processed / max(elapsed, 1e-6)
506
+ remaining = run.objects_total - run.objects_processed
507
+ run.eta_s = remaining / max(run.throughput_objs_per_s, 1e-6)
508
+
509
+ with ThreadPoolExecutor(max_workers=run.workers) as ex:
510
+ list(ex.map(work, range(run.partitions_total)))
511
+
512
+ run.status = "reducing"
513
+ cube_df = cube_mod.reduce_partials(partials)
514
+
515
+ run.status = "materializing"
516
+ cube_mod.save_cube(os.path.join(d, "cube.npz"), cube_df)
517
+
518
+ errors = (pd.concat(error_frames, ignore_index=True)
519
+ if error_frames else pd.DataFrame())
520
+ if len(errors):
521
+ errors = errors.reset_index(drop=True)
522
+ errors["error_id"] = np.arange(len(errors), dtype=np.int64)
523
+ risk = (errors["severity"] * 0.5
524
+ + errors["safety_critical"] * 0.3
525
+ + (1 - errors["confidence"]) * 0.15
526
+ + errors["sensor_disagree"] * 0.05)
527
+ errors["risk_score"] = np.clip(risk, 0, 1)
528
+ np.savez_compressed(os.path.join(d, "errors.npz"),
529
+ **{c: errors[c].to_numpy() for c in errors.columns})
530
+
531
+ containers = (pd.concat(container_frames, ignore_index=True)
532
+ .groupby(["container_id"] + pop_mod.CONTAINER_DIMS,
533
+ as_index=False, observed=True).sum())
534
+ containers["reviewed"] = 0
535
+ containers["verified"] = 0
536
+ err_share = (containers["fn"] + containers["fp"]) / containers["n_objects"].clip(lower=1)
537
+ containers["risk_score"] = np.clip(
538
+ 0.55 * err_share + 0.25 * (containers["anomalies"] / containers["n_objects"].clip(lower=1))
539
+ + 0.20 * (containers["safety_n"] > 0), 0, 1).round(4)
540
+ containers["status"] = np.select(
541
+ [containers["risk_score"] >= 0.45, containers["risk_score"] >= 0.2],
542
+ [2, 1], default=0).astype(np.int8)
543
+ np.savez_compressed(os.path.join(d, "containers.npz"),
544
+ **{c: containers[c].to_numpy() for c in containers.columns})
545
+
546
+ self._build_embeddings(d, containers)
547
+
548
+ np.savez_compressed(os.path.join(d, "sketches.npz"),
549
+ conf_hist=conf_hist.counts, iou_hist=iou_hist.counts,
550
+ hll_estimate=np.array(hll.estimate()),
551
+ exemplars=np.array(reservoir.items, dtype=np.int64))
552
+
553
+ headline_rows, _ = cube_mod.aggregate(
554
+ cube_df, None, None,
555
+ ["n", "tp", "fp", "fn", "loc_err", "anomalies", "precision", "recall", "f1",
556
+ "mean_iou", "anomaly_rate", "safety_recall", "conf_mean", "error_rate"], 1)
557
+ run.headline = headline_rows[0]
558
+ run.headline["containers"] = int(containers.shape[0])
559
+ run.headline["containers_hll_estimate"] = round(hll.estimate(), 1)
560
+
561
+ per_class_rows, _ = cube_mod.aggregate(
562
+ cube_df, None, ["class"],
563
+ ["n", "precision", "recall", "f1", "mean_iou", "safety_recall", "anomaly_rate"], 50)
564
+ run.per_class = {r["class"]: r for r in per_class_rows}
565
+
566
+ run.status = "published"
567
+ run.published_at = _now()
568
+ run.eta_s = 0.0
569
+ self.save_run(run)
570
+ self.invalidate_artifacts(run.run_id)
571
+ except Exception as e: # pragma: no cover - defensive
572
+ run.status = "failed"
573
+ run.error = str(e)
574
+ self.save_run(run)
575
+
576
+ @staticmethod
577
+ def _build_embeddings(d: str, containers: pd.DataFrame) -> None:
578
+ """32-d container embeddings: structural features + seeded random projection."""
579
+ feats = []
580
+ nobj = containers["n_objects"].clip(lower=1).to_numpy(dtype=np.float64)
581
+ for col in ("tp", "fp", "fn", "anomalies", "safety_n"):
582
+ feats.append(containers[col].to_numpy(dtype=np.float64) / nobj)
583
+ feats.append(containers["sum_iou"].to_numpy(dtype=np.float64)
584
+ / containers["tp"].clip(lower=1).to_numpy(dtype=np.float64))
585
+ feats.append(np.log1p(nobj) / 4.0)
586
+ feats.append(containers["risk_score"].to_numpy(dtype=np.float64))
587
+ for dim in pop_mod.CONTAINER_DIMS:
588
+ k = len(DIMENSIONS[dim])
589
+ onehot = np.eye(k)[containers[dim].to_numpy(dtype=np.int64)]
590
+ feats.extend(onehot.T)
591
+ X = np.vstack(feats).T # (n, ~25)
592
+ rng = np.random.default_rng(1234)
593
+ proj = rng.normal(size=(X.shape[1], 32)) / np.sqrt(X.shape[1])
594
+ emb = X @ proj
595
+ emb = emb / np.clip(np.linalg.norm(emb, axis=1, keepdims=True), 1e-9, None)
596
+ np.savez_compressed(os.path.join(d, "embeddings.npz"),
597
+ container_id=containers["container_id"].to_numpy(),
598
+ emb=emb.astype(np.float32))
599
+
600
+ # ---- forensic drill-down (deepest level; loads exactly one partition)
601
+
602
+ def container_objects(self, run: EvaluationRun, container_id: int) -> List[Dict]:
603
+ p = pop_mod.partition_of_container(container_id)
604
+ pop_cols = pop_mod.load_partition(run.population_id, p)
605
+ d = run_dir(run.run_id)
606
+ with np.load(os.path.join(d, f"objects-part-{p:04d}.npz")) as z:
607
+ out_cols = {k: z[k] for k in z.files}
608
+ mask = pop_cols["container_id"] == container_id
609
+ idx = np.where(mask)[0]
610
+ rows = []
611
+ for i in idx.tolist():
612
+ det = bool(out_cols["detected"][i])
613
+ tp = bool(out_cols["tp"][i])
614
+ rows.append({
615
+ "annotation_id": f"obj-{int(pop_cols['object_id'][i])}",
616
+ "container_id": int(container_id),
617
+ **{k: DIMENSIONS[k][int(pop_cols[k][i])] for k in DIM_NAMES},
618
+ "safety_critical": bool(pop_cols["safety_critical"][i]),
619
+ "difficulty": round(float(pop_cols["difficulty"][i]), 4),
620
+ "detected": det,
621
+ "outcome": "TP" if tp else ("LOCALIZATION" if det else "FN"),
622
+ "iou": round(float(out_cols["iou"][i]), 4) if det else None,
623
+ "confidence": round(float(out_cols["confidence"][i]), 4) if det else None,
624
+ "anomaly": bool(out_cols["anomaly"][i]),
625
+ "sensor_disagree": bool(out_cols["sensor_disagree"][i]),
626
+ })
627
+ with np.load(os.path.join(d, f"fp-part-{p:04d}.npz")) as z:
628
+ fp_cols = {k: z[k] for k in z.files}
629
+ fp_mask = fp_cols["container_id"] == container_id
630
+ for j in np.where(fp_mask)[0].tolist():
631
+ rows.append({
632
+ "annotation_id": f"fp-{run.run_id[-4:]}-{p}-{j}",
633
+ "container_id": int(container_id),
634
+ **{k: DIMENSIONS[k][int(fp_cols[k][j])] for k in DIM_NAMES},
635
+ "safety_critical": False,
636
+ "difficulty": None,
637
+ "detected": True,
638
+ "outcome": "FP",
639
+ "iou": 0.0,
640
+ "confidence": round(float(fp_cols["confidence"][j]), 4),
641
+ "anomaly": False,
642
+ "sensor_disagree": False,
643
+ })
644
+ return rows
645
+
646
+ # ---- honest record-scan fallback for the query router
647
+
648
+ def scan_records(self, run: EvaluationRun, filters: Optional[Dict],
649
+ group_by: Optional[List[str]], metrics: Optional[List[str]],
650
+ limit: int) -> tuple:
651
+ frames = []
652
+ d = run_dir(run.run_id)
653
+ for p in range(run.partitions_total):
654
+ pop_cols = pop_mod.load_partition(run.population_id, p)
655
+ with np.load(os.path.join(d, f"objects-part-{p:04d}.npz")) as z:
656
+ df = pd.DataFrame({
657
+ **{k: pop_cols[k] for k in DIM_NAMES},
658
+ "container_id": pop_cols["container_id"],
659
+ "tp": z["tp"].astype(np.int64),
660
+ "fn": (1 - z["tp"]).astype(np.int64),
661
+ "fp": (z["detected"] & ~z["tp"].astype(bool)).astype(np.int64),
662
+ "anomalies": z["anomaly"].astype(np.int64),
663
+ "iou": z["iou"],
664
+ })
665
+ frames.append(df)
666
+ full = pd.concat(frames, ignore_index=True)
667
+ full = cube_mod.apply_filters(full, filters)
668
+ gb = [g for g in (group_by or []) if g in list(full.columns)]
669
+ if not gb:
670
+ gb = ["container_id"]
671
+ agg = full.groupby(gb, as_index=False, observed=True).agg(
672
+ n=("tp", "size"), tp=("tp", "sum"), fp=("fp", "sum"), fn=("fn", "sum"),
673
+ anomalies=("anomalies", "sum"), mean_iou=("iou", "mean"))
674
+ agg["recall"] = agg["tp"] / (agg["tp"] + agg["fn"]).clip(lower=1)
675
+ agg = agg.sort_values("n", ascending=False).head(limit)
676
+ rows = []
677
+ for _, r in agg.iterrows():
678
+ row = {}
679
+ for g in gb:
680
+ row[g] = DIMENSIONS[g][int(r[g])] if g in DIM_NAMES else int(r[g])
681
+ row.update({"n": int(r["n"]), "tp": int(r["tp"]), "fp": int(r["fp"]),
682
+ "fn": int(r["fn"]), "anomalies": int(r["anomalies"]),
683
+ "mean_iou": round(float(r["mean_iou"]), 6),
684
+ "recall": round(float(r["recall"]), 6)})
685
+ rows.append(row)
686
+ return rows, len(full)
687
+
688
+
689
+ _STORE: Optional[MegaStore] = None
690
+ _STORE_LOCK = threading.Lock()
691
+
692
+
693
+ def get_mega_store() -> MegaStore:
694
+ global _STORE
695
+ with _STORE_LOCK:
696
+ if _STORE is None:
697
+ _STORE = MegaStore()
698
+ return _STORE
699
+
700
+
701
+ def reset_mega_store() -> None:
702
+ global _STORE
703
+ with _STORE_LOCK:
704
+ _STORE = None
sensorflow/megaeval/sampling.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Human review as statistical sampling.
2
+
3
+ Instead of a flat "review N labels" queue, review effort is a designed sample:
4
+
5
+ population -> containers -> suspicious containers -> candidate pool
6
+ -> stratified risk-weighted selection -> reviews -> CIs
7
+
8
+ Two estimands, each with its own sampling frame:
9
+ - precision: frame = predicted positives, strata = confidence tier x safety
10
+ - recall: frame = ground-truth objects, strata = model outcome x safety
11
+
12
+ Risk weighting happens through *allocation* (risky strata get more samples);
13
+ estimates stay honest because strata are combined with their true population
14
+ weights (classic stratified estimator):
15
+
16
+ p_hat = sum_h W_h * p_hat_h
17
+ SE = sqrt( sum_h W_h^2 * p_hat_h (1 - p_hat_h) / n_h ) (normal approx)
18
+
19
+ Per-stratum intervals use Wilson. Simulated reviewers agree with ground truth
20
+ with 98.5% fidelity (the synthetic stand-in for human accuracy).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import math
27
+ import os
28
+ from typing import Dict, List, Optional, Tuple
29
+
30
+ import numpy as np
31
+ import pandas as pd
32
+
33
+ from sensorflow.megaeval import population as pop_mod
34
+ from sensorflow.megaeval.population import DIM_NAMES
35
+ from sensorflow.megaeval.runs import EvaluationRun, MegaStore, run_dir
36
+
37
+ HUMAN_FIDELITY = 0.985
38
+ Z95 = 1.959964
39
+
40
+
41
+ def wilson_interval(k: int, n: int, z: float = Z95) -> Tuple[float, float]:
42
+ """Wilson score interval for a binomial proportion."""
43
+ if n == 0:
44
+ return (0.0, 1.0)
45
+ p = k / n
46
+ denom = 1 + z**2 / n
47
+ center = (p + z**2 / (2 * n)) / denom
48
+ margin = z * math.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) / denom
49
+ return (max(0.0, center - margin), min(1.0, center + margin))
50
+
51
+
52
+ def _load_unit_frames(store: MegaStore, run: EvaluationRun) -> Tuple[pd.DataFrame, pd.DataFrame]:
53
+ """One batch scan (NOT on the query path) to build the two sampling frames."""
54
+ d = run_dir(run.run_id)
55
+ gt_frames, pp_frames = [], []
56
+ for p in range(run.partitions_total):
57
+ pop_cols = pop_mod.load_partition(run.population_id, p)
58
+ with np.load(os.path.join(d, f"objects-part-{p:04d}.npz")) as z:
59
+ detected = z["detected"].astype(bool)
60
+ tp = z["tp"].astype(bool)
61
+ conf = z["confidence"]
62
+ base = pd.DataFrame({
63
+ "unit_id": pop_cols["object_id"],
64
+ "container_id": pop_cols["container_id"],
65
+ **{k: pop_cols[k] for k in DIM_NAMES},
66
+ "safety": pop_cols["safety_critical"].astype(bool),
67
+ "detected": detected, "tp": tp, "confidence": conf,
68
+ })
69
+ gt_frames.append(base)
70
+ pp = base[detected].copy()
71
+ pp["correct"] = pp["tp"]
72
+ pp_frames.append(pp)
73
+ with np.load(os.path.join(d, f"fp-part-{p:04d}.npz")) as z:
74
+ if z["container_id"].size:
75
+ fpdf = pd.DataFrame({
76
+ "unit_id": -np.arange(1, z["container_id"].size + 1) - p * 10_000_000,
77
+ "container_id": z["container_id"],
78
+ **{k: z[k] for k in DIM_NAMES},
79
+ "safety": False, "detected": True, "tp": False,
80
+ "confidence": z["confidence"], "correct": False,
81
+ })
82
+ pp_frames.append(fpdf)
83
+ return pd.concat(gt_frames, ignore_index=True), pd.concat(pp_frames, ignore_index=True)
84
+
85
+
86
+ def _stratify_precision(pp: pd.DataFrame) -> pd.Series:
87
+ tier = np.select([pp["confidence"] < 0.45, pp["confidence"] < 0.75], [0, 1], default=2)
88
+ return pd.Series(tier * 2 + pp["safety"].astype(int).to_numpy(), index=pp.index)
89
+
90
+
91
+ def _stratify_recall(gt: pd.DataFrame) -> pd.Series:
92
+ return pd.Series((~gt["detected"]).astype(int).to_numpy() * 2
93
+ + gt["safety"].astype(int).to_numpy(), index=gt.index)
94
+
95
+
96
+ _PRECISION_STRATA = {0: "low_conf", 1: "low_conf_safety", 2: "mid_conf", 3: "mid_conf_safety",
97
+ 4: "high_conf", 5: "high_conf_safety"}
98
+ _RECALL_STRATA = {0: "detected", 1: "detected_safety", 2: "missed", 3: "missed_safety"}
99
+ # risk-weighted allocation multipliers (riskier strata get proportionally more reviews)
100
+ _PRECISION_RISK_W = {0: 3.0, 1: 4.0, 2: 1.5, 3: 2.5, 4: 1.0, 5: 2.0}
101
+ _RECALL_RISK_W = {0: 1.0, 1: 2.0, 2: 3.0, 3: 5.0}
102
+
103
+
104
+ def _allocate(counts: Dict[int, int], risk_w: Dict[int, float], total_n: int) -> Dict[int, int]:
105
+ weights = {h: counts[h] * risk_w.get(h, 1.0) for h in counts}
106
+ wsum = sum(weights.values()) or 1.0
107
+ alloc = {}
108
+ for h, c in counts.items():
109
+ n_h = int(round(total_n * weights[h] / wsum))
110
+ alloc[h] = max(1, min(n_h, c)) if c > 0 else 0
111
+ return alloc
112
+
113
+
114
+ def build_review_plan(store: MegaStore, run: EvaluationRun,
115
+ target_n: Optional[int] = None, seed: int = 99) -> Dict:
116
+ """Build the sampling funnel + stratified selection. Persists review.json."""
117
+ target_n = target_n or run.lineage["sampling_config"].get("target_n", 1500)
118
+ rng = np.random.default_rng(seed + run.seed)
119
+ gt, pp = _load_unit_frames(store, run)
120
+ art = store.artifacts(run.run_id)
121
+ containers = art["containers"]
122
+ errors = art.get("errors", pd.DataFrame())
123
+
124
+ suspicious = int((containers["risk_score"] >= 0.2).sum())
125
+ candidates = int(len(errors)) if len(errors) else 0
126
+
127
+ plans = {}
128
+ for name, frame, strat_fn, labels, risk_w in (
129
+ ("precision", pp, _stratify_precision, _PRECISION_STRATA, _PRECISION_RISK_W),
130
+ ("recall", gt, _stratify_recall, _RECALL_STRATA, _RECALL_RISK_W)):
131
+ strata = strat_fn(frame)
132
+ counts = strata.value_counts().to_dict()
133
+ alloc = _allocate(counts, risk_w, target_n // 2)
134
+ selected = {}
135
+ for h, n_h in alloc.items():
136
+ if n_h <= 0:
137
+ continue
138
+ idx = frame.index[strata == h].to_numpy()
139
+ chosen = rng.choice(idx, size=min(n_h, idx.size), replace=False)
140
+ selected[str(h)] = frame.loc[chosen, "unit_id"].astype(np.int64).tolist()
141
+ plans[name] = {
142
+ "frame_size": int(len(frame)),
143
+ "strata": {str(h): {"label": labels[h], "N": int(counts.get(h, 0)),
144
+ "allocated": int(alloc.get(h, 0))}
145
+ for h in labels},
146
+ "selected_ids": selected,
147
+ }
148
+
149
+ selected_total = sum(len(v) for p in plans.values() for v in p["selected_ids"].values())
150
+ review = {
151
+ "run_id": run.run_id,
152
+ "method": "stratified risk-weighted sampling; Wilson per-stratum, "
153
+ "stratified normal-approx combined CI",
154
+ "target_n": target_n,
155
+ "funnel": {
156
+ "population_objects": int(run.objects_total),
157
+ "containers": int(len(containers)),
158
+ "suspicious_containers": suspicious,
159
+ "candidate_pool": candidates,
160
+ "statistically_selected": int(selected_total),
161
+ "reviewed": 0,
162
+ },
163
+ "plans": plans,
164
+ "executed": False,
165
+ "results": None,
166
+ }
167
+ with open(os.path.join(run_dir(run.run_id), "review.json"), "w") as f:
168
+ json.dump(review, f)
169
+ store.invalidate_artifacts(run.run_id)
170
+ return review
171
+
172
+
173
+ def execute_reviews(store: MegaStore, run: EvaluationRun, seed: int = 7) -> Dict:
174
+ """Simulate human review of the selected sample; compute CIs; update the cube."""
175
+ d = run_dir(run.run_id)
176
+ with open(os.path.join(d, "review.json")) as f:
177
+ review = json.load(f)
178
+ rng = np.random.default_rng(seed + run.seed)
179
+ gt, pp = _load_unit_frames(store, run)
180
+ gt_idx = gt.set_index("unit_id")
181
+ pp_idx = pp.set_index("unit_id")
182
+
183
+ results = {}
184
+ reviewed_units = [] # (dims..., verified) for cube update — recall-frame units only
185
+ total_reviewed = 0
186
+ for name, frame_idx, truth_col in (("precision", pp_idx, "correct"),
187
+ ("recall", gt_idx, "tp")):
188
+ plan = review["plans"][name]
189
+ strata_out = []
190
+ p_hat_comb, var_comb = 0.0, 0.0
191
+ frame_N = plan["frame_size"]
192
+ n_total = 0
193
+ for h, sinfo in plan["strata"].items():
194
+ ids = plan["selected_ids"].get(h, [])
195
+ if not ids:
196
+ continue
197
+ units = frame_idx.loc[[i for i in ids if i in frame_idx.index]]
198
+ if isinstance(units, pd.Series):
199
+ units = units.to_frame().T
200
+ truth = units[truth_col].astype(bool).to_numpy()
201
+ agrees = rng.random(truth.size) < HUMAN_FIDELITY
202
+ verdict = np.where(agrees, truth, ~truth)
203
+ k, n_h = int(verdict.sum()), int(verdict.size)
204
+ W_h = sinfo["N"] / frame_N
205
+ p_h = k / n_h if n_h else 0.0
206
+ lo, hi = wilson_interval(k, n_h)
207
+ strata_out.append({"stratum": sinfo["label"], "N": sinfo["N"], "n": n_h,
208
+ "correct": k, "p": round(p_h, 4),
209
+ "wilson_ci": [round(lo, 4), round(hi, 4)],
210
+ "weight": round(W_h, 4)})
211
+ p_hat_comb += W_h * p_h
212
+ var_comb += (W_h**2) * p_h * (1 - p_h) / max(n_h, 1)
213
+ n_total += n_h
214
+ total_reviewed += n_h
215
+ if name == "recall":
216
+ sub = units.copy()
217
+ sub["verified"] = verdict
218
+ reviewed_units.append(sub)
219
+ se = math.sqrt(var_comb)
220
+ results[name] = {
221
+ "estimate": round(p_hat_comb, 4),
222
+ "ci_low": round(max(0.0, p_hat_comb - Z95 * se), 4),
223
+ "ci_high": round(min(1.0, p_hat_comb + Z95 * se), 4),
224
+ "n_reviewed": n_total,
225
+ "frame_size": frame_N,
226
+ "method": review["method"],
227
+ "strata": strata_out,
228
+ }
229
+
230
+ # ---- fold reviews back into the cube + container table (reviewed/verified counters)
231
+ if reviewed_units:
232
+ ru = pd.concat(reviewed_units)
233
+ ru = ru.reset_index(drop=True)
234
+ upd = ru.groupby(DIM_NAMES, as_index=False, observed=True).agg(
235
+ add_reviewed=("verified", "size"), add_verified=("verified", "sum"))
236
+ from sensorflow.megaeval import cube as cube_mod
237
+ cube_path = os.path.join(d, "cube.npz")
238
+ cube_df = cube_mod.load_cube(cube_path)
239
+ merged = cube_df.merge(upd, on=DIM_NAMES, how="left")
240
+ merged["reviewed"] = merged["reviewed"] + merged["add_reviewed"].fillna(0).astype(np.int64)
241
+ merged["verified"] = merged["verified"] + merged["add_verified"].fillna(0).astype(np.int64)
242
+ cube_mod.save_cube(cube_path, merged.drop(columns=["add_reviewed", "add_verified"]))
243
+
244
+ cupd = ru.groupby("container_id", as_index=False).agg(
245
+ add_reviewed=("verified", "size"), add_verified=("verified", "sum"))
246
+ cont_path = os.path.join(d, "containers.npz")
247
+ with np.load(cont_path) as z:
248
+ cont = pd.DataFrame({k: z[k] for k in z.files})
249
+ cont = cont.merge(cupd, on="container_id", how="left")
250
+ cont["reviewed"] = cont["reviewed"] + cont["add_reviewed"].fillna(0).astype(np.int64)
251
+ cont["verified"] = cont["verified"] + cont["add_verified"].fillna(0).astype(np.int64)
252
+ cont = cont.drop(columns=["add_reviewed", "add_verified"])
253
+ np.savez_compressed(cont_path, **{c: cont[c].to_numpy() for c in cont.columns})
254
+
255
+ review["executed"] = True
256
+ review["funnel"]["reviewed"] = total_reviewed
257
+ review["results"] = results
258
+ with open(os.path.join(d, "review.json"), "w") as f:
259
+ json.dump(review, f)
260
+ store.invalidate_artifacts(run.run_id)
261
+ return review
sensorflow/megaeval/sketches.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lightweight approximate data structures for population statistics.
2
+
3
+ All numpy-based, mergeable (so they can be maintained per partition and reduced),
4
+ and each clearly reports whether an answer is exact or approximate.
5
+
6
+ - QuantileHistogram: fixed-bin histogram over a known range; percentile queries by
7
+ linear interpolation inside the winning bin. Stand-in for a t-digest.
8
+ - HyperLogLog: classic HLL with 2^p registers and numpy vectorized updates.
9
+ - Reservoir: uniform reservoir sample of exemplar ids (Algorithm R, vectorized-ish).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Dict, List, Optional
15
+
16
+ import numpy as np
17
+
18
+ # ---------------------------------------------------------------- hashing
19
+
20
+ _SPLITMIX_1 = np.uint64(0x9E3779B97F4A7C15)
21
+ _SPLITMIX_2 = np.uint64(0xBF58476D1CE4E5B9)
22
+ _SPLITMIX_3 = np.uint64(0x94D049BB133111EB)
23
+
24
+
25
+ def _hash64(values: np.ndarray) -> np.ndarray:
26
+ """Deterministic 64-bit mix (splitmix64) of integer identifiers."""
27
+ with np.errstate(over="ignore"):
28
+ z = values.astype(np.uint64) + _SPLITMIX_1
29
+ z = (z ^ (z >> np.uint64(30))) * _SPLITMIX_2
30
+ z = (z ^ (z >> np.uint64(27))) * _SPLITMIX_3
31
+ return z ^ (z >> np.uint64(31))
32
+
33
+
34
+ # ---------------------------------------------------------------- quantile histogram
35
+
36
+
37
+ class QuantileHistogram:
38
+ """Fixed-bin histogram over [lo, hi]; approximate percentiles via interpolation.
39
+
40
+ Mergeable: histograms with identical bin config add counts element-wise.
41
+ """
42
+
43
+ def __init__(self, lo: float = 0.0, hi: float = 1.0, bins: int = 64,
44
+ counts: Optional[np.ndarray] = None):
45
+ self.lo, self.hi, self.bins = float(lo), float(hi), int(bins)
46
+ self.counts = counts if counts is not None else np.zeros(bins, dtype=np.int64)
47
+
48
+ def add(self, values: np.ndarray) -> None:
49
+ if values.size == 0:
50
+ return
51
+ clipped = np.clip(values, self.lo, self.hi)
52
+ idx = np.minimum(
53
+ ((clipped - self.lo) / (self.hi - self.lo) * self.bins).astype(np.int64),
54
+ self.bins - 1)
55
+ self.counts += np.bincount(idx, minlength=self.bins)
56
+
57
+ def merge(self, other: "QuantileHistogram") -> "QuantileHistogram":
58
+ assert (self.lo, self.hi, self.bins) == (other.lo, other.hi, other.bins)
59
+ return QuantileHistogram(self.lo, self.hi, self.bins, self.counts + other.counts)
60
+
61
+ @property
62
+ def total(self) -> int:
63
+ return int(self.counts.sum())
64
+
65
+ def percentile(self, q: float) -> Optional[float]:
66
+ """Approximate q-th percentile (q in [0, 100])."""
67
+ total = self.total
68
+ if total == 0:
69
+ return None
70
+ target = total * (q / 100.0)
71
+ cum = np.cumsum(self.counts)
72
+ bin_idx = int(np.searchsorted(cum, target, side="left"))
73
+ bin_idx = min(bin_idx, self.bins - 1)
74
+ prev_cum = cum[bin_idx - 1] if bin_idx > 0 else 0
75
+ in_bin = self.counts[bin_idx]
76
+ frac = 0.5 if in_bin == 0 else float(np.clip((target - prev_cum) / in_bin, 0.0, 1.0))
77
+ width = (self.hi - self.lo) / self.bins
78
+ return self.lo + (bin_idx + frac) * width
79
+
80
+ def to_dict(self) -> Dict:
81
+ return {"lo": self.lo, "hi": self.hi, "bins": self.bins,
82
+ "counts": self.counts.tolist(), "exact": False}
83
+
84
+ @staticmethod
85
+ def from_arrays(lo: float, hi: float, counts: np.ndarray) -> "QuantileHistogram":
86
+ return QuantileHistogram(lo, hi, len(counts), counts.astype(np.int64))
87
+
88
+
89
+ # ---------------------------------------------------------------- HyperLogLog
90
+
91
+
92
+ class HyperLogLog:
93
+ """HLL cardinality estimator with 2^p registers (approximate).
94
+
95
+ Mergeable: element-wise max of registers.
96
+ """
97
+
98
+ def __init__(self, p: int = 12, registers: Optional[np.ndarray] = None):
99
+ self.p = int(p)
100
+ self.m = 1 << self.p
101
+ self.registers = registers if registers is not None else np.zeros(self.m, dtype=np.uint8)
102
+
103
+ def add_ids(self, ids: np.ndarray) -> None:
104
+ if ids.size == 0:
105
+ return
106
+ h = _hash64(np.asarray(ids))
107
+ reg_idx = (h >> np.uint64(64 - self.p)).astype(np.int64)
108
+ rest = (h << np.uint64(self.p)) | np.uint64((1 << self.p) - 1)
109
+ # rank = leading zeros of the remaining bits + 1
110
+ ranks = np.zeros(ids.shape, dtype=np.uint8)
111
+ remaining = rest.copy()
112
+ # 64-bit leading-zero count via float log2 is unsafe; loop over 64 bits max via vector ops
113
+ lz = np.full(ids.shape, 64, dtype=np.int64)
114
+ nonzero = remaining != 0
115
+ # bit_length via log2 on uint64 split into hi/lo 32-bit halves (exact for ints < 2^53 per half)
116
+ hi = (remaining >> np.uint64(32)).astype(np.float64)
117
+ lo = (remaining & np.uint64(0xFFFFFFFF)).astype(np.float64)
118
+ bl = np.zeros(ids.shape, dtype=np.int64)
119
+ hi_nz = hi > 0
120
+ bl[hi_nz] = 32 + np.floor(np.log2(hi[hi_nz])).astype(np.int64) + 1
121
+ lo_only = (~hi_nz) & (lo > 0)
122
+ bl[lo_only] = np.floor(np.log2(lo[lo_only])).astype(np.int64) + 1
123
+ lz[nonzero] = 64 - bl[nonzero]
124
+ ranks = np.minimum(lz + 1, 64 - self.p + 1).astype(np.uint8)
125
+ np.maximum.at(self.registers, reg_idx, ranks)
126
+
127
+ def merge(self, other: "HyperLogLog") -> "HyperLogLog":
128
+ assert self.p == other.p
129
+ return HyperLogLog(self.p, np.maximum(self.registers, other.registers))
130
+
131
+ def estimate(self) -> float:
132
+ m = float(self.m)
133
+ alpha = 0.7213 / (1.0 + 1.079 / m)
134
+ inv_sum = np.sum(np.exp2(-self.registers.astype(np.float64)))
135
+ raw = alpha * m * m / inv_sum
136
+ zeros = int(np.sum(self.registers == 0))
137
+ if raw <= 2.5 * m and zeros > 0: # small-range correction
138
+ return m * np.log(m / zeros)
139
+ return float(raw)
140
+
141
+
142
+ # ---------------------------------------------------------------- reservoir sample
143
+
144
+
145
+ class Reservoir:
146
+ """Uniform reservoir sample of ids (exemplars for drill-down)."""
147
+
148
+ def __init__(self, k: int = 200, seed: int = 0):
149
+ self.k = int(k)
150
+ self.rng = np.random.default_rng(seed)
151
+ self.items: List[int] = []
152
+ self.seen = 0
153
+
154
+ def add_ids(self, ids: np.ndarray) -> None:
155
+ for v in np.asarray(ids).tolist():
156
+ self.seen += 1
157
+ if len(self.items) < self.k:
158
+ self.items.append(int(v))
159
+ else:
160
+ j = int(self.rng.integers(0, self.seen))
161
+ if j < self.k:
162
+ self.items[j] = int(v)
163
+
164
+ def merge(self, other: "Reservoir") -> "Reservoir":
165
+ merged = Reservoir(self.k, seed=self.seen + other.seen + 1)
166
+ pool = self.items + other.items
167
+ merged.seen = self.seen + other.seen
168
+ if len(pool) <= self.k:
169
+ merged.items = pool
170
+ else:
171
+ idx = merged.rng.choice(len(pool), size=self.k, replace=False)
172
+ merged.items = [pool[i] for i in idx]
173
+ return merged
src/App.tsx CHANGED
@@ -11,6 +11,7 @@ import ListSubheader from '@mui/material/ListSubheader';
11
  import Toolbar from '@mui/material/Toolbar';
12
  import Typography from '@mui/material/Typography';
13
  import {
 
14
  LayoutDashboard,
15
  Database,
16
  Tag,
@@ -44,6 +45,7 @@ import AuditPage from './pages/AuditPage';
44
  import PipelineArchitecturePage from './pages/PipelineArchitecturePage';
45
  import SSAMSafetyDashboard from './pages/SSAMSafetyDashboard';
46
  import LegacyStudioPage from './pages/LegacyStudioPage';
 
47
 
48
  const DRAWER_WIDTH = 230;
49
 
@@ -54,6 +56,7 @@ interface NavItem {
54
  }
55
 
56
  const PLATFORM_NAV: NavItem[] = [
 
57
  { id: 'overview', label: 'Overview', icon: <LayoutDashboard size={18} /> },
58
  { id: 'datasets', label: 'Datasets', icon: <Database size={18} /> },
59
  { id: 'label-generation', label: 'Label Generation', icon: <Tag size={18} /> },
@@ -75,6 +78,7 @@ const LEGACY_NAV: NavItem[] = [
75
  ];
76
 
77
  const PAGE_TITLES: Record<PageId, string> = {
 
78
  overview: 'Overview',
79
  datasets: 'Datasets',
80
  'label-generation': 'Label Generation',
@@ -95,7 +99,7 @@ const PAGE_TITLES: Record<PageId, string> = {
95
  function parseHash(): { page: PageId; entityId: string | null } {
96
  const raw = window.location.hash.replace(/^#\/?/, '');
97
  const [pageRaw, entityRaw] = raw.split('/');
98
- const page = (ALL_PAGE_IDS as string[]).includes(pageRaw ?? '') ? (pageRaw as PageId) : 'overview';
99
  return { page, entityId: entityRaw ? decodeURIComponent(entityRaw) : null };
100
  }
101
 
@@ -268,6 +272,7 @@ export default function App() {
268
  <Typography variant="h5" sx={{ fontWeight: 800, mb: 2 }}>
269
  {PAGE_TITLES[page]}
270
  </Typography>
 
271
  {page === 'overview' ? <OverviewPage /> : null}
272
  {page === 'datasets' ? <DatasetsPage /> : null}
273
  {page === 'label-generation' ? <LabelGenerationPage /> : null}
 
11
  import Toolbar from '@mui/material/Toolbar';
12
  import Typography from '@mui/material/Typography';
13
  import {
14
+ Gauge,
15
  LayoutDashboard,
16
  Database,
17
  Tag,
 
45
  import PipelineArchitecturePage from './pages/PipelineArchitecturePage';
46
  import SSAMSafetyDashboard from './pages/SSAMSafetyDashboard';
47
  import LegacyStudioPage from './pages/LegacyStudioPage';
48
+ import CommandCenterPage from './pages/CommandCenterPage';
49
 
50
  const DRAWER_WIDTH = 230;
51
 
 
56
  }
57
 
58
  const PLATFORM_NAV: NavItem[] = [
59
+ { id: 'command', label: 'Command Center', icon: <Gauge size={18} /> },
60
  { id: 'overview', label: 'Overview', icon: <LayoutDashboard size={18} /> },
61
  { id: 'datasets', label: 'Datasets', icon: <Database size={18} /> },
62
  { id: 'label-generation', label: 'Label Generation', icon: <Tag size={18} /> },
 
78
  ];
79
 
80
  const PAGE_TITLES: Record<PageId, string> = {
81
+ command: 'Evaluation Command Center',
82
  overview: 'Overview',
83
  datasets: 'Datasets',
84
  'label-generation': 'Label Generation',
 
99
  function parseHash(): { page: PageId; entityId: string | null } {
100
  const raw = window.location.hash.replace(/^#\/?/, '');
101
  const [pageRaw, entityRaw] = raw.split('/');
102
+ const page = (ALL_PAGE_IDS as string[]).includes(pageRaw ?? '') ? (pageRaw as PageId) : 'command';
103
  return { page, entityId: entityRaw ? decodeURIComponent(entityRaw) : null };
104
  }
105
 
 
272
  <Typography variant="h5" sx={{ fontWeight: 800, mb: 2 }}>
273
  {PAGE_TITLES[page]}
274
  </Typography>
275
+ {page === 'command' ? <CommandCenterPage /> : null}
276
  {page === 'overview' ? <OverviewPage /> : null}
277
  {page === 'datasets' ? <DatasetsPage /> : null}
278
  {page === 'label-generation' ? <LabelGenerationPage /> : null}
src/components/megaeval/CohortTab.tsx ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Cohort explorer: pick a dimension tab, click a cohort value to drill —
3
+ * the value becomes a filter breadcrumb (e.g. Pedestrian → Night → Occluded)
4
+ * and the next level groups by the newly selected dimension. Every node shows
5
+ * cube metrics via POST /api/evaluations/query. "Why?" runs the factor
6
+ * decomposition for the current filter set.
7
+ */
8
+ import { useMemo, useState } from 'react';
9
+ import Box from '@mui/material/Box';
10
+ import Button from '@mui/material/Button';
11
+ import Chip from '@mui/material/Chip';
12
+ import CircularProgress from '@mui/material/CircularProgress';
13
+ import Tab from '@mui/material/Tab';
14
+ import Table from '@mui/material/Table';
15
+ import TableBody from '@mui/material/TableBody';
16
+ import TableCell from '@mui/material/TableCell';
17
+ import TableHead from '@mui/material/TableHead';
18
+ import TableRow from '@mui/material/TableRow';
19
+ import Tabs from '@mui/material/Tabs';
20
+ import Typography from '@mui/material/Typography';
21
+ import { HelpCircle, ChevronRight } from 'lucide-react';
22
+ import type { DimName, WhyResponse } from '../../types/megaeval';
23
+ import { DIM_NAMES } from '../../types/megaeval';
24
+ import { fmtCompact, queryEvaluations, whyAnalysis } from '../../services/megaeval';
25
+ import { usePoll } from '../../services/labeleval';
26
+ import { ErrorNote, HBar, LoadingBox, SectionCard, fmtNum, fmtPct } from '../labeleval/shared';
27
+ import { PctBarCell, QueryBadge, rowNum, rowStr } from './shared';
28
+
29
+ interface Crumb {
30
+ dim: DimName;
31
+ value: string;
32
+ }
33
+
34
+ function crumbsToFilters(crumbs: Crumb[]): Partial<Record<DimName, string[]>> {
35
+ const filters: Partial<Record<DimName, string[]>> = {};
36
+ for (const c of crumbs) {
37
+ filters[c.dim] = [...(filters[c.dim] ?? []), c.value];
38
+ }
39
+ return filters;
40
+ }
41
+
42
+ function WhyPanel({ why }: { why: WhyResponse }) {
43
+ const maxCohortN = Math.max(1, ...why.top_cohorts.map((c) => c.n));
44
+ return (
45
+ <SectionCard
46
+ title={`Why is ${why.metric} degraded here?`}
47
+ sx={{ flex: '1 1 340px', borderColor: '#4a3b12' }}
48
+ >
49
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1 }}>
50
+ {fmtCompact(why.failure_count)} failures decomposed · {why.method}
51
+ </Typography>
52
+ {why.factors.map((f) => (
53
+ <HBar
54
+ key={f.factor}
55
+ label={f.factor.replace(/_/g, ' ')}
56
+ value={f.share <= 1 ? f.share * 100 : f.share}
57
+ max={100}
58
+ color="#ffd54f"
59
+ valueLabel={`${fmtPct(f.share)} (${fmtCompact(f.count)})`}
60
+ />
61
+ ))}
62
+ {why.top_cohorts.length ? (
63
+ <>
64
+ <Typography variant="caption" sx={{ color: '#aab4be', display: 'block', mt: 1.5, mb: 0.5 }}>
65
+ Top affected cohorts
66
+ </Typography>
67
+ {why.top_cohorts.map((c) => (
68
+ <HBar
69
+ key={c.cohort}
70
+ label={c.cohort}
71
+ value={c.n}
72
+ max={maxCohortN}
73
+ color="#ef5350"
74
+ valueLabel={`n=${fmtCompact(c.n)}${c.fn !== undefined && c.fn !== null ? ` · fn=${fmtCompact(c.fn)}` : ''}${
75
+ c.fp !== undefined && c.fp !== null ? ` · fp=${fmtCompact(c.fp)}` : ''
76
+ }`}
77
+ />
78
+ ))}
79
+ </>
80
+ ) : null}
81
+ </SectionCard>
82
+ );
83
+ }
84
+
85
+ export default function CohortTab({ runId, refreshKey }: { runId: string; refreshKey: number }) {
86
+ const [crumbs, setCrumbs] = useState<Crumb[]>([]);
87
+ const [dim, setDim] = useState<DimName>('class');
88
+ const [why, setWhy] = useState<WhyResponse | null>(null);
89
+ const [whyBusy, setWhyBusy] = useState(false);
90
+ const [whyError, setWhyError] = useState<string | null>(null);
91
+
92
+ const filters = useMemo(() => crumbsToFilters(crumbs), [crumbs]);
93
+ const filtersKey = JSON.stringify(filters);
94
+
95
+ const query = usePoll(
96
+ () =>
97
+ queryEvaluations({
98
+ evaluation_id: runId,
99
+ filters,
100
+ group_by: [dim],
101
+ metrics: ['n', 'recall', 'precision', 'mean_iou', 'anomaly_rate'],
102
+ }),
103
+ null,
104
+ [runId, refreshKey, dim, filtersKey]
105
+ );
106
+
107
+ const usedDims = new Set(crumbs.map((c) => c.dim));
108
+
109
+ const drillInto = (value: string) => {
110
+ const next = [...crumbs, { dim, value }];
111
+ setCrumbs(next);
112
+ setWhy(null);
113
+ // Auto-advance the group-by dimension to the next unused one.
114
+ const nextUsed = new Set(next.map((c) => c.dim));
115
+ const nextDim = DIM_NAMES.find((d) => !nextUsed.has(d));
116
+ if (nextDim) setDim(nextDim);
117
+ };
118
+
119
+ const removeCrumb = (idx: number) => {
120
+ setCrumbs((prev) => prev.filter((_, i) => i !== idx));
121
+ setWhy(null);
122
+ };
123
+
124
+ const runWhy = async () => {
125
+ setWhyBusy(true);
126
+ setWhyError(null);
127
+ try {
128
+ setWhy(await whyAnalysis({ run_id: runId, filters, metric: 'recall' }));
129
+ } catch (err) {
130
+ setWhyError(err instanceof Error ? err.message : String(err));
131
+ } finally {
132
+ setWhyBusy(false);
133
+ }
134
+ };
135
+
136
+ const rows = useMemo(
137
+ () =>
138
+ query.data
139
+ ? // usePoll keeps the previous response while a new query is in flight;
140
+ // drop rows that don't carry the currently-selected dimension so stale
141
+ // groupings never render (and never collide on duplicate row keys).
142
+ query.data.rows
143
+ .filter((r) => typeof r[dim] === 'string')
144
+ .sort((a, b) => b.n - a.n)
145
+ : [],
146
+ [query.data, dim]
147
+ );
148
+
149
+ return (
150
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
151
+ <SectionCard
152
+ title="Cohort explorer"
153
+ action={
154
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
155
+ <QueryBadge meta={query.data?.meta} />
156
+ <Button
157
+ size="small"
158
+ variant="outlined"
159
+ color="warning"
160
+ startIcon={whyBusy ? <CircularProgress size={14} color="inherit" /> : <HelpCircle size={14} />}
161
+ disabled={crumbs.length === 0 || whyBusy}
162
+ onClick={() => void runWhy()}
163
+ >
164
+ Why?
165
+ </Button>
166
+ </Box>
167
+ }
168
+ sx={{ flex: '2 1 560px' }}
169
+ >
170
+ {/* Filter breadcrumb */}
171
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap', mb: 1 }}>
172
+ <Chip
173
+ size="small"
174
+ label="All objects"
175
+ onClick={() => {
176
+ setCrumbs([]);
177
+ setWhy(null);
178
+ }}
179
+ sx={{ height: 22, bgcolor: crumbs.length ? '#232a31' : '#12303f', color: '#81d4fa', fontWeight: 700 }}
180
+ />
181
+ {crumbs.map((c, i) => (
182
+ <Box key={`${c.dim}-${c.value}-${i}`} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
183
+ <ChevronRight size={13} color="#8a949e" />
184
+ <Chip
185
+ size="small"
186
+ label={`${c.dim}: ${c.value}`}
187
+ onDelete={() => removeCrumb(i)}
188
+ sx={{ height: 22, bgcolor: '#12303f', color: '#81d4fa', fontFamily: 'monospace', fontSize: 11 }}
189
+ />
190
+ </Box>
191
+ ))}
192
+ </Box>
193
+
194
+ {/* Dimension tabs */}
195
+ <Tabs
196
+ value={dim}
197
+ onChange={(_, v: DimName) => setDim(v)}
198
+ variant="scrollable"
199
+ scrollButtons="auto"
200
+ sx={{ minHeight: 32, mb: 1, '& .MuiTab-root': { minHeight: 32, py: 0.5, fontSize: 12 } }}
201
+ >
202
+ {DIM_NAMES.map((d) => (
203
+ <Tab key={d} label={d.replace(/_/g, ' ')} value={d} disabled={usedDims.has(d)} />
204
+ ))}
205
+ </Tabs>
206
+
207
+ {query.error ? <ErrorNote error={query.error} /> : null}
208
+ {query.loading && !query.data ? <LoadingBox /> : null}
209
+ {query.data ? (
210
+ <Table size="small">
211
+ <TableHead>
212
+ <TableRow>
213
+ <TableCell>{dim.replace(/_/g, ' ')}</TableCell>
214
+ <TableCell align="right">n</TableCell>
215
+ <TableCell>Recall</TableCell>
216
+ <TableCell>Precision</TableCell>
217
+ <TableCell align="right">Mean IoU</TableCell>
218
+ <TableCell align="right">Anomaly rate</TableCell>
219
+ </TableRow>
220
+ </TableHead>
221
+ <TableBody>
222
+ {rows.map((row, i) => {
223
+ const value = rowStr(row, dim);
224
+ return (
225
+ <TableRow
226
+ key={`${value}-${i}`}
227
+ hover
228
+ onClick={() => drillInto(value)}
229
+ sx={{ cursor: 'pointer' }}
230
+ >
231
+ <TableCell sx={{ fontWeight: 600, color: '#4fc3f7' }}>{value}</TableCell>
232
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
233
+ {fmtCompact(row.n)}
234
+ </TableCell>
235
+ <TableCell>
236
+ <PctBarCell value={rowNum(row, 'recall')} color="#66bb6a" />
237
+ </TableCell>
238
+ <TableCell>
239
+ <PctBarCell value={rowNum(row, 'precision')} color="#4fc3f7" />
240
+ </TableCell>
241
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
242
+ {fmtNum(rowNum(row, 'mean_iou'), 3)}
243
+ </TableCell>
244
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
245
+ {fmtPct(rowNum(row, 'anomaly_rate'))}
246
+ </TableCell>
247
+ </TableRow>
248
+ );
249
+ })}
250
+ </TableBody>
251
+ </Table>
252
+ ) : null}
253
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1 }}>
254
+ Click a row to drill into that cohort; the next level groups by the selected dimension tab.
255
+ </Typography>
256
+ </SectionCard>
257
+
258
+ {whyError ? (
259
+ <Box sx={{ flex: '1 1 340px' }}>
260
+ <ErrorNote error={whyError} />
261
+ </Box>
262
+ ) : null}
263
+ {why ? <WhyPanel why={why} /> : null}
264
+ </Box>
265
+ );
266
+ }
src/components/megaeval/CompareTab.tsx ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Model-vs-model comparison: candidate (selected run) against the baseline
3
+ * chosen in the header. Headline deltas, per-class deltas, top regressions,
4
+ * and the PROMOTE / DO_NOT_PROMOTE recommendation with blockers.
5
+ */
6
+ import Alert from '@mui/material/Alert';
7
+ import AlertTitle from '@mui/material/AlertTitle';
8
+ import Box from '@mui/material/Box';
9
+ import Table from '@mui/material/Table';
10
+ import TableBody from '@mui/material/TableBody';
11
+ import TableCell from '@mui/material/TableCell';
12
+ import TableHead from '@mui/material/TableHead';
13
+ import TableRow from '@mui/material/TableRow';
14
+ import Typography from '@mui/material/Typography';
15
+ import { compareRuns, fmtCompact } from '../../services/megaeval';
16
+ import { usePoll } from '../../services/labeleval';
17
+ import { ErrorNote, LoadingBox, SectionCard, fmtPct } from '../labeleval/shared';
18
+ import { DeltaText } from './shared';
19
+
20
+ /** Metrics where a positive delta is a regression rather than an improvement. */
21
+ const LOWER_IS_BETTER = new Set(['anomaly_rate', 'error_rate', 'fp', 'fn', 'loc_err', 'anomalies']);
22
+
23
+ function fmtMetricValue(v: number): string {
24
+ // Rates arrive as fractions; counts as integers > 1.
25
+ return Math.abs(v) <= 1 ? fmtPct(v) : fmtCompact(v);
26
+ }
27
+
28
+ export default function CompareTab({
29
+ runId,
30
+ baselineRunId,
31
+ refreshKey,
32
+ }: {
33
+ runId: string;
34
+ baselineRunId: string | null;
35
+ refreshKey: number;
36
+ }) {
37
+ const cmp = usePoll(
38
+ () =>
39
+ baselineRunId
40
+ ? compareRuns({ candidate_run_id: runId, baseline_run_id: baselineRunId })
41
+ : Promise.resolve(null),
42
+ null,
43
+ [runId, baselineRunId, refreshKey]
44
+ );
45
+
46
+ if (!baselineRunId) {
47
+ return (
48
+ <Alert severity="info" variant="outlined">
49
+ Select a <strong>baseline run</strong> in the header to compare the current candidate against it.
50
+ </Alert>
51
+ );
52
+ }
53
+ if (cmp.error) return <ErrorNote error={cmp.error} />;
54
+ if (cmp.loading && !cmp.data) return <LoadingBox label="Comparing runs…" />;
55
+ const data = cmp.data;
56
+ if (!data) return null;
57
+
58
+ const promote = data.recommendation === 'PROMOTE';
59
+
60
+ return (
61
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
62
+ <Alert severity={promote ? 'success' : 'error'} variant="filled" sx={{ fontWeight: 700 }}>
63
+ <AlertTitle sx={{ fontWeight: 800 }}>
64
+ {promote ? 'PROMOTE' : 'DO NOT PROMOTE'} — {data.candidate.model_version} vs baseline{' '}
65
+ {data.baseline.model_version}
66
+ </AlertTitle>
67
+ {data.blockers.length ? (
68
+ <Box component="ul" sx={{ m: 0, pl: 2.5 }}>
69
+ {data.blockers.map((b, i) => (
70
+ <li key={i}>
71
+ <Typography variant="body2">{b}</Typography>
72
+ </li>
73
+ ))}
74
+ </Box>
75
+ ) : (
76
+ <Typography variant="body2">No blockers under the current promotion policy.</Typography>
77
+ )}
78
+ </Alert>
79
+
80
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
81
+ <SectionCard title="Headline deltas" sx={{ flex: '1 1 380px' }}>
82
+ <Table size="small">
83
+ <TableHead>
84
+ <TableRow>
85
+ <TableCell>Metric</TableCell>
86
+ <TableCell align="right">Baseline</TableCell>
87
+ <TableCell align="right">Candidate</TableCell>
88
+ <TableCell align="right">Δ</TableCell>
89
+ </TableRow>
90
+ </TableHead>
91
+ <TableBody>
92
+ {data.headline_deltas.map((d) => (
93
+ <TableRow key={d.metric} hover>
94
+ <TableCell sx={{ fontFamily: 'monospace' }}>{d.metric}</TableCell>
95
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
96
+ {fmtMetricValue(d.baseline)}
97
+ </TableCell>
98
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
99
+ {fmtMetricValue(d.candidate)}
100
+ </TableCell>
101
+ <TableCell align="right">
102
+ <DeltaText
103
+ delta={d.delta}
104
+ higherIsBetter={!LOWER_IS_BETTER.has(d.metric)}
105
+ asPct={Math.abs(d.baseline) <= 1 && Math.abs(d.candidate) <= 1}
106
+ />
107
+ </TableCell>
108
+ </TableRow>
109
+ ))}
110
+ </TableBody>
111
+ </Table>
112
+ </SectionCard>
113
+
114
+ <SectionCard title="Per-class deltas" sx={{ flex: '1 1 480px' }}>
115
+ <Table size="small">
116
+ <TableHead>
117
+ <TableRow>
118
+ <TableCell>Class</TableCell>
119
+ <TableCell align="right">n</TableCell>
120
+ <TableCell align="right">Recall</TableCell>
121
+ <TableCell align="right">Δ recall</TableCell>
122
+ <TableCell align="right">Precision</TableCell>
123
+ <TableCell align="right">Δ precision</TableCell>
124
+ </TableRow>
125
+ </TableHead>
126
+ <TableBody>
127
+ {data.per_class.map((c) => (
128
+ <TableRow key={c.class} hover>
129
+ <TableCell sx={{ fontWeight: 600 }}>{c.class}</TableCell>
130
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
131
+ {fmtCompact(c.n)}
132
+ </TableCell>
133
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
134
+ {fmtPct(c.recall_baseline)} → {fmtPct(c.recall_candidate)}
135
+ </TableCell>
136
+ <TableCell align="right">
137
+ <DeltaText delta={c.recall_delta} />
138
+ </TableCell>
139
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
140
+ {fmtPct(c.precision_baseline)} → {fmtPct(c.precision_candidate)}
141
+ </TableCell>
142
+ <TableCell align="right">
143
+ <DeltaText delta={c.precision_delta} />
144
+ </TableCell>
145
+ </TableRow>
146
+ ))}
147
+ </TableBody>
148
+ </Table>
149
+ </SectionCard>
150
+ </Box>
151
+
152
+ <SectionCard title={`Top regressions (${data.regressions.length})`}>
153
+ {data.regressions.length === 0 ? (
154
+ <Typography variant="body2" sx={{ color: '#8a949e' }}>
155
+ No cohort regressions detected against the baseline.
156
+ </Typography>
157
+ ) : (
158
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
159
+ {data.regressions.map((r) => (
160
+ <Box
161
+ key={r.cohort}
162
+ sx={{
163
+ p: 1,
164
+ border: '1px solid #5c1a1a',
165
+ bgcolor: '#2a1214',
166
+ borderRadius: 1,
167
+ display: 'flex',
168
+ alignItems: 'center',
169
+ gap: 1,
170
+ flexWrap: 'wrap',
171
+ }}
172
+ >
173
+ <Typography variant="body2" sx={{ fontWeight: 700, color: '#ef9a9a' }}>
174
+ 🚨 REGRESSION:
175
+ </Typography>
176
+ <Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
177
+ {r.cohort} recall {fmtPct(r.recall_baseline)} → {fmtPct(r.recall_candidate)} (Δ{' '}
178
+ <DeltaText delta={r.recall_delta} />)
179
+ </Typography>
180
+ <Typography variant="caption" sx={{ color: '#8a949e', ml: 'auto', fontFamily: 'monospace' }}>
181
+ n={fmtCompact(r.n)}
182
+ </Typography>
183
+ </Box>
184
+ ))}
185
+ </Box>
186
+ )}
187
+ </SectionCard>
188
+
189
+ {data.worst_cohorts.length ? (
190
+ <SectionCard title="Worst cohorts (candidate)">
191
+ <Table size="small">
192
+ <TableHead>
193
+ <TableRow>
194
+ <TableCell>Cohort</TableCell>
195
+ <TableCell align="right">n</TableCell>
196
+ <TableCell align="right">Recall (baseline → candidate)</TableCell>
197
+ <TableCell align="right">Δ</TableCell>
198
+ </TableRow>
199
+ </TableHead>
200
+ <TableBody>
201
+ {data.worst_cohorts.map((c) => (
202
+ <TableRow key={c.cohort} hover>
203
+ <TableCell sx={{ fontFamily: 'monospace' }}>{c.cohort}</TableCell>
204
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
205
+ {fmtCompact(c.n)}
206
+ </TableCell>
207
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
208
+ {fmtPct(c.recall_baseline)} → {fmtPct(c.recall_candidate)}
209
+ </TableCell>
210
+ <TableCell align="right">
211
+ <DeltaText delta={c.recall_delta} />
212
+ </TableCell>
213
+ </TableRow>
214
+ ))}
215
+ </TableBody>
216
+ </Table>
217
+ </SectionCard>
218
+ ) : null}
219
+ </Box>
220
+ );
221
+ }
src/components/megaeval/ContainersTab.tsx ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Container explorer ("Quality by Container"): sortable container table with
3
+ * risk presets. Drilling into a container opens the forensic object table —
4
+ * intentionally the deepest level of the app — plus nearest-neighbor
5
+ * "similar containers" retrieval for casting a wider net.
6
+ */
7
+ import { useState } from 'react';
8
+ import Box from '@mui/material/Box';
9
+ import Button from '@mui/material/Button';
10
+ import CircularProgress from '@mui/material/CircularProgress';
11
+ import MenuItem from '@mui/material/MenuItem';
12
+ import Table from '@mui/material/Table';
13
+ import TableBody from '@mui/material/TableBody';
14
+ import TableCell from '@mui/material/TableCell';
15
+ import TableHead from '@mui/material/TableHead';
16
+ import TableRow from '@mui/material/TableRow';
17
+ import TextField from '@mui/material/TextField';
18
+ import Typography from '@mui/material/Typography';
19
+ import { ArrowLeft, Search, ShieldAlert } from 'lucide-react';
20
+ import type { ContainerSortPreset, SimilarityResponse } from '../../types/megaeval';
21
+ import {
22
+ findSimilarContainers,
23
+ fmtCompact,
24
+ getContainerObjects,
25
+ getRunContainers,
26
+ } from '../../services/megaeval';
27
+ import { usePoll } from '../../services/labeleval';
28
+ import { ErrorNote, LoadingBox, SectionCard, fmtNum, fmtPct } from '../labeleval/shared';
29
+ import { ContainerStatusIcon, DimChips, OutcomeChip } from './shared';
30
+
31
+ const SORT_PRESETS: Array<{ value: ContainerSortPreset; label: string }> = [
32
+ { value: 'highest_risk', label: 'Highest risk' },
33
+ { value: 'worst_recall', label: 'Worst recall' },
34
+ { value: 'worst_precision', label: 'Worst precision' },
35
+ { value: 'worst_iou', label: 'Worst IoU' },
36
+ { value: 'most_anomalies', label: 'Most anomalies' },
37
+ { value: 'least_verified', label: 'Least verified' },
38
+ ];
39
+
40
+ // ---------------------------------------------------------------- forensic view
41
+
42
+ function ForensicView({
43
+ runId,
44
+ containerId,
45
+ onBack,
46
+ onDrill,
47
+ }: {
48
+ runId: string;
49
+ containerId: number;
50
+ onBack: () => void;
51
+ onDrill: (cid: number) => void;
52
+ }) {
53
+ const objects = usePoll(() => getContainerObjects(runId, containerId), null, [runId, containerId]);
54
+ const [similar, setSimilar] = useState<SimilarityResponse | null>(null);
55
+ const [simBusy, setSimBusy] = useState(false);
56
+ const [simError, setSimError] = useState<string | null>(null);
57
+
58
+ const findSimilar = async () => {
59
+ setSimBusy(true);
60
+ setSimError(null);
61
+ try {
62
+ setSimilar(await findSimilarContainers({ run_id: runId, container_id: containerId, k: 10 }));
63
+ } catch (err) {
64
+ setSimError(err instanceof Error ? err.message : String(err));
65
+ } finally {
66
+ setSimBusy(false);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
72
+ <SectionCard
73
+ title={
74
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
75
+ <Button size="small" startIcon={<ArrowLeft size={14} />} onClick={onBack}>
76
+ Containers
77
+ </Button>
78
+ <span>Forensic object table · container #{containerId}</span>
79
+ </Box>
80
+ }
81
+ action={
82
+ <Button
83
+ size="small"
84
+ variant="outlined"
85
+ startIcon={simBusy ? <CircularProgress size={14} color="inherit" /> : <Search size={14} />}
86
+ disabled={simBusy}
87
+ onClick={() => void findSimilar()}
88
+ >
89
+ Find similar containers
90
+ </Button>
91
+ }
92
+ sx={{ flex: '2 1 620px' }}
93
+ >
94
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1 }}>
95
+ Deepest drill-down level: individual annotations, loaded on demand for this container only.
96
+ </Typography>
97
+ {objects.error ? <ErrorNote error={objects.error} /> : null}
98
+ {objects.loading && !objects.data ? <LoadingBox /> : null}
99
+ {objects.data ? (
100
+ <Box sx={{ maxHeight: 460, overflowY: 'auto' }}>
101
+ <Table size="small" stickyHeader>
102
+ <TableHead>
103
+ <TableRow>
104
+ <TableCell>Outcome</TableCell>
105
+ <TableCell>Class</TableCell>
106
+ <TableCell align="right">IoU</TableCell>
107
+ <TableCell align="right">Conf</TableCell>
108
+ <TableCell align="center">Anomaly</TableCell>
109
+ <TableCell>Dims</TableCell>
110
+ </TableRow>
111
+ </TableHead>
112
+ <TableBody>
113
+ {objects.data.objects.map((o) => (
114
+ <TableRow key={o.annotation_id} hover>
115
+ <TableCell>
116
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
117
+ <OutcomeChip outcome={o.outcome} />
118
+ {o.safety_critical ? <ShieldAlert size={14} color="#ef5350" /> : null}
119
+ </Box>
120
+ </TableCell>
121
+ <TableCell sx={{ fontWeight: 600 }}>{o.class}</TableCell>
122
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
123
+ {fmtNum(o.iou, 3)}
124
+ </TableCell>
125
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
126
+ {fmtNum(o.confidence, 2)}
127
+ </TableCell>
128
+ <TableCell align="center">
129
+ {o.anomaly ? (
130
+ <Typography component="span" sx={{ color: '#ec407a', fontSize: 13 }}>
131
+ ●
132
+ </Typography>
133
+ ) : (
134
+ <Typography component="span" sx={{ color: '#37474f', fontSize: 13 }}>
135
+ ○
136
+ </Typography>
137
+ )}
138
+ </TableCell>
139
+ <TableCell>
140
+ <DimChips
141
+ values={[
142
+ o.weather,
143
+ o.lighting,
144
+ o.scenario,
145
+ o.sensor,
146
+ o.distance_band,
147
+ o.occlusion,
148
+ o.sensor_disagree ? 'sensor-disagree' : undefined,
149
+ ]}
150
+ />
151
+ </TableCell>
152
+ </TableRow>
153
+ ))}
154
+ </TableBody>
155
+ </Table>
156
+ </Box>
157
+ ) : null}
158
+ </SectionCard>
159
+
160
+ {simError ? (
161
+ <Box sx={{ flex: '1 1 340px' }}>
162
+ <ErrorNote error={simError} />
163
+ </Box>
164
+ ) : null}
165
+ {similar ? (
166
+ <SectionCard
167
+ title={`Similar containers${similar.retrieval ? ` · ${similar.retrieval}` : ''}`}
168
+ sx={{ flex: '1 1 380px' }}
169
+ >
170
+ <Table size="small">
171
+ <TableHead>
172
+ <TableRow>
173
+ <TableCell>Container</TableCell>
174
+ <TableCell align="right">Similarity</TableCell>
175
+ <TableCell>Context</TableCell>
176
+ <TableCell align="right">FN/FP</TableCell>
177
+ <TableCell align="right">Risk</TableCell>
178
+ </TableRow>
179
+ </TableHead>
180
+ <TableBody>
181
+ {similar.results.map((s) => (
182
+ <TableRow key={s.container_id} hover onClick={() => onDrill(s.container_id)} sx={{ cursor: 'pointer' }}>
183
+ <TableCell sx={{ fontFamily: 'monospace', color: '#4fc3f7' }}>#{s.container_id}</TableCell>
184
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
185
+ {fmtPct(s.similarity)}
186
+ </TableCell>
187
+ <TableCell>
188
+ <DimChips values={[s.scenario, s.lighting, s.weather]} />
189
+ </TableCell>
190
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
191
+ {s.fn}/{s.fp}
192
+ </TableCell>
193
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
194
+ {fmtNum(s.risk_score, 2)}
195
+ </TableCell>
196
+ </TableRow>
197
+ ))}
198
+ </TableBody>
199
+ </Table>
200
+ </SectionCard>
201
+ ) : null}
202
+ </Box>
203
+ );
204
+ }
205
+
206
+ // ---------------------------------------------------------------- containers table
207
+
208
+ export default function ContainersTab({
209
+ runId,
210
+ refreshKey,
211
+ drillContainerId,
212
+ onDrill,
213
+ }: {
214
+ runId: string;
215
+ refreshKey: number;
216
+ drillContainerId: number | null;
217
+ onDrill: (cid: number | null) => void;
218
+ }) {
219
+ const [sort, setSort] = useState<ContainerSortPreset>('highest_risk');
220
+ const containers = usePoll(() => getRunContainers(runId, sort, 50), null, [runId, sort, refreshKey]);
221
+
222
+ if (drillContainerId !== null) {
223
+ return (
224
+ <ForensicView
225
+ runId={runId}
226
+ containerId={drillContainerId}
227
+ onBack={() => onDrill(null)}
228
+ onDrill={(cid) => onDrill(cid)}
229
+ />
230
+ );
231
+ }
232
+
233
+ return (
234
+ <SectionCard
235
+ title="Quality by container"
236
+ action={
237
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
238
+ {containers.data ? (
239
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
240
+ {fmtCompact(containers.data.total)} containers (exact) · showing top {containers.data.rows.length}
241
+ </Typography>
242
+ ) : null}
243
+ <TextField
244
+ select
245
+ size="small"
246
+ label="Sort"
247
+ value={sort}
248
+ onChange={(e) => setSort(e.target.value as ContainerSortPreset)}
249
+ sx={{ minWidth: 180 }}
250
+ >
251
+ {SORT_PRESETS.map((p) => (
252
+ <MenuItem key={p.value} value={p.value}>
253
+ {p.label}
254
+ </MenuItem>
255
+ ))}
256
+ </TextField>
257
+ </Box>
258
+ }
259
+ >
260
+ {containers.error ? <ErrorNote error={containers.error} /> : null}
261
+ {containers.loading && !containers.data ? <LoadingBox /> : null}
262
+ {containers.data ? (
263
+ <Box sx={{ maxHeight: 520, overflowY: 'auto' }}>
264
+ <Table size="small" stickyHeader>
265
+ <TableHead>
266
+ <TableRow>
267
+ <TableCell />
268
+ <TableCell>Container</TableCell>
269
+ <TableCell>Context</TableCell>
270
+ <TableCell align="right">Objects</TableCell>
271
+ <TableCell align="right">Verified</TableCell>
272
+ <TableCell align="right">Precision</TableCell>
273
+ <TableCell align="right">Recall</TableCell>
274
+ <TableCell align="right">Mean IoU</TableCell>
275
+ <TableCell align="right">Anomalies</TableCell>
276
+ <TableCell align="right">Risk</TableCell>
277
+ </TableRow>
278
+ </TableHead>
279
+ <TableBody>
280
+ {containers.data.rows.map((c) => (
281
+ <TableRow key={c.container_id} hover onClick={() => onDrill(c.container_id)} sx={{ cursor: 'pointer' }}>
282
+ <TableCell sx={{ width: 30 }}>
283
+ <ContainerStatusIcon status={c.status} />
284
+ </TableCell>
285
+ <TableCell sx={{ fontFamily: 'monospace', color: '#4fc3f7' }}>#{c.container_id}</TableCell>
286
+ <TableCell>
287
+ <DimChips values={[c.scenario, c.lighting, c.weather, c.road_type]} />
288
+ </TableCell>
289
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
290
+ {fmtCompact(c.n_objects)}
291
+ </TableCell>
292
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
293
+ {c.verified}
294
+ </TableCell>
295
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
296
+ {fmtPct(c.precision)}
297
+ </TableCell>
298
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
299
+ {fmtPct(c.recall)}
300
+ </TableCell>
301
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
302
+ {fmtNum(c.mean_iou, 3)}
303
+ </TableCell>
304
+ <TableCell align="right" sx={{ fontFamily: 'monospace', color: c.anomalies > 0 ? '#ec407a' : undefined }}>
305
+ {c.anomalies}
306
+ </TableCell>
307
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
308
+ {fmtNum(c.risk_score, 2)}
309
+ </TableCell>
310
+ </TableRow>
311
+ ))}
312
+ </TableBody>
313
+ </Table>
314
+ </Box>
315
+ ) : null}
316
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1 }}>
317
+ Click a container to open its forensic object table (deepest drill-down).
318
+ </Typography>
319
+ </SectionCard>
320
+ );
321
+ }
src/components/megaeval/HeaderBar.tsx ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Command Center header: population + run + baseline selectors, run status,
3
+ * cache stats chip, and the two entry-point actions (generate population,
4
+ * launch evaluation run). The action buttons are exported so the page-level
5
+ * empty state can reuse them front and center.
6
+ */
7
+ import { useMemo, useState } from 'react';
8
+ import Alert from '@mui/material/Alert';
9
+ import Box from '@mui/material/Box';
10
+ import Button from '@mui/material/Button';
11
+ import Chip from '@mui/material/Chip';
12
+ import CircularProgress from '@mui/material/CircularProgress';
13
+ import Dialog from '@mui/material/Dialog';
14
+ import DialogActions from '@mui/material/DialogActions';
15
+ import DialogContent from '@mui/material/DialogContent';
16
+ import DialogTitle from '@mui/material/DialogTitle';
17
+ import FormControlLabel from '@mui/material/FormControlLabel';
18
+ import MenuItem from '@mui/material/MenuItem';
19
+ import Switch from '@mui/material/Switch';
20
+ import TextField from '@mui/material/TextField';
21
+ import Tooltip from '@mui/material/Tooltip';
22
+ import Typography from '@mui/material/Typography';
23
+ import { Database, FlaskConical } from 'lucide-react';
24
+ import type { CacheStats, EvaluationRunInfo, PopulationMeta } from '../../types/megaeval';
25
+ import { createRun, fmtCompact, generatePopulation } from '../../services/megaeval';
26
+ import { fmtPct } from '../labeleval/shared';
27
+ import { RunStatusChip } from './shared';
28
+
29
+ // ---------------------------------------------------------------- action buttons
30
+
31
+ export function GeneratePopulationButton({
32
+ onGenerated,
33
+ variant = 'outlined',
34
+ size = 'small',
35
+ }: {
36
+ onGenerated: (pop: PopulationMeta) => void;
37
+ variant?: 'outlined' | 'contained';
38
+ size?: 'small' | 'medium' | 'large';
39
+ }) {
40
+ const [open, setOpen] = useState(false);
41
+ const [numObjects, setNumObjects] = useState('320000');
42
+ const [name, setName] = useState('');
43
+ const [busy, setBusy] = useState(false);
44
+ const [error, setError] = useState<string | null>(null);
45
+
46
+ const submit = async () => {
47
+ setBusy(true);
48
+ setError(null);
49
+ try {
50
+ const parsed = Number(numObjects);
51
+ const pop = await generatePopulation({
52
+ ...(name.trim() ? { name: name.trim() } : {}),
53
+ num_objects: Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 320000,
54
+ });
55
+ setOpen(false);
56
+ onGenerated(pop);
57
+ } catch (err) {
58
+ setError(err instanceof Error ? err.message : String(err));
59
+ } finally {
60
+ setBusy(false);
61
+ }
62
+ };
63
+
64
+ return (
65
+ <>
66
+ <Button variant={variant} size={size} startIcon={<Database size={16} />} onClick={() => setOpen(true)}>
67
+ Generate population
68
+ </Button>
69
+ <Dialog open={open} onClose={() => (busy ? undefined : setOpen(false))} maxWidth="xs" fullWidth>
70
+ <DialogTitle>Generate evaluation population</DialogTitle>
71
+ <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '12px !important' }}>
72
+ {error ? (
73
+ <Alert severity="error" variant="outlined">
74
+ {error}
75
+ </Alert>
76
+ ) : null}
77
+ <TextField
78
+ label="Name (optional)"
79
+ size="small"
80
+ value={name}
81
+ onChange={(e) => setName(e.target.value)}
82
+ placeholder="e.g. q3-highway-mix"
83
+ />
84
+ <TextField
85
+ label="Objects"
86
+ size="small"
87
+ type="number"
88
+ value={numObjects}
89
+ onChange={(e) => setNumObjects(e.target.value)}
90
+ helperText="1K – 1.2M synthetic annotated objects (default 320K)"
91
+ />
92
+ </DialogContent>
93
+ <DialogActions>
94
+ <Button onClick={() => setOpen(false)} disabled={busy}>
95
+ Cancel
96
+ </Button>
97
+ <Button
98
+ variant="contained"
99
+ onClick={() => void submit()}
100
+ disabled={busy}
101
+ startIcon={busy ? <CircularProgress size={16} color="inherit" /> : <Database size={16} />}
102
+ >
103
+ {busy ? 'Generating…' : 'Generate'}
104
+ </Button>
105
+ </DialogActions>
106
+ </Dialog>
107
+ </>
108
+ );
109
+ }
110
+
111
+ export function NewRunButton({
112
+ populationId,
113
+ onCreated,
114
+ variant = 'contained',
115
+ size = 'small',
116
+ }: {
117
+ populationId: string | null;
118
+ onCreated: (run: EvaluationRunInfo) => void;
119
+ variant?: 'outlined' | 'contained';
120
+ size?: 'small' | 'medium' | 'large';
121
+ }) {
122
+ const [open, setOpen] = useState(false);
123
+ const [modelVersion, setModelVersion] = useState('perception-v2.1');
124
+ const [injectRegression, setInjectRegression] = useState(false);
125
+ const [busy, setBusy] = useState(false);
126
+ const [error, setError] = useState<string | null>(null);
127
+
128
+ const submit = async () => {
129
+ if (!populationId) return;
130
+ setBusy(true);
131
+ setError(null);
132
+ try {
133
+ const run = await createRun({
134
+ population_id: populationId,
135
+ model_version: modelVersion.trim() || 'perception-v2.1',
136
+ ...(injectRegression ? { overrides: { night_penalty: 0.3, vru_penalty: 0.1 } } : {}),
137
+ });
138
+ setOpen(false);
139
+ onCreated(run);
140
+ } catch (err) {
141
+ setError(err instanceof Error ? err.message : String(err));
142
+ } finally {
143
+ setBusy(false);
144
+ }
145
+ };
146
+
147
+ return (
148
+ <>
149
+ <Tooltip title={populationId ? '' : 'Generate a population first'}>
150
+ <span>
151
+ <Button
152
+ variant={variant}
153
+ size={size}
154
+ startIcon={<FlaskConical size={16} />}
155
+ disabled={!populationId}
156
+ onClick={() => setOpen(true)}
157
+ >
158
+ New evaluation run
159
+ </Button>
160
+ </span>
161
+ </Tooltip>
162
+ <Dialog open={open} onClose={() => (busy ? undefined : setOpen(false))} maxWidth="xs" fullWidth>
163
+ <DialogTitle>Launch evaluation run</DialogTitle>
164
+ <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '12px !important' }}>
165
+ {error ? (
166
+ <Alert severity="error" variant="outlined">
167
+ {error}
168
+ </Alert>
169
+ ) : null}
170
+ <TextField
171
+ label="Model version"
172
+ size="small"
173
+ value={modelVersion}
174
+ onChange={(e) => setModelVersion(e.target.value)}
175
+ />
176
+ <FormControlLabel
177
+ control={
178
+ <Switch checked={injectRegression} onChange={(e) => setInjectRegression(e.target.checked)} />
179
+ }
180
+ label={
181
+ <Box>
182
+ <Typography variant="body2">Inject regression (demo)</Typography>
183
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
184
+ Applies overrides night_penalty=0.30, vru_penalty=0.10
185
+ </Typography>
186
+ </Box>
187
+ }
188
+ />
189
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
190
+ Runs are asynchronous: the run is queued, evaluated across distributed workers, then published.
191
+ </Typography>
192
+ </DialogContent>
193
+ <DialogActions>
194
+ <Button onClick={() => setOpen(false)} disabled={busy}>
195
+ Cancel
196
+ </Button>
197
+ <Button
198
+ variant="contained"
199
+ onClick={() => void submit()}
200
+ disabled={busy || !populationId}
201
+ startIcon={busy ? <CircularProgress size={16} color="inherit" /> : <FlaskConical size={16} />}
202
+ >
203
+ {busy ? 'Queueing…' : 'Launch run'}
204
+ </Button>
205
+ </DialogActions>
206
+ </Dialog>
207
+ </>
208
+ );
209
+ }
210
+
211
+ // ---------------------------------------------------------------- header bar
212
+
213
+ export default function HeaderBar({
214
+ populations,
215
+ populationId,
216
+ onPopulationChange,
217
+ runs,
218
+ runId,
219
+ onRunChange,
220
+ baselineRunId,
221
+ onBaselineChange,
222
+ selectedRun,
223
+ cache,
224
+ onGenerated,
225
+ onRunCreated,
226
+ }: {
227
+ populations: PopulationMeta[];
228
+ populationId: string | null;
229
+ onPopulationChange: (id: string) => void;
230
+ runs: EvaluationRunInfo[];
231
+ runId: string | null;
232
+ onRunChange: (id: string) => void;
233
+ baselineRunId: string | null;
234
+ onBaselineChange: (id: string | null) => void;
235
+ selectedRun: EvaluationRunInfo | null;
236
+ cache: CacheStats | null;
237
+ onGenerated: (pop: PopulationMeta) => void;
238
+ onRunCreated: (run: EvaluationRunInfo) => void;
239
+ }) {
240
+ const sortedRuns = useMemo(
241
+ () => [...runs].sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? '')),
242
+ [runs]
243
+ );
244
+ const publishedRuns = useMemo(() => sortedRuns.filter((r) => r.status === 'published'), [sortedRuns]);
245
+
246
+ const runLabel = (r: EvaluationRunInfo) =>
247
+ `${r.model_version} · ${r.run_id.slice(0, 8)}${r.status === 'published' ? '' : ` (${r.status})`}`;
248
+
249
+ return (
250
+ <Box
251
+ sx={{
252
+ display: 'flex',
253
+ alignItems: 'center',
254
+ gap: 1.25,
255
+ flexWrap: 'wrap',
256
+ p: 1.5,
257
+ bgcolor: '#161b21',
258
+ border: '1px solid #232a31',
259
+ borderRadius: 1,
260
+ }}
261
+ >
262
+ <TextField
263
+ select
264
+ size="small"
265
+ label="Population"
266
+ value={populationId ?? ''}
267
+ onChange={(e) => onPopulationChange(e.target.value)}
268
+ sx={{ minWidth: 220 }}
269
+ disabled={populations.length === 0}
270
+ >
271
+ {populations.map((p) => (
272
+ <MenuItem key={p.population_id} value={p.population_id}>
273
+ {p.name} · {fmtCompact(p.num_objects)} objects
274
+ </MenuItem>
275
+ ))}
276
+ </TextField>
277
+
278
+ <TextField
279
+ select
280
+ size="small"
281
+ label="Evaluation run"
282
+ value={runId && sortedRuns.some((r) => r.run_id === runId) ? runId : ''}
283
+ onChange={(e) => onRunChange(e.target.value)}
284
+ sx={{ minWidth: 240 }}
285
+ disabled={sortedRuns.length === 0}
286
+ >
287
+ {sortedRuns.map((r) => (
288
+ <MenuItem key={r.run_id} value={r.run_id}>
289
+ {runLabel(r)}
290
+ </MenuItem>
291
+ ))}
292
+ </TextField>
293
+
294
+ <TextField
295
+ select
296
+ size="small"
297
+ label="Baseline (compare)"
298
+ value={
299
+ baselineRunId && publishedRuns.some((r) => r.run_id === baselineRunId) ? baselineRunId : ''
300
+ }
301
+ onChange={(e) => onBaselineChange(e.target.value === '' ? null : e.target.value)}
302
+ sx={{ minWidth: 220 }}
303
+ disabled={publishedRuns.length === 0}
304
+ >
305
+ <MenuItem value="">
306
+ <em>None</em>
307
+ </MenuItem>
308
+ {publishedRuns
309
+ .filter((r) => r.run_id !== runId)
310
+ .map((r) => (
311
+ <MenuItem key={r.run_id} value={r.run_id}>
312
+ {runLabel(r)}
313
+ </MenuItem>
314
+ ))}
315
+ </TextField>
316
+
317
+ {selectedRun ? <RunStatusChip status={selectedRun.status} /> : null}
318
+
319
+ <Box sx={{ flex: 1 }} />
320
+
321
+ {cache ? (
322
+ <Tooltip title={`Aggregate cache: ${cache.hits} hits / ${cache.misses} misses`}>
323
+ <Chip
324
+ size="small"
325
+ label={`cache ${cache.entries} · hit ${cache.hit_rate === null ? '—' : fmtPct(cache.hit_rate)}`}
326
+ sx={{ height: 20, fontSize: 10.5, fontFamily: 'monospace', bgcolor: '#232a31', color: '#aab4be' }}
327
+ />
328
+ </Tooltip>
329
+ ) : null}
330
+
331
+ <GeneratePopulationButton onGenerated={onGenerated} />
332
+ <NewRunButton populationId={populationId} onCreated={onRunCreated} />
333
+ </Box>
334
+ );
335
+ }
src/components/megaeval/HeroRow.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Hero aggregates for a published run: population-scale counts followed by the
3
+ * headline quality metrics. Precision/recall are annotated with sampling-verified
4
+ * confidence intervals when review results exist.
5
+ */
6
+ import Box from '@mui/material/Box';
7
+ import Chip from '@mui/material/Chip';
8
+ import type { EvaluationRunInfo, MetricEstimate, ReviewState } from '../../types/megaeval';
9
+ import { fmtCompact, queryEvaluations } from '../../services/megaeval';
10
+ import { usePoll } from '../../services/labeleval';
11
+ import { MetricCard, fmtNum, fmtPct } from '../labeleval/shared';
12
+ import { QueryBadge, rowNum } from './shared';
13
+
14
+ function ciSub(est: MetricEstimate | undefined): string | null {
15
+ if (!est) return null;
16
+ return `95% CI ${fmtPct(est.ci_low)}–${fmtPct(est.ci_high)}, n=${est.n_reviewed.toLocaleString()} reviewed`;
17
+ }
18
+
19
+ const VERIFIED_CHIP = (
20
+ <Chip
21
+ size="small"
22
+ label="sampling-verified"
23
+ sx={{ height: 16, fontSize: 9, fontWeight: 700, bgcolor: '#12303f', color: '#81d4fa', ml: 0.5 }}
24
+ />
25
+ );
26
+
27
+ export default function HeroRow({
28
+ run,
29
+ review,
30
+ refreshKey,
31
+ }: {
32
+ run: EvaluationRunInfo;
33
+ review: ReviewState | null;
34
+ refreshKey: number;
35
+ }) {
36
+ const h = run.headline;
37
+ // Reviewed / verified totals come from the aggregate query API (also shows provenance).
38
+ const totals = usePoll(
39
+ () =>
40
+ queryEvaluations({
41
+ evaluation_id: run.run_id,
42
+ metrics: ['n', 'reviewed', 'verified', 'review_coverage'],
43
+ }),
44
+ null,
45
+ [run.run_id, refreshKey]
46
+ );
47
+ const totalsRow = totals.data?.rows[0];
48
+ const reviewed = totalsRow ? rowNum(totalsRow, 'reviewed') : null;
49
+ const verified = totalsRow ? rowNum(totalsRow, 'verified') : null;
50
+
51
+ const results = review?.results ?? null;
52
+ const precisionCi = ciSub(results?.precision);
53
+ const recallCi = ciSub(results?.recall);
54
+
55
+ return (
56
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
57
+ <Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
58
+ <MetricCard label="Objects evaluated" value={fmtCompact(h.n)} sub="exact (cube)" />
59
+ <MetricCard
60
+ label="Containers"
61
+ value={fmtCompact(h.containers)}
62
+ sub={h.containers_hll_estimate !== undefined ? `HLL est ${fmtCompact(h.containers_hll_estimate)} (approx)` : 'exact'}
63
+ />
64
+ <MetricCard
65
+ label="Reviewed"
66
+ value={fmtCompact(reviewed)}
67
+ sub={
68
+ <Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
69
+ <span>human reviews</span>
70
+ <QueryBadge meta={totals.data?.meta} />
71
+ </Box>
72
+ }
73
+ accent="#42a5f5"
74
+ />
75
+ <MetricCard label="Verified" value={fmtCompact(verified)} accent="#66bb6a" sub="human-verified" />
76
+ </Box>
77
+ <Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
78
+ <MetricCard
79
+ label="Precision"
80
+ value={fmtPct(h.precision)}
81
+ sub={
82
+ precisionCi ? (
83
+ <Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', flexWrap: 'wrap' }}>
84
+ <span>
85
+ {fmtPct(results?.precision.estimate)} ({precisionCi})
86
+ </span>
87
+ {VERIFIED_CHIP}
88
+ </Box>
89
+ ) : (
90
+ 'point estimate'
91
+ )
92
+ }
93
+ />
94
+ <MetricCard
95
+ label="Recall"
96
+ value={fmtPct(h.recall)}
97
+ sub={
98
+ recallCi ? (
99
+ <Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', flexWrap: 'wrap' }}>
100
+ <span>
101
+ {fmtPct(results?.recall.estimate)} ({recallCi})
102
+ </span>
103
+ {VERIFIED_CHIP}
104
+ </Box>
105
+ ) : (
106
+ 'point estimate'
107
+ )
108
+ }
109
+ />
110
+ <MetricCard label="F1" value={fmtPct(h.f1)} />
111
+ <MetricCard label="Mean IoU" value={fmtNum(h.mean_iou, 3)} />
112
+ <MetricCard label="Anomaly rate" value={fmtPct(h.anomaly_rate)} accent="#ffa726" />
113
+ <MetricCard label="Safety recall" value={fmtPct(h.safety_recall)} accent="#ef5350" />
114
+ </Box>
115
+ </Box>
116
+ );
117
+ }
src/components/megaeval/InvestigationTab.tsx ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Investigation panel: multi-criteria error search against the inverted error
3
+ * index. Results stay aggregate-first (counts, by-type shares, worst
4
+ * containers) — clicking a container jumps to the forensic drill-down.
5
+ */
6
+ import { useState } from 'react';
7
+ import Box from '@mui/material/Box';
8
+ import Button from '@mui/material/Button';
9
+ import Checkbox from '@mui/material/Checkbox';
10
+ import Chip from '@mui/material/Chip';
11
+ import CircularProgress from '@mui/material/CircularProgress';
12
+ import FormControlLabel from '@mui/material/FormControlLabel';
13
+ import MenuItem from '@mui/material/MenuItem';
14
+ import Table from '@mui/material/Table';
15
+ import TableBody from '@mui/material/TableBody';
16
+ import TableCell from '@mui/material/TableCell';
17
+ import TableHead from '@mui/material/TableHead';
18
+ import TableRow from '@mui/material/TableRow';
19
+ import TextField from '@mui/material/TextField';
20
+ import Typography from '@mui/material/Typography';
21
+ import { SearchCode, ShieldAlert } from 'lucide-react';
22
+ import type { DimName, ErrorSearchResponse, ErrorType } from '../../types/megaeval';
23
+ import { fmtCompact, getDimensions, searchErrors } from '../../services/megaeval';
24
+ import { usePoll } from '../../services/labeleval';
25
+ import { ErrorNote, MetricCard, SectionCard, fmtNum } from '../labeleval/shared';
26
+ import { DimChips, ERROR_TYPE_COLORS, OutcomeChip } from './shared';
27
+
28
+ const ALL_ERROR_TYPES: ErrorType[] = ['FN', 'FP', 'LOCALIZATION', 'ANOMALY', 'LOW_CONF'];
29
+
30
+ function MultiSelect({
31
+ label,
32
+ options,
33
+ value,
34
+ onChange,
35
+ minWidth = 170,
36
+ }: {
37
+ label: string;
38
+ options: string[];
39
+ value: string[];
40
+ onChange: (v: string[]) => void;
41
+ minWidth?: number;
42
+ }) {
43
+ return (
44
+ <TextField
45
+ select
46
+ size="small"
47
+ label={label}
48
+ value={value}
49
+ onChange={(e) => {
50
+ const v = e.target.value as unknown;
51
+ onChange(typeof v === 'string' ? v.split(',').filter(Boolean) : (v as string[]));
52
+ }}
53
+ SelectProps={{ multiple: true }}
54
+ sx={{ minWidth }}
55
+ >
56
+ {options.map((o) => (
57
+ <MenuItem key={o} value={o}>
58
+ {o}
59
+ </MenuItem>
60
+ ))}
61
+ </TextField>
62
+ );
63
+ }
64
+
65
+ export default function InvestigationTab({
66
+ runId,
67
+ onOpenContainer,
68
+ }: {
69
+ runId: string;
70
+ onOpenContainer: (cid: number) => void;
71
+ }) {
72
+ const dims = usePoll(getDimensions, null, []);
73
+ const dimOptions = (d: DimName): string[] => dims.data?.dimensions[d] ?? [];
74
+
75
+ const [errorTypes, setErrorTypes] = useState<string[]>([]);
76
+ const [classes, setClasses] = useState<string[]>([]);
77
+ const [lightings, setLightings] = useState<string[]>([]);
78
+ const [weathers, setWeathers] = useState<string[]>([]);
79
+ const [confMax, setConfMax] = useState('');
80
+ const [riskMin, setRiskMin] = useState('');
81
+ const [safetyOnly, setSafetyOnly] = useState(false);
82
+
83
+ const [result, setResult] = useState<ErrorSearchResponse | null>(null);
84
+ const [busy, setBusy] = useState(false);
85
+ const [error, setError] = useState<string | null>(null);
86
+
87
+ const runSearch = async () => {
88
+ setBusy(true);
89
+ setError(null);
90
+ try {
91
+ const filters: Partial<Record<DimName, string[]>> = {};
92
+ if (classes.length) filters.class = classes;
93
+ if (lightings.length) filters.lighting = lightings;
94
+ if (weathers.length) filters.weather = weathers;
95
+ const confParsed = Number(confMax);
96
+ const riskParsed = Number(riskMin);
97
+ setResult(
98
+ await searchErrors({
99
+ run_id: runId,
100
+ ...(errorTypes.length ? { error_types: errorTypes as ErrorType[] } : {}),
101
+ ...(Object.keys(filters).length ? { filters } : {}),
102
+ ...(confMax.trim() !== '' && Number.isFinite(confParsed) ? { confidence_max: confParsed } : {}),
103
+ ...(riskMin.trim() !== '' && Number.isFinite(riskParsed) ? { risk_min: riskParsed } : {}),
104
+ ...(safetyOnly ? { safety_only: true } : {}),
105
+ limit_containers: 20,
106
+ })
107
+ );
108
+ } catch (err) {
109
+ setError(err instanceof Error ? err.message : String(err));
110
+ } finally {
111
+ setBusy(false);
112
+ }
113
+ };
114
+
115
+ return (
116
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
117
+ <SectionCard title="Error search">
118
+ <Box sx={{ display: 'flex', gap: 1.25, flexWrap: 'wrap', alignItems: 'center' }}>
119
+ <MultiSelect label="Error types" options={ALL_ERROR_TYPES} value={errorTypes} onChange={setErrorTypes} />
120
+ <MultiSelect label="Class" options={dimOptions('class')} value={classes} onChange={setClasses} />
121
+ <MultiSelect label="Lighting" options={dimOptions('lighting')} value={lightings} onChange={setLightings} />
122
+ <MultiSelect label="Weather" options={dimOptions('weather')} value={weathers} onChange={setWeathers} />
123
+ <TextField
124
+ size="small"
125
+ label="Max confidence"
126
+ type="number"
127
+ value={confMax}
128
+ onChange={(e) => setConfMax(e.target.value)}
129
+ sx={{ width: 130 }}
130
+ inputProps={{ step: 0.05, min: 0, max: 1 }}
131
+ />
132
+ <TextField
133
+ size="small"
134
+ label="Min risk"
135
+ type="number"
136
+ value={riskMin}
137
+ onChange={(e) => setRiskMin(e.target.value)}
138
+ sx={{ width: 110 }}
139
+ inputProps={{ step: 0.1, min: 0 }}
140
+ />
141
+ <FormControlLabel
142
+ control={<Checkbox size="small" checked={safetyOnly} onChange={(e) => setSafetyOnly(e.target.checked)} />}
143
+ label={<Typography variant="body2">Safety-critical only</Typography>}
144
+ />
145
+ <Button
146
+ variant="contained"
147
+ size="small"
148
+ startIcon={busy ? <CircularProgress size={14} color="inherit" /> : <SearchCode size={15} />}
149
+ disabled={busy}
150
+ onClick={() => void runSearch()}
151
+ >
152
+ Search errors
153
+ </Button>
154
+ </Box>
155
+ {error ? <ErrorNote error={error} /> : null}
156
+ </SectionCard>
157
+
158
+ {result ? (
159
+ <>
160
+ <Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
161
+ <MetricCard label="Matched errors" value={fmtCompact(result.matched_errors)} accent="#ef5350" />
162
+ <Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap', alignItems: 'center' }}>
163
+ {ALL_ERROR_TYPES.filter((t) => (result.by_type[t] ?? 0) > 0).map((t) => (
164
+ <Chip
165
+ key={t}
166
+ size="small"
167
+ label={`${t}: ${fmtCompact(result.by_type[t] ?? 0)}`}
168
+ sx={{
169
+ fontFamily: 'monospace',
170
+ fontWeight: 700,
171
+ bgcolor: '#232a31',
172
+ color: ERROR_TYPE_COLORS[t],
173
+ }}
174
+ />
175
+ ))}
176
+ </Box>
177
+ </Box>
178
+
179
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
180
+ <SectionCard title={`Worst containers (${result.worst_containers.length})`} sx={{ flex: '1 1 480px' }}>
181
+ <Table size="small">
182
+ <TableHead>
183
+ <TableRow>
184
+ <TableCell>Container</TableCell>
185
+ <TableCell>Context</TableCell>
186
+ <TableCell align="right">Errors</TableCell>
187
+ <TableCell align="right">Mean risk</TableCell>
188
+ <TableCell align="right">Max severity</TableCell>
189
+ <TableCell align="right">Safety hits</TableCell>
190
+ </TableRow>
191
+ </TableHead>
192
+ <TableBody>
193
+ {result.worst_containers.map((c) => (
194
+ <TableRow
195
+ key={c.container_id}
196
+ hover
197
+ onClick={() => onOpenContainer(c.container_id)}
198
+ sx={{ cursor: 'pointer' }}
199
+ >
200
+ <TableCell sx={{ fontFamily: 'monospace', color: '#4fc3f7' }}>#{c.container_id}</TableCell>
201
+ <TableCell>
202
+ <DimChips values={[c.scenario, c.lighting, c.weather]} />
203
+ </TableCell>
204
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
205
+ {c.error_count}
206
+ </TableCell>
207
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
208
+ {fmtNum(c.mean_risk, 2)}
209
+ </TableCell>
210
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
211
+ {fmtNum(c.max_severity, 2)}
212
+ </TableCell>
213
+ <TableCell align="right" sx={{ fontFamily: 'monospace', color: c.safety_hits > 0 ? '#ef5350' : undefined }}>
214
+ {c.safety_hits}
215
+ </TableCell>
216
+ </TableRow>
217
+ ))}
218
+ </TableBody>
219
+ </Table>
220
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1 }}>
221
+ Click a container to open its forensic object table.
222
+ </Typography>
223
+ </SectionCard>
224
+
225
+ <SectionCard title={`Top examples (${result.examples.length})`} sx={{ flex: '1 1 420px' }}>
226
+ <Box sx={{ maxHeight: 420, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 0.75 }}>
227
+ {result.examples.map((ex) => (
228
+ <Box
229
+ key={ex.error_id}
230
+ sx={{
231
+ display: 'flex',
232
+ alignItems: 'center',
233
+ gap: 1,
234
+ p: 0.75,
235
+ border: '1px solid #232a31',
236
+ borderRadius: 1,
237
+ cursor: 'pointer',
238
+ '&:hover': { bgcolor: '#1b222a' },
239
+ }}
240
+ onClick={() => onOpenContainer(ex.container_id)}
241
+ >
242
+ <OutcomeChip outcome={ex.error_type} />
243
+ {ex.safety_critical ? <ShieldAlert size={14} color="#ef5350" /> : null}
244
+ <Typography variant="body2" sx={{ fontWeight: 600 }}>
245
+ {ex.class}
246
+ </Typography>
247
+ <DimChips values={[ex.scenario, ex.lighting, ex.weather]} />
248
+ <Box sx={{ flex: 1 }} />
249
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#8a949e' }}>
250
+ sev {fmtNum(ex.severity, 2)} · conf {fmtNum(ex.confidence, 2)} · risk {fmtNum(ex.risk_score, 2)} · #
251
+ {ex.container_id}
252
+ </Typography>
253
+ </Box>
254
+ ))}
255
+ </Box>
256
+ </SectionCard>
257
+ </Box>
258
+ </>
259
+ ) : (
260
+ <Typography variant="body2" sx={{ color: '#8a949e' }}>
261
+ Define criteria and search the error index — results are served from the inverted index, not a scan.
262
+ </Typography>
263
+ )}
264
+ </Box>
265
+ );
266
+ }
src/components/megaeval/LineageCard.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Collapsible reproducibility card: every version pin that produced this
3
+ * evaluation run (dataset, model, labels, evaluator code, metric definitions,
4
+ * thresholds, sampling config, seed, hardware).
5
+ */
6
+ import { useState } from 'react';
7
+ import Box from '@mui/material/Box';
8
+ import Collapse from '@mui/material/Collapse';
9
+ import IconButton from '@mui/material/IconButton';
10
+ import Typography from '@mui/material/Typography';
11
+ import { ChevronDown, ChevronUp } from 'lucide-react';
12
+ import type { RunLineage } from '../../types/megaeval';
13
+ import { SectionCard } from '../labeleval/shared';
14
+
15
+ function Field({ label, value }: { label: string; value: string | number }) {
16
+ return (
17
+ <Box sx={{ minWidth: 220, flex: '1 1 220px' }}>
18
+ <Typography variant="caption" sx={{ color: '#8a949e', textTransform: 'uppercase', fontSize: 9.5 }}>
19
+ {label}
20
+ </Typography>
21
+ <Typography variant="body2" sx={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>
22
+ {value}
23
+ </Typography>
24
+ </Box>
25
+ );
26
+ }
27
+
28
+ export default function LineageCard({ lineage }: { lineage: RunLineage }) {
29
+ const [open, setOpen] = useState(true);
30
+ return (
31
+ <SectionCard
32
+ title="Run lineage & reproducibility"
33
+ action={
34
+ <IconButton size="small" onClick={() => setOpen((o) => !o)}>
35
+ {open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
36
+ </IconButton>
37
+ }
38
+ >
39
+ <Collapse in={open}>
40
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }}>
41
+ <Field label="Evaluation ID" value={lineage.evaluation_id} />
42
+ <Field label="Dataset version" value={lineage.dataset_version} />
43
+ <Field label="Model version" value={lineage.model_version} />
44
+ <Field label="Model checkpoint" value={lineage.model_checkpoint} />
45
+ <Field label="Label version" value={lineage.label_version} />
46
+ <Field label="Evaluator code version" value={lineage.evaluator_code_version} />
47
+ <Field label="Metric version" value={lineage.metric_version} />
48
+ <Field label="Seed" value={lineage.seed} />
49
+ <Field label="Hardware" value={lineage.hardware} />
50
+ <Field label="Timestamp" value={new Date(lineage.timestamp).toLocaleString()} />
51
+ </Box>
52
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
53
+ <Box sx={{ flex: '1 1 320px' }}>
54
+ <Typography variant="caption" sx={{ color: '#8a949e', textTransform: 'uppercase', fontSize: 9.5 }}>
55
+ Threshold config
56
+ </Typography>
57
+ <Box
58
+ component="pre"
59
+ sx={{
60
+ m: 0,
61
+ p: 1,
62
+ bgcolor: '#12171d',
63
+ border: '1px solid #232a31',
64
+ borderRadius: 1,
65
+ fontSize: 11.5,
66
+ fontFamily: 'monospace',
67
+ overflowX: 'auto',
68
+ }}
69
+ >
70
+ {JSON.stringify(lineage.threshold_config, null, 2)}
71
+ </Box>
72
+ </Box>
73
+ <Box sx={{ flex: '1 1 320px' }}>
74
+ <Typography variant="caption" sx={{ color: '#8a949e', textTransform: 'uppercase', fontSize: 9.5 }}>
75
+ Sampling config
76
+ </Typography>
77
+ <Box
78
+ component="pre"
79
+ sx={{
80
+ m: 0,
81
+ p: 1,
82
+ bgcolor: '#12171d',
83
+ border: '1px solid #232a31',
84
+ borderRadius: 1,
85
+ fontSize: 11.5,
86
+ fontFamily: 'monospace',
87
+ overflowX: 'auto',
88
+ }}
89
+ >
90
+ {JSON.stringify(lineage.sampling_config, null, 2)}
91
+ </Box>
92
+ </Box>
93
+ </Box>
94
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1.5 }}>
95
+ Identical inputs + seed reproduce this evaluation bit-for-bit.
96
+ </Typography>
97
+ </Collapse>
98
+ </SectionCard>
99
+ );
100
+ }
src/components/megaeval/QualityTab.tsx ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Quality tab: per-class quality bars, error-type distribution, the quality
3
+ * funnel, cross-run quality trend, and sketch-based distributions.
4
+ * Every aggregate-query panel surfaces its provenance badge.
5
+ */
6
+ import { useMemo } from 'react';
7
+ import Box from '@mui/material/Box';
8
+ import Table from '@mui/material/Table';
9
+ import TableBody from '@mui/material/TableBody';
10
+ import TableCell from '@mui/material/TableCell';
11
+ import TableHead from '@mui/material/TableHead';
12
+ import TableRow from '@mui/material/TableRow';
13
+ import Typography from '@mui/material/Typography';
14
+ import type { ErrorType, EvaluationRunInfo } from '../../types/megaeval';
15
+ import {
16
+ fmtCompact,
17
+ getRunDistributions,
18
+ getRunFunnel,
19
+ queryEvaluations,
20
+ searchErrors,
21
+ } from '../../services/megaeval';
22
+ import { usePoll } from '../../services/labeleval';
23
+ import { ErrorNote, HBar, LoadingBox, SectionCard, fmtNum, fmtPct } from '../labeleval/shared';
24
+ import {
25
+ ERROR_TYPE_COLORS,
26
+ ExactnessTag,
27
+ HistogramSparkline,
28
+ PctBarCell,
29
+ QueryBadge,
30
+ rowNum,
31
+ rowStr,
32
+ } from './shared';
33
+
34
+ // ---------------------------------------------------------------- per-class quality
35
+
36
+ function QualityByClass({ runId, refreshKey }: { runId: string; refreshKey: number }) {
37
+ const q = usePoll(
38
+ () =>
39
+ queryEvaluations({
40
+ evaluation_id: runId,
41
+ group_by: ['class'],
42
+ metrics: ['n', 'precision', 'recall', 'f1', 'mean_iou'],
43
+ }),
44
+ null,
45
+ [runId, refreshKey]
46
+ );
47
+ return (
48
+ <SectionCard title="Quality by class" action={<QueryBadge meta={q.data?.meta} />} sx={{ flex: '1 1 460px' }}>
49
+ {q.error ? <ErrorNote error={q.error} /> : null}
50
+ {q.loading && !q.data ? <LoadingBox /> : null}
51
+ {q.data ? (
52
+ <Table size="small">
53
+ <TableHead>
54
+ <TableRow>
55
+ <TableCell>Class</TableCell>
56
+ <TableCell align="right">n</TableCell>
57
+ <TableCell>Recall</TableCell>
58
+ <TableCell>Precision</TableCell>
59
+ <TableCell align="right">F1</TableCell>
60
+ <TableCell align="right">Mean IoU</TableCell>
61
+ </TableRow>
62
+ </TableHead>
63
+ <TableBody>
64
+ {[...q.data.rows]
65
+ .sort((a, b) => b.n - a.n)
66
+ .map((row) => (
67
+ <TableRow key={rowStr(row, 'class')} hover>
68
+ <TableCell sx={{ fontWeight: 600 }}>{rowStr(row, 'class')}</TableCell>
69
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
70
+ {fmtCompact(row.n)}
71
+ </TableCell>
72
+ <TableCell>
73
+ <PctBarCell value={rowNum(row, 'recall')} color="#66bb6a" />
74
+ </TableCell>
75
+ <TableCell>
76
+ <PctBarCell value={rowNum(row, 'precision')} color="#4fc3f7" />
77
+ </TableCell>
78
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
79
+ {fmtPct(rowNum(row, 'f1'))}
80
+ </TableCell>
81
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
82
+ {fmtNum(rowNum(row, 'mean_iou'), 3)}
83
+ </TableCell>
84
+ </TableRow>
85
+ ))}
86
+ </TableBody>
87
+ </Table>
88
+ ) : null}
89
+ </SectionCard>
90
+ );
91
+ }
92
+
93
+ // ---------------------------------------------------------------- error distribution
94
+
95
+ const ERROR_TYPE_ORDER: ErrorType[] = ['FN', 'FP', 'LOCALIZATION', 'ANOMALY', 'LOW_CONF'];
96
+
97
+ function ErrorDistribution({ runId, refreshKey }: { runId: string; refreshKey: number }) {
98
+ const res = usePoll(() => searchErrors({ run_id: runId }), null, [runId, refreshKey]);
99
+ const byType = res.data?.by_type ?? {};
100
+ const total = ERROR_TYPE_ORDER.reduce((acc, t) => acc + (byType[t] ?? 0), 0);
101
+ const max = Math.max(1, ...ERROR_TYPE_ORDER.map((t) => byType[t] ?? 0));
102
+ return (
103
+ <SectionCard title="Error distribution" sx={{ flex: '1 1 320px' }}>
104
+ {res.error ? <ErrorNote error={res.error} /> : null}
105
+ {res.loading && !res.data ? <LoadingBox /> : null}
106
+ {res.data ? (
107
+ <>
108
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1 }}>
109
+ {fmtCompact(res.data.matched_errors)} potential errors indexed (exact)
110
+ </Typography>
111
+ {ERROR_TYPE_ORDER.map((t) => {
112
+ const count = byType[t] ?? 0;
113
+ return (
114
+ <HBar
115
+ key={t}
116
+ label={t}
117
+ value={count}
118
+ max={max}
119
+ color={ERROR_TYPE_COLORS[t]}
120
+ valueLabel={`${fmtCompact(count)} (${total > 0 ? fmtPct(count / total) : '—'})`}
121
+ />
122
+ );
123
+ })}
124
+ </>
125
+ ) : null}
126
+ </SectionCard>
127
+ );
128
+ }
129
+
130
+ // ---------------------------------------------------------------- quality funnel
131
+
132
+ function QualityFunnel({ runId, refreshKey }: { runId: string; refreshKey: number }) {
133
+ const funnel = usePoll(() => getRunFunnel(runId), null, [runId, refreshKey]);
134
+ const stages = funnel.data?.stages ?? [];
135
+ const max = stages.length ? Math.max(...stages.map((s) => s.count)) : 1;
136
+ const est = funnel.data?.estimated_precision ?? null;
137
+ return (
138
+ <SectionCard title="Quality funnel" sx={{ flex: '1 1 420px' }}>
139
+ {funnel.error ? <ErrorNote error={funnel.error} /> : null}
140
+ {funnel.loading && !funnel.data ? <LoadingBox /> : null}
141
+ {funnel.data ? (
142
+ <>
143
+ {stages.map((s) => (
144
+ <HBar
145
+ key={s.stage}
146
+ label={s.stage}
147
+ value={s.count}
148
+ max={max}
149
+ color="#4fc3f7"
150
+ valueLabel={`${fmtCompact(s.count)} (${fmtPct(s.pct_of_population)})`}
151
+ />
152
+ ))}
153
+ {est && est.estimate !== null ? (
154
+ <Typography variant="body2" sx={{ mt: 1.5, color: '#a5d6a7' }}>
155
+ Estimated precision {fmtPct(est.estimate)}
156
+ {est.ci_low !== null && est.ci_high !== null
157
+ ? ` (95% CI ${fmtPct(est.ci_low)}–${fmtPct(est.ci_high)}${
158
+ est.n_reviewed !== null ? `, n=${est.n_reviewed.toLocaleString()} reviewed` : ''
159
+ })`
160
+ : ''}
161
+ </Typography>
162
+ ) : null}
163
+ </>
164
+ ) : null}
165
+ </SectionCard>
166
+ );
167
+ }
168
+
169
+ // ---------------------------------------------------------------- quality trend
170
+
171
+ const TREND_SERIES: Array<{ key: 'precision' | 'recall' | 'safety_recall'; label: string; color: string }> = [
172
+ { key: 'precision', label: 'precision', color: '#4fc3f7' },
173
+ { key: 'recall', label: 'recall', color: '#66bb6a' },
174
+ { key: 'safety_recall', label: 'safety recall', color: '#ffa726' },
175
+ ];
176
+
177
+ function QualityTrend({ runs }: { runs: EvaluationRunInfo[] }) {
178
+ const published = useMemo(
179
+ () =>
180
+ runs
181
+ .filter((r) => r.status === 'published')
182
+ .sort((a, b) => (a.created_at ?? '').localeCompare(b.created_at ?? '')),
183
+ [runs]
184
+ );
185
+
186
+ const width = 520;
187
+ const height = 150;
188
+ const padL = 40;
189
+ const padB = 30;
190
+ const padT = 10;
191
+
192
+ const values = published.flatMap((r) =>
193
+ TREND_SERIES.map((s) => r.headline[s.key]).filter((v): v is number => v !== null && v !== undefined)
194
+ );
195
+ const lo = values.length ? Math.max(0, Math.min(...values) - 0.05) : 0;
196
+ const hi = values.length ? Math.min(1, Math.max(...values) + 0.02) : 1;
197
+ const xFor = (i: number) =>
198
+ padL + (published.length <= 1 ? (width - padL) / 2 : (i / (published.length - 1)) * (width - padL - 10));
199
+ const yFor = (v: number) => padT + (1 - (v - lo) / Math.max(1e-9, hi - lo)) * (height - padT - padB);
200
+
201
+ return (
202
+ <SectionCard title="Quality trend across published runs" sx={{ flex: '1 1 460px' }}>
203
+ {published.length < 2 ? (
204
+ <Typography variant="body2" sx={{ color: '#8a949e' }}>
205
+ {published.length === 0
206
+ ? 'No published runs yet.'
207
+ : 'Only one published run — launch another run to see the trend.'}
208
+ </Typography>
209
+ ) : null}
210
+ {published.length >= 1 ? (
211
+ <>
212
+ <svg width={width} height={height}>
213
+ {[lo, (lo + hi) / 2, hi].map((v, i) => (
214
+ <g key={i}>
215
+ <line x1={padL} x2={width - 5} y1={yFor(v)} y2={yFor(v)} stroke="#232a31" />
216
+ <text x={2} y={yFor(v) + 3} fontSize={9} fill="#8a949e" fontFamily="monospace">
217
+ {fmtPct(v)}
218
+ </text>
219
+ </g>
220
+ ))}
221
+ {TREND_SERIES.map((s) => {
222
+ const pts = published
223
+ .map((r, i) => ({ v: r.headline[s.key], i }))
224
+ .filter((p): p is { v: number; i: number } => p.v !== null && p.v !== undefined);
225
+ if (!pts.length) return null;
226
+ return (
227
+ <g key={s.key}>
228
+ <polyline
229
+ fill="none"
230
+ stroke={s.color}
231
+ strokeWidth={2}
232
+ points={pts.map((p) => `${xFor(p.i)},${yFor(p.v)}`).join(' ')}
233
+ />
234
+ {pts.map((p) => (
235
+ <circle key={p.i} cx={xFor(p.i)} cy={yFor(p.v)} r={3} fill={s.color} />
236
+ ))}
237
+ </g>
238
+ );
239
+ })}
240
+ {published.map((r, i) => (
241
+ <text
242
+ key={r.run_id}
243
+ x={xFor(i)}
244
+ y={height - 6}
245
+ fontSize={9}
246
+ fill="#aab4be"
247
+ textAnchor="middle"
248
+ fontFamily="monospace"
249
+ >
250
+ {r.model_version.length > 14 ? `${r.model_version.slice(0, 13)}…` : r.model_version}
251
+ </text>
252
+ ))}
253
+ </svg>
254
+ <Box sx={{ display: 'flex', gap: 2 }}>
255
+ {TREND_SERIES.map((s) => (
256
+ <Box key={s.key} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
257
+ <Box sx={{ width: 10, height: 3, bgcolor: s.color, borderRadius: 1 }} />
258
+ <Typography variant="caption" sx={{ color: '#aab4be' }}>
259
+ {s.label}
260
+ </Typography>
261
+ </Box>
262
+ ))}
263
+ </Box>
264
+ </>
265
+ ) : null}
266
+ </SectionCard>
267
+ );
268
+ }
269
+
270
+ // ---------------------------------------------------------------- distributions (sketches)
271
+
272
+ function DistributionsCard({ runId, refreshKey }: { runId: string; refreshKey: number }) {
273
+ const dist = usePoll(() => getRunDistributions(runId), null, [runId, refreshKey]);
274
+ const d = dist.data;
275
+ return (
276
+ <SectionCard
277
+ title={
278
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
279
+ <span>Distributions</span>
280
+ <ExactnessTag approx />
281
+ </Box>
282
+ }
283
+ sx={{ flex: '1 1 320px' }}
284
+ >
285
+ {dist.error ? <ErrorNote error={dist.error} /> : null}
286
+ {dist.loading && !d ? <LoadingBox /> : null}
287
+ {d ? (
288
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
289
+ {d.confidence ? (
290
+ <Box>
291
+ <Typography variant="caption" sx={{ color: '#aab4be' }}>
292
+ Confidence — approximate (sketch)
293
+ </Typography>
294
+ <HistogramSparkline sketch={d.confidence} color="#4fc3f7" />
295
+ </Box>
296
+ ) : null}
297
+ {d.iou ? (
298
+ <Box>
299
+ <Typography variant="caption" sx={{ color: '#aab4be' }}>
300
+ IoU — approximate (sketch)
301
+ </Typography>
302
+ <HistogramSparkline sketch={d.iou} color="#66bb6a" />
303
+ </Box>
304
+ ) : null}
305
+ <Box sx={{ display: 'flex', gap: 3, alignItems: 'center' }}>
306
+ <Box>
307
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
308
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
309
+ Containers (HLL)
310
+ </Typography>
311
+ <ExactnessTag approx />
312
+ </Box>
313
+ <Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 700 }}>
314
+ {d.containers_hll_estimate === null ? '—' : fmtCompact(d.containers_hll_estimate)}
315
+ </Typography>
316
+ </Box>
317
+ <Box>
318
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
319
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
320
+ Containers (exact)
321
+ </Typography>
322
+ <ExactnessTag approx={false} />
323
+ </Box>
324
+ <Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 700 }}>
325
+ {d.containers_exact === null ? '—' : fmtCompact(d.containers_exact)}
326
+ </Typography>
327
+ </Box>
328
+ </Box>
329
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
330
+ {d.note}
331
+ </Typography>
332
+ </Box>
333
+ ) : null}
334
+ </SectionCard>
335
+ );
336
+ }
337
+
338
+ // ---------------------------------------------------------------- tab
339
+
340
+ export default function QualityTab({
341
+ runId,
342
+ runs,
343
+ refreshKey,
344
+ }: {
345
+ runId: string;
346
+ runs: EvaluationRunInfo[];
347
+ refreshKey: number;
348
+ }) {
349
+ return (
350
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
351
+ <QualityByClass runId={runId} refreshKey={refreshKey} />
352
+ <ErrorDistribution runId={runId} refreshKey={refreshKey} />
353
+ <QualityFunnel runId={runId} refreshKey={refreshKey} />
354
+ <QualityTrend runs={runs} />
355
+ <DistributionsCard runId={runId} refreshKey={refreshKey} />
356
+ </Box>
357
+ );
358
+ }
src/components/megaeval/ReviewTab.tsx ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Review sampling panel: statistically grounded human review. Build a
3
+ * stratified sampling plan, execute (simulated) reviews, then read precision
4
+ * and recall estimates with Wilson confidence intervals per stratum.
5
+ */
6
+ import { useState } from 'react';
7
+ import Box from '@mui/material/Box';
8
+ import Button from '@mui/material/Button';
9
+ import CircularProgress from '@mui/material/CircularProgress';
10
+ import Table from '@mui/material/Table';
11
+ import TableBody from '@mui/material/TableBody';
12
+ import TableCell from '@mui/material/TableCell';
13
+ import TableHead from '@mui/material/TableHead';
14
+ import TableRow from '@mui/material/TableRow';
15
+ import Typography from '@mui/material/Typography';
16
+ import { ClipboardList, UserCheck } from 'lucide-react';
17
+ import type { MetricEstimate, ReviewState, SamplingFunnel, StratumPlan } from '../../types/megaeval';
18
+ import { executeReview, fmtCompact, planReview } from '../../services/megaeval';
19
+ import { ErrorNote, HBar, SectionCard, fmtPct } from '../labeleval/shared';
20
+
21
+ const FUNNEL_STAGES: Array<{ key: keyof SamplingFunnel; label: string }> = [
22
+ { key: 'population_objects', label: 'Population objects' },
23
+ { key: 'containers', label: 'Containers' },
24
+ { key: 'suspicious_containers', label: 'Suspicious containers' },
25
+ { key: 'candidate_pool', label: 'Candidate pool' },
26
+ { key: 'statistically_selected', label: 'Statistically selected' },
27
+ { key: 'reviewed', label: 'Reviewed' },
28
+ ];
29
+
30
+ function EstimateCard({ label, est }: { label: string; est: MetricEstimate }) {
31
+ return (
32
+ <SectionCard title={`${label} estimate`} sx={{ flex: '1 1 420px' }}>
33
+ <Typography variant="h5" sx={{ fontWeight: 800, color: '#a5d6a7' }}>
34
+ {fmtPct(est.estimate)}
35
+ <Typography component="span" variant="body2" sx={{ color: '#8a949e', ml: 1 }}>
36
+ 95% CI {fmtPct(est.ci_low)}–{fmtPct(est.ci_high)} · n={est.n_reviewed.toLocaleString()} reviewed · frame{' '}
37
+ {fmtCompact(est.frame_size)}
38
+ </Typography>
39
+ </Typography>
40
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1 }}>
41
+ {est.method}
42
+ </Typography>
43
+ <Table size="small">
44
+ <TableHead>
45
+ <TableRow>
46
+ <TableCell>Stratum</TableCell>
47
+ <TableCell align="right">N</TableCell>
48
+ <TableCell align="right">n</TableCell>
49
+ <TableCell align="right">p</TableCell>
50
+ <TableCell align="right">Wilson 95% CI</TableCell>
51
+ </TableRow>
52
+ </TableHead>
53
+ <TableBody>
54
+ {est.strata.map((s) => (
55
+ <TableRow key={s.stratum} hover>
56
+ <TableCell sx={{ fontFamily: 'monospace', fontSize: 12 }}>{s.stratum}</TableCell>
57
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
58
+ {fmtCompact(s.N)}
59
+ </TableCell>
60
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
61
+ {s.n.toLocaleString()}
62
+ </TableCell>
63
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
64
+ {fmtPct(s.p)}
65
+ </TableCell>
66
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
67
+ {fmtPct(s.wilson_ci[0])}–{fmtPct(s.wilson_ci[1])}
68
+ </TableCell>
69
+ </TableRow>
70
+ ))}
71
+ </TableBody>
72
+ </Table>
73
+ </SectionCard>
74
+ );
75
+ }
76
+
77
+ function PlanTable({ title, strata }: { title: string; strata: Record<string, StratumPlan> }) {
78
+ const entries = Object.entries(strata);
79
+ return (
80
+ <SectionCard title={title} sx={{ flex: '1 1 340px' }}>
81
+ <Table size="small">
82
+ <TableHead>
83
+ <TableRow>
84
+ <TableCell>Stratum</TableCell>
85
+ <TableCell align="right">N</TableCell>
86
+ <TableCell align="right">Allocated</TableCell>
87
+ </TableRow>
88
+ </TableHead>
89
+ <TableBody>
90
+ {entries.map(([key, plan]) => (
91
+ <TableRow key={key} hover>
92
+ <TableCell sx={{ fontFamily: 'monospace', fontSize: 12 }}>{plan.label || key}</TableCell>
93
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
94
+ {fmtCompact(plan.N)}
95
+ </TableCell>
96
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
97
+ {plan.allocated.toLocaleString()}
98
+ </TableCell>
99
+ </TableRow>
100
+ ))}
101
+ </TableBody>
102
+ </Table>
103
+ </SectionCard>
104
+ );
105
+ }
106
+
107
+ export default function ReviewTab({
108
+ runId,
109
+ review,
110
+ onReview,
111
+ }: {
112
+ runId: string;
113
+ review: ReviewState | null;
114
+ onReview: (r: ReviewState) => void;
115
+ }) {
116
+ const [busy, setBusy] = useState<'plan' | 'execute' | null>(null);
117
+ const [error, setError] = useState<string | null>(null);
118
+
119
+ const act = async (kind: 'plan' | 'execute') => {
120
+ setBusy(kind);
121
+ setError(null);
122
+ try {
123
+ onReview(kind === 'plan' ? await planReview(runId) : await executeReview(runId));
124
+ } catch (err) {
125
+ setError(err instanceof Error ? err.message : String(err));
126
+ } finally {
127
+ setBusy(null);
128
+ }
129
+ };
130
+
131
+ const funnel = review?.funnel;
132
+ const max = funnel ? Math.max(1, funnel.population_objects) : 1;
133
+
134
+ return (
135
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
136
+ <SectionCard
137
+ title="Statistical review sampling"
138
+ action={
139
+ <Box sx={{ display: 'flex', gap: 1 }}>
140
+ <Button
141
+ size="small"
142
+ variant="outlined"
143
+ startIcon={busy === 'plan' ? <CircularProgress size={14} color="inherit" /> : <ClipboardList size={14} />}
144
+ disabled={busy !== null}
145
+ onClick={() => void act('plan')}
146
+ >
147
+ Build sampling plan
148
+ </Button>
149
+ <Button
150
+ size="small"
151
+ variant="contained"
152
+ startIcon={busy === 'execute' ? <CircularProgress size={14} color="inherit" /> : <UserCheck size={14} />}
153
+ disabled={busy !== null || !review?.planned}
154
+ onClick={() => void act('execute')}
155
+ >
156
+ Execute reviews (simulated)
157
+ </Button>
158
+ </Box>
159
+ }
160
+ >
161
+ {error ? <ErrorNote error={error} /> : null}
162
+ {!review || !review.planned ? (
163
+ <Typography variant="body2" sx={{ color: '#8a949e' }}>
164
+ No sampling plan yet. Reviewing everything at this scale is impossible — build a stratified plan to
165
+ estimate precision and recall from a small, statistically selected sample.
166
+ </Typography>
167
+ ) : (
168
+ <>
169
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1 }}>
170
+ {review.method ?? 'stratified sampling'}
171
+ {review.target_n !== undefined ? ` · target n=${review.target_n.toLocaleString()}` : ''}
172
+ {review.executed ? ' · executed' : ' · planned, not executed'}
173
+ </Typography>
174
+ {funnel
175
+ ? FUNNEL_STAGES.map((s) => (
176
+ <HBar
177
+ key={s.key}
178
+ label={s.label}
179
+ value={funnel[s.key]}
180
+ max={max}
181
+ color={s.key === 'reviewed' ? '#66bb6a' : '#4fc3f7'}
182
+ valueLabel={`${fmtCompact(funnel[s.key])} (${fmtPct(funnel[s.key] / max)})`}
183
+ />
184
+ ))
185
+ : null}
186
+ </>
187
+ )}
188
+ </SectionCard>
189
+
190
+ {review?.planned && !review.executed && review.strata ? (
191
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
192
+ <PlanTable title="Precision strata plan" strata={review.strata.precision} />
193
+ <PlanTable title="Recall strata plan" strata={review.strata.recall} />
194
+ </Box>
195
+ ) : null}
196
+
197
+ {review?.results ? (
198
+ <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
199
+ <EstimateCard label="Precision" est={review.results.precision} />
200
+ <EstimateCard label="Recall" est={review.results.recall} />
201
+ </Box>
202
+ ) : null}
203
+ </Box>
204
+ );
205
+ }
src/components/megaeval/RunProgressCard.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Live progress card for an in-flight (non-published) evaluation run.
3
+ * Fed by the SSE stream when available, otherwise by 1.5s polling.
4
+ */
5
+ import Box from '@mui/material/Box';
6
+ import LinearProgress from '@mui/material/LinearProgress';
7
+ import Typography from '@mui/material/Typography';
8
+ import type { RunProgress } from '../../types/megaeval';
9
+ import { fmtCompact } from '../../services/megaeval';
10
+ import { SectionCard } from '../labeleval/shared';
11
+ import { CompactStat, RunStatusChip } from './shared';
12
+
13
+ function fmtEta(s: number | null): string {
14
+ if (s === null || Number.isNaN(s)) return '—';
15
+ if (s < 60) return `${Math.max(0, Math.round(s))}s`;
16
+ return `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`;
17
+ }
18
+
19
+ export default function RunProgressCard({ progress }: { progress: RunProgress }) {
20
+ // Tolerate either 0..1 or 0..100 percent encodings from the backend.
21
+ const pct = Math.max(0, Math.min(100, progress.percent <= 1 ? progress.percent * 100 : progress.percent));
22
+ return (
23
+ <SectionCard
24
+ title={
25
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
26
+ <span>
27
+ Evaluation run {progress.run_id.slice(0, 8)} · {progress.model_version}
28
+ </span>
29
+ <RunStatusChip status={progress.status} />
30
+ </Box>
31
+ }
32
+ >
33
+ <LinearProgress
34
+ variant="determinate"
35
+ value={pct}
36
+ sx={{ height: 10, borderRadius: 1, mb: 1.5, bgcolor: '#232a31' }}
37
+ />
38
+ <Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
39
+ <CompactStat label="Progress" value={`${pct.toFixed(1)}%`} />
40
+ <CompactStat
41
+ label="Objects"
42
+ value={`${fmtCompact(progress.objects_processed)} / ${fmtCompact(progress.objects_total)}`}
43
+ />
44
+ <CompactStat
45
+ label="Partitions"
46
+ value={`${progress.partitions_done} / ${progress.partitions_total}`}
47
+ />
48
+ <CompactStat label="Workers" value={progress.workers} />
49
+ <CompactStat label="Throughput" value={`${fmtCompact(progress.throughput_objs_per_s)} objs/s`} />
50
+ <CompactStat label="ETA" value={fmtEta(progress.eta_s)} />
51
+ </Box>
52
+ {progress.error ? (
53
+ <Typography variant="body2" sx={{ color: '#ef5350', mt: 1.5, fontFamily: 'monospace' }}>
54
+ {progress.error}
55
+ </Typography>
56
+ ) : (
57
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1.5 }}>
58
+ Map partitions across workers → reduce partial aggregates → materialize cube &amp; indexes → publish.
59
+ </Typography>
60
+ )}
61
+ </SectionCard>
62
+ );
63
+ }
src/components/megaeval/ShiftTab.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Distribution shift panel: cohorts whose share of the evaluation population
3
+ * diverges from the training mix, annotated with the recall gap so a shift
4
+ * that also hurts quality stands out.
5
+ */
6
+ import Box from '@mui/material/Box';
7
+ import Table from '@mui/material/Table';
8
+ import TableBody from '@mui/material/TableBody';
9
+ import TableCell from '@mui/material/TableCell';
10
+ import TableHead from '@mui/material/TableHead';
11
+ import TableRow from '@mui/material/TableRow';
12
+ import Typography from '@mui/material/Typography';
13
+ import { fmtCompact, getRunShift } from '../../services/megaeval';
14
+ import { usePoll } from '../../services/labeleval';
15
+ import { ErrorNote, LoadingBox, SectionCard, fmtPct } from '../labeleval/shared';
16
+
17
+ function gapColor(gap: number | null): string | undefined {
18
+ if (gap === null) return undefined;
19
+ const g = Math.abs(gap);
20
+ if (g >= 0.1) return '#ef5350';
21
+ if (g >= 0.05) return '#ffa726';
22
+ return undefined;
23
+ }
24
+
25
+ export default function ShiftTab({ runId, refreshKey }: { runId: string; refreshKey: number }) {
26
+ const shift = usePoll(() => getRunShift(runId), null, [runId, refreshKey]);
27
+ const data = shift.data;
28
+
29
+ return (
30
+ <SectionCard title="Distribution shift (train mix vs evaluation population)">
31
+ {shift.error ? <ErrorNote error={shift.error} /> : null}
32
+ {shift.loading && !data ? <LoadingBox /> : null}
33
+ {data ? (
34
+ <>
35
+ <Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1 }}>
36
+ {data.method} · thresholds: min eval count {fmtCompact(data.thresholds.min_eval_count)}, relative change ≥{' '}
37
+ {fmtPct(data.thresholds.rel_threshold)}
38
+ </Typography>
39
+ {data.shifts.length === 0 ? (
40
+ <Typography variant="body2" sx={{ color: '#8a949e' }}>
41
+ No significant distribution shifts detected.
42
+ </Typography>
43
+ ) : (
44
+ <Table size="small">
45
+ <TableHead>
46
+ <TableRow>
47
+ <TableCell>Cohort</TableCell>
48
+ <TableCell align="right">Train → eval share</TableCell>
49
+ <TableCell align="right">Relative change</TableCell>
50
+ <TableCell align="right">Eval count</TableCell>
51
+ <TableCell align="right">Cohort recall vs overall</TableCell>
52
+ <TableCell align="right">Recall gap</TableCell>
53
+ </TableRow>
54
+ </TableHead>
55
+ <TableBody>
56
+ {data.shifts.map((s) => {
57
+ const color = gapColor(s.recall_gap);
58
+ return (
59
+ <TableRow key={s.cohort} hover sx={color === '#ef5350' ? { bgcolor: '#2a1214' } : undefined}>
60
+ <TableCell sx={{ fontFamily: 'monospace', fontWeight: 600 }}>{s.cohort}</TableCell>
61
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
62
+ {fmtPct(s.train_share)} → {fmtPct(s.eval_share)}
63
+ </TableCell>
64
+ <TableCell
65
+ align="right"
66
+ sx={{ fontFamily: 'monospace', color: s.relative_change > 0 ? '#ffa726' : '#4fc3f7' }}
67
+ >
68
+ {s.relative_change > 0 ? '+' : ''}
69
+ {Math.round(s.relative_change * 100)}%
70
+ </TableCell>
71
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
72
+ {fmtCompact(s.eval_count)}
73
+ </TableCell>
74
+ <TableCell align="right" sx={{ fontFamily: 'monospace' }}>
75
+ {fmtPct(s.cohort_recall)} vs {fmtPct(s.overall_recall)}
76
+ </TableCell>
77
+ <TableCell align="right" sx={{ fontFamily: 'monospace', fontWeight: 700, color }}>
78
+ {s.recall_gap === null ? '—' : `${(s.recall_gap * 100).toFixed(1)} pts`}
79
+ </TableCell>
80
+ </TableRow>
81
+ );
82
+ })}
83
+ </TableBody>
84
+ </Table>
85
+ )}
86
+ <Box sx={{ mt: 1 }}>
87
+ <Typography variant="caption" sx={{ color: '#8a949e' }}>
88
+ Highlighted rows are cohorts that are over-represented vs training <em>and</em> underperforming on
89
+ recall — prime candidates for targeted data collection.
90
+ </Typography>
91
+ </Box>
92
+ </>
93
+ ) : null}
94
+ </SectionCard>
95
+ );
96
+ }
src/components/megaeval/shared.tsx ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Shared UI helpers for the Evaluation Command Center (megaeval).
3
+ * Complements components/labeleval/shared.tsx with aggregate-first idioms:
4
+ * query provenance badges, exact/approx honesty tags, delta coloring,
5
+ * outcome chips and sketch histogram sparklines.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ import Box from '@mui/material/Box';
9
+ import Chip from '@mui/material/Chip';
10
+ import Tooltip from '@mui/material/Tooltip';
11
+ import Typography from '@mui/material/Typography';
12
+ import { CheckCircle2, AlertTriangle, OctagonAlert } from 'lucide-react';
13
+ import type { ErrorType, HistogramSketch, QueryMeta, QueryRow } from '../../types/megaeval';
14
+ import { fmtCompact } from '../../services/megaeval';
15
+ import { fmtPct } from '../labeleval/shared';
16
+
17
+ // ---------------------------------------------------------------- query provenance
18
+
19
+ const SOURCE_COLORS: Record<QueryMeta['source'], { bg: string; fg: string }> = {
20
+ cache: { bg: '#1b5e20', fg: '#a5d6a7' },
21
+ cube: { bg: '#0d47a1', fg: '#90caf9' },
22
+ scan: { bg: '#e65100', fg: '#ffe0b2' },
23
+ };
24
+
25
+ function fmtLatency(ms: number): string {
26
+ if (ms < 1) return `${ms.toFixed(1)}ms`;
27
+ if (ms < 100) return `${ms.toFixed(1)}ms`;
28
+ return `${Math.round(ms)}ms`;
29
+ }
30
+
31
+ /** "cube · 74ms" / "cache · 0.1ms" provenance badge for POST /api/evaluations/query panels. */
32
+ export function QueryBadge({ meta }: { meta: QueryMeta | null | undefined }) {
33
+ if (!meta) return null;
34
+ const colors = SOURCE_COLORS[meta.source] ?? SOURCE_COLORS.cube;
35
+ const label = `${meta.source} · ${fmtLatency(meta.latency_ms)}`;
36
+ return (
37
+ <Tooltip
38
+ title={`source=${meta.source} · cache_hit=${meta.cache_hit ? 'yes' : 'no'} · cells=${meta.cells_touched}${
39
+ meta.exact ? ' · exact' : ` · approx (${meta.approximate_fields.join(', ') || 'sketch'})`
40
+ }`}
41
+ >
42
+ <Chip
43
+ size="small"
44
+ label={meta.cache_hit ? `${label} ✓` : label}
45
+ sx={{
46
+ height: 18,
47
+ fontSize: 10,
48
+ fontFamily: 'monospace',
49
+ fontWeight: 700,
50
+ bgcolor: colors.bg,
51
+ color: colors.fg,
52
+ }}
53
+ />
54
+ </Tooltip>
55
+ );
56
+ }
57
+
58
+ /** Exact vs sketch-derived honesty tag. */
59
+ export function ExactnessTag({ approx, sx }: { approx: boolean; sx?: object }) {
60
+ return (
61
+ <Chip
62
+ size="small"
63
+ label={approx ? 'approx' : 'exact'}
64
+ sx={{
65
+ height: 16,
66
+ fontSize: 9.5,
67
+ fontFamily: 'monospace',
68
+ bgcolor: approx ? '#4a3b12' : '#12303f',
69
+ color: approx ? '#ffd54f' : '#81d4fa',
70
+ ...sx,
71
+ }}
72
+ />
73
+ );
74
+ }
75
+
76
+ // ---------------------------------------------------------------- query row helpers
77
+
78
+ /** Reads a numeric metric out of a QueryRow (values arrive as string | number | null). */
79
+ export function rowNum(row: QueryRow, key: string): number | null {
80
+ const v = row[key];
81
+ if (v === null || v === undefined) return null;
82
+ const n = typeof v === 'number' ? v : Number(v);
83
+ return Number.isNaN(n) ? null : n;
84
+ }
85
+
86
+ export function rowStr(row: QueryRow, key: string): string {
87
+ const v = row[key];
88
+ return v === null || v === undefined ? '—' : String(v);
89
+ }
90
+
91
+ // ---------------------------------------------------------------- inline percent bar
92
+
93
+ /** Compact percentage cell with an inline bar, for dense aggregate tables. */
94
+ export function PctBarCell({
95
+ value,
96
+ color = '#4fc3f7',
97
+ width = 84,
98
+ }: {
99
+ value: number | null | undefined;
100
+ color?: string;
101
+ width?: number;
102
+ }) {
103
+ if (value === null || value === undefined || Number.isNaN(value)) {
104
+ return (
105
+ <Typography variant="caption" sx={{ color: '#8a949e', fontFamily: 'monospace' }}>
106
+ —
107
+ </Typography>
108
+ );
109
+ }
110
+ const frac = Math.max(0, Math.min(1, value <= 1 ? value : value / 100));
111
+ return (
112
+ <Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}>
113
+ <Box sx={{ width, height: 6, bgcolor: '#232a31', borderRadius: 1, overflow: 'hidden' }}>
114
+ <Box sx={{ height: '100%', width: `${frac * 100}%`, bgcolor: color, borderRadius: 1 }} />
115
+ </Box>
116
+ <Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#e6e9ec', minWidth: 44 }}>
117
+ {fmtPct(value)}
118
+ </Typography>
119
+ </Box>
120
+ );
121
+ }
122
+
123
+ // ---------------------------------------------------------------- outcome / error type chips
124
+
125
+ export const OUTCOME_COLORS: Record<string, { bg: string; fg: string }> = {
126
+ TP: { bg: '#1b5e20', fg: '#a5d6a7' },
127
+ FN: { bg: '#b71c1c', fg: '#ffcdd2' },
128
+ FP: { bg: '#e65100', fg: '#ffe0b2' },
129
+ LOCALIZATION: { bg: '#4a148c', fg: '#ce93d8' },
130
+ ANOMALY: { bg: '#880e4f', fg: '#f48fb1' },
131
+ LOW_CONF: { bg: '#f9a825', fg: '#212121' },
132
+ };
133
+
134
+ export function OutcomeChip({ outcome }: { outcome: string }) {
135
+ const colors = OUTCOME_COLORS[outcome] ?? { bg: '#37474f', fg: '#cfd8dc' };
136
+ return (
137
+ <Chip
138
+ size="small"
139
+ label={outcome}
140
+ sx={{ height: 18, fontSize: 10, fontWeight: 700, fontFamily: 'monospace', bgcolor: colors.bg, color: colors.fg }}
141
+ />
142
+ );
143
+ }
144
+
145
+ export const ERROR_TYPE_COLORS: Record<ErrorType, string> = {
146
+ FN: '#ef5350',
147
+ FP: '#ffa726',
148
+ LOCALIZATION: '#ab47bc',
149
+ ANOMALY: '#ec407a',
150
+ LOW_CONF: '#ffee58',
151
+ };
152
+
153
+ // ---------------------------------------------------------------- run status
154
+
155
+ const RUN_STATUS_COLORS: Record<string, { bg: string; fg: string }> = {
156
+ created: { bg: '#37474f', fg: '#cfd8dc' },
157
+ queued: { bg: '#37474f', fg: '#cfd8dc' },
158
+ running: { bg: '#0d47a1', fg: '#90caf9' },
159
+ reducing: { bg: '#4a148c', fg: '#ce93d8' },
160
+ materializing: { bg: '#004d40', fg: '#80cbc4' },
161
+ published: { bg: '#1b5e20', fg: '#a5d6a7' },
162
+ failed: { bg: '#b71c1c', fg: '#ffcdd2' },
163
+ };
164
+
165
+ export function RunStatusChip({ status }: { status: string }) {
166
+ const colors = RUN_STATUS_COLORS[status] ?? { bg: '#37474f', fg: '#cfd8dc' };
167
+ return (
168
+ <Chip
169
+ size="small"
170
+ label={status.toUpperCase()}
171
+ sx={{ height: 20, fontSize: 10, fontWeight: 700, letterSpacing: 0.5, bgcolor: colors.bg, color: colors.fg }}
172
+ />
173
+ );
174
+ }
175
+
176
+ // ---------------------------------------------------------------- container status
177
+
178
+ export function ContainerStatusIcon({ status }: { status: 'ok' | 'warn' | 'critical' }) {
179
+ if (status === 'critical') return <OctagonAlert size={15} color="#ef5350" />;
180
+ if (status === 'warn') return <AlertTriangle size={15} color="#ffa726" />;
181
+ return <CheckCircle2 size={15} color="#66bb6a" />;
182
+ }
183
+
184
+ // ---------------------------------------------------------------- deltas
185
+
186
+ /** Colored delta text. Set higherIsBetter=false for metrics like anomaly_rate / error_rate / fp. */
187
+ export function DeltaText({
188
+ delta,
189
+ higherIsBetter = true,
190
+ asPct = true,
191
+ }: {
192
+ delta: number;
193
+ higherIsBetter?: boolean;
194
+ asPct?: boolean;
195
+ }) {
196
+ const good = higherIsBetter ? delta >= 0 : delta <= 0;
197
+ const neutral = Math.abs(delta) < 1e-9;
198
+ const magnitude = asPct ? (Math.abs(delta) * 100).toFixed(1) : Math.abs(delta).toFixed(3);
199
+ const sign = delta > 0 ? '+' : delta < 0 ? '−' : '±';
200
+ const text = `${sign}${magnitude}`;
201
+ return (
202
+ <Typography
203
+ component="span"
204
+ variant="caption"
205
+ sx={{
206
+ fontFamily: 'monospace',
207
+ fontWeight: 700,
208
+ color: neutral ? '#8a949e' : good ? '#66bb6a' : '#ef5350',
209
+ }}
210
+ >
211
+ {text}
212
+ </Typography>
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------- dimension chips
217
+
218
+ export function DimChips({ values, sx }: { values: (string | undefined)[]; sx?: object }) {
219
+ return (
220
+ <Box sx={{ display: 'inline-flex', gap: 0.5, flexWrap: 'wrap', ...sx }}>
221
+ {values
222
+ .filter((v): v is string => Boolean(v))
223
+ .map((v, i) => (
224
+ <Chip
225
+ key={`${v}-${i}`}
226
+ size="small"
227
+ label={v}
228
+ sx={{ height: 18, fontSize: 10, bgcolor: '#232a31', color: '#aab4be' }}
229
+ />
230
+ ))}
231
+ </Box>
232
+ );
233
+ }
234
+
235
+ // ---------------------------------------------------------------- histogram sparkline (sketch)
236
+
237
+ /** SVG histogram sparkline with p10 / p50 / p90 markers. Sketch-derived — always labeled approx. */
238
+ export function HistogramSparkline({
239
+ sketch,
240
+ color = '#4fc3f7',
241
+ width = 260,
242
+ height = 56,
243
+ }: {
244
+ sketch: HistogramSketch;
245
+ color?: string;
246
+ width?: number;
247
+ height?: number;
248
+ }) {
249
+ const maxCount = Math.max(1, ...sketch.counts);
250
+ const barW = width / Math.max(1, sketch.counts.length);
251
+ const span = sketch.hi - sketch.lo || 1;
252
+ const xFor = (v: number) => ((v - sketch.lo) / span) * width;
253
+ const markers: Array<{ key: string; v: number }> = ['p10', 'p50', 'p90']
254
+ .filter((k) => sketch.percentiles[k] !== undefined)
255
+ .map((k) => ({ key: k, v: sketch.percentiles[k] }));
256
+ return (
257
+ <Box>
258
+ <svg width={width} height={height + 14}>
259
+ {sketch.counts.map((c, i) => {
260
+ const h = (c / maxCount) * height;
261
+ return (
262
+ <rect
263
+ key={i}
264
+ x={i * barW + 0.5}
265
+ y={height - h}
266
+ width={Math.max(1, barW - 1)}
267
+ height={Math.max(0.5, h)}
268
+ fill={color}
269
+ opacity={0.75}
270
+ />
271
+ );
272
+ })}
273
+ {markers.map((m) => (
274
+ <g key={m.key}>
275
+ <line x1={xFor(m.v)} x2={xFor(m.v)} y1={0} y2={height} stroke="#ffd54f" strokeDasharray="2,2" />
276
+ <text x={xFor(m.v)} y={height + 11} fontSize={9} fill="#ffd54f" textAnchor="middle" fontFamily="monospace">
277
+ {m.key} {m.v.toFixed(2)}
278
+ </text>
279
+ </g>
280
+ ))}
281
+ </svg>
282
+ </Box>
283
+ );
284
+ }
285
+
286
+ // ---------------------------------------------------------------- misc
287
+
288
+ export function CompactStat({ label, value }: { label: string; value: ReactNode }) {
289
+ return (
290
+ <Box sx={{ display: 'flex', flexDirection: 'column', minWidth: 90 }}>
291
+ <Typography variant="caption" sx={{ color: '#8a949e', textTransform: 'uppercase', fontSize: 9.5 }}>
292
+ {label}
293
+ </Typography>
294
+ <Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 700 }}>
295
+ {value}
296
+ </Typography>
297
+ </Box>
298
+ );
299
+ }
300
+
301
+ export function compactWithExact(n: number | null | undefined): string {
302
+ if (n === null || n === undefined) return '—';
303
+ return `${fmtCompact(n)} (${Math.round(n).toLocaleString()})`;
304
+ }
src/context/LabelEvalContext.tsx CHANGED
@@ -2,6 +2,7 @@ import { createContext, useContext } from 'react';
2
  import type { StreamEvent } from '../types/labeleval';
3
 
4
  export type PageId =
 
5
  | 'overview'
6
  | 'datasets'
7
  | 'label-generation'
@@ -19,6 +20,7 @@ export type PageId =
19
  | 'legacy';
20
 
21
  export const ALL_PAGE_IDS: PageId[] = [
 
22
  'overview',
23
  'datasets',
24
  'label-generation',
 
2
  import type { StreamEvent } from '../types/labeleval';
3
 
4
  export type PageId =
5
+ | 'command'
6
  | 'overview'
7
  | 'datasets'
8
  | 'label-generation'
 
20
  | 'legacy';
21
 
22
  export const ALL_PAGE_IDS: PageId[] = [
23
+ 'command',
24
  'overview',
25
  'datasets',
26
  'label-generation',
src/pages/CommandCenterPage.tsx ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Evaluation Command Center — the aggregate-first landing page.
3
+ *
4
+ * Mental model surfaced in the UI:
5
+ * Dataset (population) → Evaluation Run → Population → Cohort → Container → Annotation
6
+ * Aggregates come first; individual annotations are the deepest drill-down.
7
+ *
8
+ * Runs are async: launched runs are queued, progress streams over SSE
9
+ * (stream.megaeval.active_runs) with a 1.5s polling fallback on GET /runs/{id},
10
+ * and the page auto-refreshes when the run publishes.
11
+ */
12
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
+ import Alert from '@mui/material/Alert';
14
+ import Box from '@mui/material/Box';
15
+ import Tab from '@mui/material/Tab';
16
+ import Tabs from '@mui/material/Tabs';
17
+ import Typography from '@mui/material/Typography';
18
+ import type { EvaluationRunInfo, ReviewState, RunStatus } from '../types/megaeval';
19
+ import {
20
+ getCacheStats,
21
+ getPopulations,
22
+ getRun,
23
+ getRunReview,
24
+ getRuns,
25
+ } from '../services/megaeval';
26
+ import { usePoll } from '../services/labeleval';
27
+ import { useLabelEval } from '../context/LabelEvalContext';
28
+ import { EmptyState, ErrorNote, LoadingBox, SectionCard } from '../components/labeleval/shared';
29
+ import HeaderBar, {
30
+ GeneratePopulationButton,
31
+ NewRunButton,
32
+ } from '../components/megaeval/HeaderBar';
33
+ import RunProgressCard from '../components/megaeval/RunProgressCard';
34
+ import HeroRow from '../components/megaeval/HeroRow';
35
+ import QualityTab from '../components/megaeval/QualityTab';
36
+ import CohortTab from '../components/megaeval/CohortTab';
37
+ import ContainersTab from '../components/megaeval/ContainersTab';
38
+ import InvestigationTab from '../components/megaeval/InvestigationTab';
39
+ import CompareTab from '../components/megaeval/CompareTab';
40
+ import ReviewTab from '../components/megaeval/ReviewTab';
41
+ import ShiftTab from '../components/megaeval/ShiftTab';
42
+ import LineageCard from '../components/megaeval/LineageCard';
43
+
44
+ const ACTIVE_STATUSES: RunStatus[] = ['created', 'queued', 'running', 'reducing', 'materializing'];
45
+
46
+ type TabId =
47
+ | 'quality'
48
+ | 'cohorts'
49
+ | 'containers'
50
+ | 'investigation'
51
+ | 'compare'
52
+ | 'review'
53
+ | 'shift'
54
+ | 'lineage';
55
+
56
+ export default function CommandCenterPage() {
57
+ const { stream, entityId, navigate } = useLabelEval();
58
+
59
+ const populationsPoll = usePoll(getPopulations, 15000);
60
+ const runsPoll = usePoll(getRuns, 8000);
61
+ const cachePoll = usePoll(getCacheStats, 10000);
62
+
63
+ const populations = useMemo(() => {
64
+ const list = populationsPoll.data?.populations ?? [];
65
+ return [...list].sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''));
66
+ }, [populationsPoll.data]);
67
+ const runs = useMemo(() => runsPoll.data?.runs ?? [], [runsPoll.data]);
68
+
69
+ const [populationId, setPopulationId] = useState<string | null>(null);
70
+ const [runId, setRunId] = useState<string | null>(entityId);
71
+ const [baselineRunId, setBaselineRunId] = useState<string | null>(null);
72
+ const [tab, setTab] = useState<TabId>('quality');
73
+ const [drillContainerId, setDrillContainerId] = useState<number | null>(null);
74
+ const [publishTick, setPublishTick] = useState(0);
75
+ const [review, setReview] = useState<ReviewState | null>(null);
76
+ const [selectedRun, setSelectedRun] = useState<EvaluationRunInfo | null>(null);
77
+ const [runError, setRunError] = useState<string | null>(null);
78
+
79
+ // Explicit selection persists the run in the URL hash (#/command/<run_id>).
80
+ const selectRun = useCallback(
81
+ (id: string) => {
82
+ setRunId(id);
83
+ setDrillContainerId(null);
84
+ navigate('command', id);
85
+ },
86
+ [navigate]
87
+ );
88
+
89
+ // Follow hash changes (back/forward navigation, deep links).
90
+ useEffect(() => {
91
+ if (entityId && entityId !== runId) {
92
+ setRunId(entityId);
93
+ setDrillContainerId(null);
94
+ }
95
+ }, [entityId]); // eslint-disable-line react-hooks/exhaustive-deps
96
+
97
+ // Default selections once data arrives.
98
+ useEffect(() => {
99
+ if (!populationId && populations.length) setPopulationId(populations[0].population_id);
100
+ }, [populations, populationId]);
101
+
102
+ useEffect(() => {
103
+ if (!runId && runs.length) {
104
+ const byCreated = [...runs].sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''));
105
+ const pick = byCreated.find((r) => r.status === 'published') ?? byCreated[0];
106
+ if (pick) setRunId(pick.run_id);
107
+ }
108
+ }, [runs, runId]);
109
+
110
+ // Selected run detail: 1.5s polling while the run is in flight (SSE-independent fallback).
111
+ useEffect(() => {
112
+ setSelectedRun(null);
113
+ setRunError(null);
114
+ if (!runId) return;
115
+ let cancelled = false;
116
+ let timer: ReturnType<typeof setTimeout> | null = null;
117
+ const load = async () => {
118
+ try {
119
+ const info = await getRun(runId);
120
+ if (cancelled) return;
121
+ setSelectedRun(info);
122
+ setRunError(null);
123
+ if (info.status !== 'published' && info.status !== 'failed') timer = setTimeout(() => void load(), 1500);
124
+ } catch (err) {
125
+ if (cancelled) return;
126
+ setRunError(err instanceof Error ? err.message : String(err));
127
+ timer = setTimeout(() => void load(), 3000);
128
+ }
129
+ };
130
+ void load();
131
+ return () => {
132
+ cancelled = true;
133
+ if (timer) clearTimeout(timer);
134
+ };
135
+ }, [runId]);
136
+
137
+ // Auto-refresh all panels when the selected run publishes.
138
+ const prevStatusRef = useRef<RunStatus | null>(null);
139
+ const runsRefresh = runsPoll.refresh;
140
+ useEffect(() => {
141
+ const status = selectedRun?.status ?? null;
142
+ if (status === 'published' && prevStatusRef.current && prevStatusRef.current !== 'published') {
143
+ setPublishTick((t) => t + 1);
144
+ runsRefresh();
145
+ }
146
+ prevStatusRef.current = status;
147
+ }, [selectedRun?.status, runsRefresh]);
148
+
149
+ const published = selectedRun?.status === 'published';
150
+
151
+ // Review state (shared by the hero CI annotations and the Review tab).
152
+ useEffect(() => {
153
+ setReview(null);
154
+ if (!runId || !published) return;
155
+ let cancelled = false;
156
+ getRunReview(runId)
157
+ .then((r) => {
158
+ if (!cancelled) setReview(r);
159
+ })
160
+ .catch(() => undefined);
161
+ return () => {
162
+ cancelled = true;
163
+ };
164
+ }, [runId, published, publishTick]);
165
+
166
+ const megaStatus = stream?.megaeval ?? null;
167
+ const cache = megaStatus?.cache ?? cachePoll.data ?? null;
168
+
169
+ // Prefer SSE progress for the selected run when it is fresher than the poll.
170
+ const streamProgress = megaStatus?.active_runs.find((r) => r.run_id === runId) ?? null;
171
+ const selectedInFlight =
172
+ selectedRun && ACTIVE_STATUSES.includes(selectedRun.status) ? streamProgress ?? selectedRun : null;
173
+ const otherActiveRuns = (megaStatus?.active_runs ?? []).filter((r) => r.run_id !== runId);
174
+
175
+ const openContainer = useCallback((cid: number) => {
176
+ setDrillContainerId(cid);
177
+ setTab('containers');
178
+ }, []);
179
+
180
+ const initialLoading =
181
+ populationsPoll.loading && !populationsPoll.data && runsPoll.loading && !runsPoll.data;
182
+ const noPopulation = populationsPoll.data !== null && populations.length === 0;
183
+ const noRuns = !noPopulation && runsPoll.data !== null && runs.length === 0;
184
+
185
+ const onPopulationGenerated = useCallback(
186
+ (pop: { population_id: string }) => {
187
+ populationsPoll.refresh();
188
+ setPopulationId(pop.population_id);
189
+ },
190
+ [populationsPoll]
191
+ );
192
+
193
+ const onRunCreated = useCallback(
194
+ (run: EvaluationRunInfo) => {
195
+ runsRefresh();
196
+ selectRun(run.run_id);
197
+ },
198
+ [runsRefresh, selectRun]
199
+ );
200
+
201
+ if (initialLoading) return <LoadingBox label="Loading command center…" />;
202
+
203
+ return (
204
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
205
+ {populationsPoll.error && !populationsPoll.data ? <ErrorNote error={populationsPoll.error} /> : null}
206
+
207
+ <HeaderBar
208
+ populations={populations}
209
+ populationId={populationId}
210
+ onPopulationChange={setPopulationId}
211
+ runs={runs}
212
+ runId={runId}
213
+ onRunChange={selectRun}
214
+ baselineRunId={baselineRunId}
215
+ onBaselineChange={setBaselineRunId}
216
+ selectedRun={selectedRun}
217
+ cache={cache}
218
+ onGenerated={onPopulationGenerated}
219
+ onRunCreated={onRunCreated}
220
+ />
221
+
222
+ <Typography variant="caption" sx={{ color: '#8a949e', mt: -1 }}>
223
+ Dataset (population) → Evaluation run → Population → Cohort → Container → Annotation — aggregates first,
224
+ individual annotations only at the deepest drill-down.
225
+ </Typography>
226
+
227
+ {/* Empty states: nothing generated / no runs yet */}
228
+ {noPopulation ? (
229
+ <SectionCard title="Welcome to the Evaluation Command Center">
230
+ <EmptyState
231
+ title="No evaluation population yet"
232
+ message="Generate a synthetic mega-scale population (hundreds of thousands of annotated objects across weather, lighting, scenario and sensor cohorts), then launch an evaluation run against it."
233
+ action={
234
+ <Box sx={{ display: 'flex', gap: 1.5, justifyContent: 'center' }}>
235
+ <GeneratePopulationButton variant="contained" size="large" onGenerated={onPopulationGenerated} />
236
+ </Box>
237
+ }
238
+ />
239
+ </SectionCard>
240
+ ) : null}
241
+
242
+ {noRuns ? (
243
+ <SectionCard title="Population ready — no evaluation runs yet">
244
+ <EmptyState
245
+ title="Launch your first evaluation run"
246
+ message="An evaluation run scores every object in the population against a model version, then materializes the aggregate cube, error index and sketches that power this page."
247
+ action={
248
+ <Box sx={{ display: 'flex', gap: 1.5, justifyContent: 'center' }}>
249
+ <NewRunButton variant="contained" size="large" populationId={populationId} onCreated={onRunCreated} />
250
+ <GeneratePopulationButton variant="outlined" size="large" onGenerated={onPopulationGenerated} />
251
+ </Box>
252
+ }
253
+ />
254
+ </SectionCard>
255
+ ) : null}
256
+
257
+ {runError && !selectedRun ? <ErrorNote error={runError} /> : null}
258
+
259
+ {/* Live progress for the selected run + any other active runs */}
260
+ {selectedInFlight ? <RunProgressCard progress={selectedInFlight} /> : null}
261
+ {otherActiveRuns.map((r) => (
262
+ <RunProgressCard key={r.run_id} progress={r} />
263
+ ))}
264
+
265
+ {selectedRun?.status === 'failed' ? (
266
+ <Alert severity="error" variant="outlined">
267
+ Run {selectedRun.run_id.slice(0, 8)} failed{selectedRun.error ? `: ${selectedRun.error}` : '.'} Launch a new
268
+ evaluation run from the header.
269
+ </Alert>
270
+ ) : null}
271
+
272
+ {/* Hero aggregates + tab sections (published runs only) */}
273
+ {published && selectedRun ? (
274
+ <>
275
+ <HeroRow run={selectedRun} review={review} refreshKey={publishTick} />
276
+
277
+ <Tabs
278
+ value={tab}
279
+ onChange={(_, v: TabId) => setTab(v)}
280
+ variant="scrollable"
281
+ scrollButtons="auto"
282
+ sx={{
283
+ minHeight: 36,
284
+ borderBottom: '1px solid #232a31',
285
+ '& .MuiTab-root': { minHeight: 36, py: 0.5 },
286
+ }}
287
+ >
288
+ <Tab label="Quality" value="quality" />
289
+ <Tab label="Cohorts" value="cohorts" />
290
+ <Tab label="Containers" value="containers" />
291
+ <Tab label="Investigation" value="investigation" />
292
+ <Tab label="Compare" value="compare" />
293
+ <Tab label="Review" value="review" />
294
+ <Tab label="Shift" value="shift" />
295
+ <Tab label="Lineage" value="lineage" />
296
+ </Tabs>
297
+
298
+ {tab === 'quality' ? (
299
+ <QualityTab runId={selectedRun.run_id} runs={runs} refreshKey={publishTick} />
300
+ ) : null}
301
+ {tab === 'cohorts' ? <CohortTab runId={selectedRun.run_id} refreshKey={publishTick} /> : null}
302
+ {tab === 'containers' ? (
303
+ <ContainersTab
304
+ runId={selectedRun.run_id}
305
+ refreshKey={publishTick}
306
+ drillContainerId={drillContainerId}
307
+ onDrill={setDrillContainerId}
308
+ />
309
+ ) : null}
310
+ {tab === 'investigation' ? (
311
+ <InvestigationTab runId={selectedRun.run_id} onOpenContainer={openContainer} />
312
+ ) : null}
313
+ {tab === 'compare' ? (
314
+ <CompareTab runId={selectedRun.run_id} baselineRunId={baselineRunId} refreshKey={publishTick} />
315
+ ) : null}
316
+ {tab === 'review' ? (
317
+ <ReviewTab runId={selectedRun.run_id} review={review} onReview={setReview} />
318
+ ) : null}
319
+ {tab === 'shift' ? <ShiftTab runId={selectedRun.run_id} refreshKey={publishTick} /> : null}
320
+ {tab === 'lineage' ? <LineageCard lineage={selectedRun.lineage} /> : null}
321
+ </>
322
+ ) : null}
323
+ </Box>
324
+ );
325
+ }
src/services/labeleval.ts CHANGED
@@ -34,6 +34,8 @@ import type {
34
  TrainRequest,
35
  TrainResponse,
36
  } from '../types/labeleval';
 
 
37
 
38
  async function handle<T>(res: Response): Promise<T> {
39
  if (!res.ok) {
@@ -232,12 +234,19 @@ export function useStream(): StreamEvent | null {
232
  } catch {
233
  /* keep previous alert count */
234
  }
 
 
 
 
 
 
235
  if (!disposed) {
236
  setEvent({
237
  ts: new Date().toISOString(),
238
  pipeline,
239
  training: null,
240
  alerts_count: lastAlerts,
 
241
  });
242
  }
243
  } catch {
 
34
  TrainRequest,
35
  TrainResponse,
36
  } from '../types/labeleval';
37
+ import type { MegaevalStatus } from '../types/megaeval';
38
+ import { getMegaevalStatus } from './megaeval';
39
 
40
  async function handle<T>(res: Response): Promise<T> {
41
  if (!res.ok) {
 
234
  } catch {
235
  /* keep previous alert count */
236
  }
237
+ let megaeval: MegaevalStatus | undefined;
238
+ try {
239
+ megaeval = await getMegaevalStatus();
240
+ } catch {
241
+ /* megaeval layer unavailable — leave the field absent */
242
+ }
243
  if (!disposed) {
244
  setEvent({
245
  ts: new Date().toISOString(),
246
  pipeline,
247
  training: null,
248
  alerts_count: lastAlerts,
249
+ ...(megaeval ? { megaeval } : {}),
250
  });
251
  }
252
  } catch {
src/services/megaeval.ts ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Typed fetch wrappers for the aggregate-first mega-scale evaluation API
3
+ * ("megaeval"). Mirrors the style of services/labeleval.ts.
4
+ *
5
+ * Async run semantics: POST /runs returns immediately with status "queued";
6
+ * results endpoints return HTTP 409 until the run is "published".
7
+ */
8
+ import type {
9
+ CacheStats,
10
+ CompareRequest,
11
+ CompareResponse,
12
+ ContainerObjectsResponse,
13
+ ContainerSortPreset,
14
+ ContainersResponse,
15
+ CreateRunRequest,
16
+ DimName,
17
+ DistributionsResponse,
18
+ ErrorSearchRequest,
19
+ ErrorSearchResponse,
20
+ EvaluationQueryRequest,
21
+ EvaluationQueryResponse,
22
+ EvaluationRunInfo,
23
+ GeneratePopulationRequest,
24
+ MegaevalStatus,
25
+ PopulationMeta,
26
+ QualityFunnelResponse,
27
+ ReviewState,
28
+ ShiftResponse,
29
+ SimilarityRequest,
30
+ SimilarityResponse,
31
+ WhyRequest,
32
+ WhyResponse,
33
+ } from '../types/megaeval';
34
+
35
+ async function handle<T>(res: Response): Promise<T> {
36
+ if (!res.ok) {
37
+ const body = await res.text().catch(() => '');
38
+ throw new Error(`API ${res.status}: ${body || res.statusText}`);
39
+ }
40
+ return res.json() as Promise<T>;
41
+ }
42
+
43
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
44
+
45
+ function get<T>(url: string): Promise<T> {
46
+ return fetch(url).then((res) => handle<T>(res));
47
+ }
48
+
49
+ function post<T>(url: string, body?: unknown): Promise<T> {
50
+ return fetch(url, {
51
+ method: 'POST',
52
+ headers: JSON_HEADERS,
53
+ body: body === undefined ? undefined : JSON.stringify(body),
54
+ }).then((res) => handle<T>(res));
55
+ }
56
+
57
+ // ---------------------------------------------------------------- populations
58
+
59
+ export const generatePopulation = (body: GeneratePopulationRequest) =>
60
+ post<PopulationMeta>('/api/megaeval/population/generate', body);
61
+
62
+ export const getPopulations = () =>
63
+ get<{ populations: PopulationMeta[] }>('/api/megaeval/populations');
64
+
65
+ export const getDimensions = () =>
66
+ get<{ dimensions: Record<DimName, string[]> }>('/api/megaeval/dimensions');
67
+
68
+ // ---------------------------------------------------------------- runs
69
+
70
+ export const createRun = (body: CreateRunRequest) =>
71
+ post<EvaluationRunInfo>('/api/megaeval/runs', body);
72
+
73
+ export const getRuns = () => get<{ runs: EvaluationRunInfo[] }>('/api/megaeval/runs');
74
+
75
+ export const getRun = (runId: string) =>
76
+ get<EvaluationRunInfo>(`/api/megaeval/runs/${encodeURIComponent(runId)}`);
77
+
78
+ export const getMegaevalStatus = () => get<MegaevalStatus>('/api/megaeval/status');
79
+
80
+ // ---------------------------------------------------------------- query / cache
81
+
82
+ export const queryEvaluations = (body: EvaluationQueryRequest) =>
83
+ post<EvaluationQueryResponse>('/api/evaluations/query', body);
84
+
85
+ export const getCacheStats = () => get<CacheStats>('/api/megaeval/cache');
86
+
87
+ // ---------------------------------------------------------------- run results
88
+
89
+ export const getRunFunnel = (runId: string) =>
90
+ get<QualityFunnelResponse>(`/api/megaeval/runs/${encodeURIComponent(runId)}/funnel`);
91
+
92
+ export const getRunContainers = (
93
+ runId: string,
94
+ sort: ContainerSortPreset = 'highest_risk',
95
+ limit = 50,
96
+ offset = 0
97
+ ) =>
98
+ get<ContainersResponse>(
99
+ `/api/megaeval/runs/${encodeURIComponent(runId)}/containers?sort=${encodeURIComponent(
100
+ sort
101
+ )}&limit=${limit}&offset=${offset}`
102
+ );
103
+
104
+ export const getContainerObjects = (runId: string, containerId: number) =>
105
+ get<ContainerObjectsResponse>(
106
+ `/api/megaeval/runs/${encodeURIComponent(runId)}/containers/${containerId}/objects`
107
+ );
108
+
109
+ export const searchErrors = (body: ErrorSearchRequest) =>
110
+ post<ErrorSearchResponse>('/api/megaeval/errors/search', body);
111
+
112
+ export const compareRuns = (body: CompareRequest) =>
113
+ post<CompareResponse>('/api/megaeval/compare', body);
114
+
115
+ export const getRunShift = (runId: string) =>
116
+ get<ShiftResponse>(`/api/megaeval/runs/${encodeURIComponent(runId)}/shift`);
117
+
118
+ export const whyAnalysis = (body: WhyRequest) => post<WhyResponse>('/api/megaeval/why', body);
119
+
120
+ export const findSimilarContainers = (body: SimilarityRequest) =>
121
+ post<SimilarityResponse>('/api/megaeval/similarity', body);
122
+
123
+ // ---------------------------------------------------------------- review sampling
124
+
125
+ export const getRunReview = (runId: string) =>
126
+ get<ReviewState>(`/api/megaeval/runs/${encodeURIComponent(runId)}/review`);
127
+
128
+ export const planReview = (runId: string, targetN?: number) =>
129
+ post<ReviewState>(
130
+ `/api/megaeval/runs/${encodeURIComponent(runId)}/review/plan`,
131
+ targetN !== undefined ? { target_n: targetN } : {}
132
+ );
133
+
134
+ export const executeReview = (runId: string) =>
135
+ post<ReviewState>(`/api/megaeval/runs/${encodeURIComponent(runId)}/review/execute`);
136
+
137
+ // ---------------------------------------------------------------- distributions
138
+
139
+ export const getRunDistributions = (runId: string) =>
140
+ get<DistributionsResponse>(`/api/megaeval/runs/${encodeURIComponent(runId)}/distributions`);
141
+
142
+ // ---------------------------------------------------------------- formatting
143
+
144
+ /** Compact count formatting for large aggregates: 950 -> "950", 214_700 -> "214.7K", 1_300_000 -> "1.3M". */
145
+ export function fmtCompact(n: number | null | undefined): string {
146
+ if (n === null || n === undefined || Number.isNaN(n)) return '—';
147
+ const abs = Math.abs(n);
148
+ if (abs >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
149
+ if (abs >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
150
+ if (abs >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
151
+ return Math.round(n).toLocaleString();
152
+ }
src/types/labeleval.ts CHANGED
@@ -4,6 +4,8 @@
4
  * Keep field names snake_case to match the JSON wire format exactly.
5
  */
6
 
 
 
7
  // ---------------------------------------------------------------- enums
8
 
9
  export type GroundTruthType =
@@ -586,4 +588,6 @@ export interface StreamEvent {
586
  pipeline: PipelineStateResponse;
587
  training: TrainingJobStatus | null;
588
  alerts_count: number;
 
 
589
  }
 
4
  * Keep field names snake_case to match the JSON wire format exactly.
5
  */
6
 
7
+ import type { MegaevalStatus } from './megaeval';
8
+
9
  // ---------------------------------------------------------------- enums
10
 
11
  export type GroundTruthType =
 
588
  pipeline: PipelineStateResponse;
589
  training: TrainingJobStatus | null;
590
  alerts_count: number;
591
+ /** Mega-scale evaluation layer status; absent on older backends and in the polling fallback. */
592
+ megaeval?: MegaevalStatus;
593
  }
src/types/megaeval.ts ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API contract for the aggregate-first mega-scale evaluation layer (megaeval).
3
+ *
4
+ * Mental model: Dataset (population) -> Evaluation Run -> Population -> Cohort
5
+ * -> Container -> Annotation (deepest, forensic drill-down only).
6
+ *
7
+ * Endpoints (all JSON):
8
+ * POST /api/megaeval/population/generate GeneratePopulationRequest -> PopulationMeta
9
+ * GET /api/megaeval/populations -> { populations: PopulationMeta[] }
10
+ * GET /api/megaeval/dimensions -> { dimensions: Record<DimName, string[]> }
11
+ * POST /api/megaeval/runs CreateRunRequest -> EvaluationRunInfo (async job!)
12
+ * GET /api/megaeval/runs -> { runs: EvaluationRunInfo[] }
13
+ * GET /api/megaeval/runs/{run_id} -> EvaluationRunInfo
14
+ * GET /api/megaeval/status -> MegaevalStatus (also inside SSE stream)
15
+ * POST /api/evaluations/query EvaluationQueryRequest -> EvaluationQueryResponse
16
+ * GET /api/megaeval/cache -> CacheStats
17
+ * GET /api/megaeval/runs/{id}/funnel -> QualityFunnelResponse
18
+ * GET /api/megaeval/runs/{id}/containers?sort=&limit=&offset= -> ContainersResponse
19
+ * GET /api/megaeval/runs/{id}/containers/{cid}/objects -> ContainerObjectsResponse
20
+ * POST /api/megaeval/errors/search ErrorSearchRequest -> ErrorSearchResponse
21
+ * POST /api/megaeval/compare CompareRequest -> CompareResponse
22
+ * GET /api/megaeval/runs/{id}/shift -> ShiftResponse
23
+ * POST /api/megaeval/why WhyRequest -> WhyResponse
24
+ * POST /api/megaeval/similarity SimilarityRequest -> SimilarityResponse
25
+ * GET /api/megaeval/runs/{id}/review -> ReviewState
26
+ * POST /api/megaeval/runs/{id}/review/plan { target_n?: number } -> ReviewState
27
+ * POST /api/megaeval/runs/{id}/review/execute -> ReviewState
28
+ * GET /api/megaeval/runs/{id}/distributions -> DistributionsResponse
29
+ *
30
+ * Notes:
31
+ * - Runs are async: POST /runs returns immediately with status "queued"; poll
32
+ * GET /runs/{id} or use the SSE stream's `megaeval.active_runs` for progress.
33
+ * - Results endpoints return HTTP 409 until the run is "published".
34
+ * - Query responses carry meta.source ("cache"|"cube"|"scan"), meta.cache_hit and
35
+ * meta.latency_ms — the UI should surface these as badges.
36
+ * - meta.exact=false means sketch-derived (approximate) numbers are included.
37
+ */
38
+
39
+ export type DimName =
40
+ | 'class' | 'weather' | 'lighting' | 'road_type' | 'scenario'
41
+ | 'sensor' | 'distance_band' | 'speed_band' | 'occlusion';
42
+
43
+ export const DIM_NAMES: DimName[] = [
44
+ 'class', 'weather', 'lighting', 'road_type', 'scenario',
45
+ 'sensor', 'distance_band', 'speed_band', 'occlusion',
46
+ ];
47
+
48
+ export type RunStatus =
49
+ | 'created' | 'queued' | 'running' | 'reducing' | 'materializing'
50
+ | 'published' | 'failed';
51
+
52
+ // ---------------------------------------------------------------- populations
53
+
54
+ export interface GeneratePopulationRequest {
55
+ name?: string;
56
+ num_objects?: number; // 1_000 .. 1_200_000, default 320_000
57
+ seed?: number;
58
+ }
59
+
60
+ export interface TrainMixRow {
61
+ class: string;
62
+ weather: string;
63
+ lighting: string;
64
+ train_share: number;
65
+ eval_share: number;
66
+ eval_count: number;
67
+ }
68
+
69
+ export interface PopulationMeta {
70
+ population_id: string;
71
+ name: string;
72
+ created_at: string;
73
+ seed: number;
74
+ num_objects: number;
75
+ num_containers: number;
76
+ num_partitions: number;
77
+ avg_objects_per_container: number;
78
+ dimensions: Record<DimName, string[]>;
79
+ dim_counts: Record<DimName, Record<string, number>>;
80
+ safety_critical_count: number;
81
+ train_mix: TrainMixRow[];
82
+ }
83
+
84
+ // ---------------------------------------------------------------- runs
85
+
86
+ export interface CreateRunRequest {
87
+ population_id: string;
88
+ model_version: string;
89
+ overrides?: Record<string, number>; // e.g. { night_penalty: 0.3 } to demo a regression
90
+ worker_delay_s?: number; // simulated distributed I/O per partition (default 0.5)
91
+ workers?: number; // default 4
92
+ seed?: number | null;
93
+ label_version?: string;
94
+ }
95
+
96
+ export interface RunProgress {
97
+ run_id: string;
98
+ population_id: string;
99
+ model_version: string;
100
+ status: RunStatus;
101
+ percent: number;
102
+ partitions_done: number;
103
+ partitions_total: number;
104
+ objects_processed: number;
105
+ objects_total: number;
106
+ workers: number;
107
+ throughput_objs_per_s: number;
108
+ eta_s: number | null;
109
+ error: string | null;
110
+ }
111
+
112
+ export interface RunLineage {
113
+ evaluation_id: string;
114
+ dataset_version: string;
115
+ model_version: string;
116
+ model_checkpoint: string;
117
+ label_version: string;
118
+ evaluator_code_version: string;
119
+ metric_version: string;
120
+ threshold_config: Record<string, number>;
121
+ sampling_config: Record<string, unknown>;
122
+ seed: number;
123
+ hardware: string;
124
+ timestamp: string;
125
+ }
126
+
127
+ export interface RunHeadline {
128
+ n: number;
129
+ tp?: number;
130
+ fp?: number;
131
+ fn?: number;
132
+ loc_err?: number;
133
+ anomalies?: number;
134
+ precision?: number | null;
135
+ recall?: number | null;
136
+ f1?: number | null;
137
+ mean_iou?: number | null;
138
+ anomaly_rate?: number | null;
139
+ safety_recall?: number | null;
140
+ conf_mean?: number | null;
141
+ error_rate?: number | null;
142
+ containers?: number;
143
+ containers_hll_estimate?: number;
144
+ }
145
+
146
+ export interface EvaluationRunInfo extends RunProgress {
147
+ created_at: string;
148
+ started_at: string | null;
149
+ published_at: string | null;
150
+ lineage: RunLineage;
151
+ headline: RunHeadline; // {} until published
152
+ per_class: Record<string, QueryRow>; // {} until published
153
+ overrides: Record<string, number>;
154
+ }
155
+
156
+ export interface CacheStats {
157
+ entries: number;
158
+ hits: number;
159
+ misses: number;
160
+ hit_rate: number | null;
161
+ }
162
+
163
+ export interface MegaevalStatus {
164
+ active_runs: RunProgress[];
165
+ cache: CacheStats | null;
166
+ total_runs: number;
167
+ }
168
+
169
+ // ---------------------------------------------------------------- query API
170
+
171
+ export type MetricName =
172
+ | 'n' | 'tp' | 'fp' | 'fn' | 'loc_err' | 'anomalies' | 'reviewed' | 'verified'
173
+ | 'safety_n' | 'safety_tp'
174
+ | 'precision' | 'recall' | 'f1' | 'mean_iou' | 'anomaly_rate'
175
+ | 'conf_mean' | 'conf_std' | 'safety_recall' | 'review_coverage'
176
+ | 'verified_rate' | 'error_rate'
177
+ | 'confidence_p50' | 'confidence_p10' | 'confidence_p90'
178
+ | 'iou_p50' | 'iou_p10' | 'iou_p90';
179
+
180
+ export interface EvaluationQueryRequest {
181
+ evaluation_id: string; // run_id
182
+ filters?: Partial<Record<DimName, string[]>>;
183
+ metrics?: MetricName[];
184
+ group_by?: DimName[];
185
+ limit?: number;
186
+ }
187
+
188
+ /** One aggregate row: group_by dims as strings + requested metrics. n always present. */
189
+ export type QueryRow = { n: number } & Record<string, string | number | null>;
190
+
191
+ export interface QueryMeta {
192
+ source: 'cache' | 'cube' | 'scan';
193
+ cache_hit: boolean;
194
+ latency_ms: number;
195
+ cells_touched: number;
196
+ exact: boolean;
197
+ approximate_fields: string[];
198
+ }
199
+
200
+ export interface EvaluationQueryResponse {
201
+ rows: QueryRow[];
202
+ meta: QueryMeta;
203
+ }
204
+
205
+ // ---------------------------------------------------------------- funnel
206
+
207
+ export interface FunnelStage {
208
+ stage: string;
209
+ count: number;
210
+ pct_of_population: number;
211
+ }
212
+
213
+ export interface QualityFunnelResponse {
214
+ stages: FunnelStage[];
215
+ estimated_precision: {
216
+ estimate: number | null;
217
+ ci_low: number | null;
218
+ ci_high: number | null;
219
+ n_reviewed: number | null;
220
+ } | null;
221
+ }
222
+
223
+ // ---------------------------------------------------------------- containers
224
+
225
+ export type ContainerSortPreset =
226
+ | 'worst_recall' | 'worst_precision' | 'worst_iou'
227
+ | 'most_anomalies' | 'least_verified' | 'highest_risk';
228
+
229
+ export interface ContainerRow {
230
+ container_id: number;
231
+ weather: string;
232
+ lighting: string;
233
+ road_type: string;
234
+ scenario: string;
235
+ n_objects: number;
236
+ tp: number;
237
+ fp: number;
238
+ fn: number;
239
+ anomalies: number;
240
+ reviewed: number;
241
+ verified: number;
242
+ recall: number | null;
243
+ precision: number | null;
244
+ mean_iou: number | null;
245
+ risk_score: number;
246
+ status: 'ok' | 'warn' | 'critical';
247
+ }
248
+
249
+ export interface ContainersResponse {
250
+ total: number;
251
+ sort: string;
252
+ rows: ContainerRow[];
253
+ }
254
+
255
+ /** Forensic object row — the deepest drill-down (old annotation-table level). */
256
+ export interface ContainerObject {
257
+ annotation_id: string;
258
+ container_id: number;
259
+ class: string;
260
+ weather: string;
261
+ lighting: string;
262
+ road_type: string;
263
+ scenario: string;
264
+ sensor: string;
265
+ distance_band: string;
266
+ speed_band: string;
267
+ occlusion: string;
268
+ safety_critical: boolean;
269
+ difficulty: number | null;
270
+ detected: boolean;
271
+ outcome: 'TP' | 'FN' | 'FP' | 'LOCALIZATION';
272
+ iou: number | null;
273
+ confidence: number | null;
274
+ anomaly: boolean;
275
+ sensor_disagree: boolean;
276
+ }
277
+
278
+ export interface ContainerObjectsResponse {
279
+ container_id: number;
280
+ objects: ContainerObject[];
281
+ }
282
+
283
+ // ---------------------------------------------------------------- error index
284
+
285
+ export type ErrorType = 'FN' | 'FP' | 'LOCALIZATION' | 'ANOMALY' | 'LOW_CONF';
286
+
287
+ export interface ErrorSearchRequest {
288
+ run_id: string;
289
+ error_types?: ErrorType[];
290
+ filters?: Partial<Record<DimName, string[]>>;
291
+ confidence_max?: number | null;
292
+ confidence_min?: number | null;
293
+ risk_min?: number | null;
294
+ severity_min?: number | null;
295
+ safety_only?: boolean;
296
+ limit_containers?: number;
297
+ }
298
+
299
+ export interface WorstContainer {
300
+ container_id: number;
301
+ error_count: number;
302
+ mean_risk: number;
303
+ max_severity: number;
304
+ safety_hits: number;
305
+ weather?: string;
306
+ lighting?: string;
307
+ road_type?: string;
308
+ scenario?: string;
309
+ n_objects?: number;
310
+ risk_score?: number;
311
+ }
312
+
313
+ export interface ErrorExample {
314
+ error_id: number;
315
+ annotation_id: string;
316
+ container_id: number;
317
+ error_type: ErrorType;
318
+ class: string;
319
+ severity: number;
320
+ confidence: number;
321
+ risk_score: number;
322
+ safety_critical: boolean;
323
+ scenario: string;
324
+ lighting: string;
325
+ weather: string;
326
+ }
327
+
328
+ export interface ErrorSearchResponse {
329
+ matched_errors: number;
330
+ by_type: Partial<Record<ErrorType, number>>;
331
+ worst_containers: WorstContainer[];
332
+ examples: ErrorExample[];
333
+ }
334
+
335
+ // ---------------------------------------------------------------- compare
336
+
337
+ export interface HeadlineDelta {
338
+ metric: string;
339
+ baseline: number;
340
+ candidate: number;
341
+ delta: number;
342
+ }
343
+
344
+ export interface PerClassDelta {
345
+ class: string;
346
+ n: number | null;
347
+ recall_baseline: number | null;
348
+ recall_candidate: number | null;
349
+ recall_delta: number;
350
+ precision_baseline: number | null;
351
+ precision_candidate: number | null;
352
+ precision_delta: number;
353
+ }
354
+
355
+ export interface CohortDelta {
356
+ cohort: string; // e.g. "highway/night/cyclist"
357
+ road_type?: string;
358
+ lighting?: string;
359
+ class?: string;
360
+ n: number;
361
+ recall_baseline: number;
362
+ recall_candidate: number;
363
+ recall_delta: number;
364
+ }
365
+
366
+ export interface CompareRequest {
367
+ candidate_run_id: string;
368
+ baseline_run_id: string;
369
+ policy?: Record<string, unknown>;
370
+ }
371
+
372
+ export interface CompareResponse {
373
+ candidate: { run_id: string; model_version: string };
374
+ baseline: { run_id: string; model_version: string };
375
+ headline_deltas: HeadlineDelta[];
376
+ per_class: PerClassDelta[];
377
+ worst_cohorts: CohortDelta[];
378
+ regressions: CohortDelta[];
379
+ policy: Record<string, unknown>;
380
+ recommendation: 'PROMOTE' | 'DO_NOT_PROMOTE';
381
+ blockers: string[];
382
+ }
383
+
384
+ // ---------------------------------------------------------------- review sampling
385
+
386
+ export interface StratumResult {
387
+ stratum: string;
388
+ N: number;
389
+ n: number;
390
+ correct: number;
391
+ p: number;
392
+ wilson_ci: [number, number];
393
+ weight: number;
394
+ }
395
+
396
+ export interface MetricEstimate {
397
+ estimate: number;
398
+ ci_low: number;
399
+ ci_high: number;
400
+ n_reviewed: number;
401
+ frame_size: number;
402
+ method: string;
403
+ strata: StratumResult[];
404
+ }
405
+
406
+ export interface SamplingFunnel {
407
+ population_objects: number;
408
+ containers: number;
409
+ suspicious_containers: number;
410
+ candidate_pool: number;
411
+ statistically_selected: number;
412
+ reviewed: number;
413
+ }
414
+
415
+ export interface StratumPlan {
416
+ label: string;
417
+ N: number;
418
+ allocated: number;
419
+ }
420
+
421
+ export interface ReviewState {
422
+ run_id: string;
423
+ planned: boolean;
424
+ executed: boolean;
425
+ method?: string;
426
+ target_n?: number;
427
+ funnel?: SamplingFunnel;
428
+ strata?: {
429
+ precision: Record<string, StratumPlan>;
430
+ recall: Record<string, StratumPlan>;
431
+ };
432
+ results?: {
433
+ precision: MetricEstimate;
434
+ recall: MetricEstimate;
435
+ } | null;
436
+ }
437
+
438
+ // ---------------------------------------------------------------- shift / why / similarity
439
+
440
+ export interface ShiftRow {
441
+ cohort: string;
442
+ class: string;
443
+ weather: string;
444
+ lighting: string;
445
+ train_share: number;
446
+ eval_share: number;
447
+ relative_change: number; // e.g. +0.90 == +90%
448
+ eval_count: number;
449
+ cohort_recall: number | null;
450
+ overall_recall: number | null;
451
+ recall_gap: number | null;
452
+ }
453
+
454
+ export interface ShiftResponse {
455
+ run_id: string;
456
+ shifts: ShiftRow[];
457
+ method: string;
458
+ thresholds: { min_eval_count: number; rel_threshold: number };
459
+ }
460
+
461
+ export interface WhyRequest {
462
+ run_id: string;
463
+ filters?: Partial<Record<DimName, string[]>>;
464
+ metric?: 'recall' | 'precision';
465
+ }
466
+
467
+ export interface WhyFactor {
468
+ factor: 'occlusion' | 'low_illumination' | 'long_range' | 'sensor_disagreement' | 'other';
469
+ count: number;
470
+ share: number;
471
+ }
472
+
473
+ export interface WhyCohort {
474
+ cohort: string;
475
+ n: number;
476
+ fn?: number | null;
477
+ fp?: number | null;
478
+ [metric: string]: string | number | null | undefined;
479
+ }
480
+
481
+ export interface WhyResponse {
482
+ metric: string;
483
+ filters: Partial<Record<DimName, string[]>>;
484
+ failure_count: number;
485
+ factors: WhyFactor[];
486
+ top_cohorts: WhyCohort[];
487
+ method: string;
488
+ }
489
+
490
+ export interface SimilarityRequest {
491
+ run_id: string;
492
+ container_id: number;
493
+ filters?: Partial<Record<DimName, string[]>>;
494
+ k?: number;
495
+ }
496
+
497
+ export interface SimilarContainer {
498
+ container_id: number;
499
+ similarity: number;
500
+ weather: string;
501
+ lighting: string;
502
+ road_type: string;
503
+ scenario: string;
504
+ n_objects: number;
505
+ fn: number;
506
+ fp: number;
507
+ anomalies: number;
508
+ risk_score: number;
509
+ }
510
+
511
+ export interface SimilarityResponse {
512
+ query_container: number;
513
+ retrieval?: string;
514
+ results: SimilarContainer[];
515
+ }
516
+
517
+ // ---------------------------------------------------------------- distributions
518
+
519
+ export interface HistogramSketch {
520
+ bins: number;
521
+ lo: number;
522
+ hi: number;
523
+ counts: number[];
524
+ percentiles: Record<string, number>;
525
+ }
526
+
527
+ export interface DistributionsResponse {
528
+ run_id: string;
529
+ exact: false;
530
+ note: string;
531
+ confidence?: HistogramSketch;
532
+ iou?: HistogramSketch;
533
+ containers_hll_estimate: number | null;
534
+ containers_exact: number | null;
535
+ available_sketch_metrics: string[];
536
+ }
tests/test_megaeval/__init__.py ADDED
File without changes
tests/test_megaeval/conftest.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared fixtures: an isolated megaeval root with a small population and two
2
+ published runs (a good candidate and a deliberately regressed one)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import pytest
7
+
8
+ from sensorflow.megaeval import population as pop_mod
9
+ from sensorflow.megaeval.runs import get_mega_store, reset_mega_store
10
+
11
+
12
+ @pytest.fixture(scope="session")
13
+ def mega_env(tmp_path_factory):
14
+ root = tmp_path_factory.mktemp("megaeval")
15
+ pop_mod.set_mega_root(str(root))
16
+ reset_mega_store()
17
+ meta = pop_mod.generate_population("test-pop", num_objects=8_000, seed=17)
18
+ store = get_mega_store()
19
+
20
+ good = store.create_run(population_id=meta["population_id"],
21
+ model_version="model-v41", worker_delay_s=0.0)
22
+ store.execute_sync(good)
23
+ assert good.status == "published", good.error
24
+
25
+ bad = store.create_run(population_id=meta["population_id"],
26
+ model_version="model-v42-regressed",
27
+ overrides={"night_penalty": 0.35, "vru_penalty": 0.12},
28
+ worker_delay_s=0.0)
29
+ store.execute_sync(bad)
30
+ assert bad.status == "published", bad.error
31
+
32
+ yield {"root": root, "meta": meta, "store": store, "good": good, "bad": bad}
33
+
34
+ # restore defaults for any later suites
35
+ pop_mod.set_mega_root("runs/megaeval")
36
+ reset_mega_store()
tests/test_megaeval/test_analysis.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model-vs-model comparison, distribution shift, why-layer, similarity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from sensorflow.megaeval import analysis
6
+
7
+
8
+ def test_compare_detects_injected_regression(mega_env):
9
+ store = mega_env["store"]
10
+ cmp = analysis.compare_runs(store, mega_env["bad"], mega_env["good"])
11
+ assert cmp["recommendation"] == "DO_NOT_PROMOTE"
12
+ assert cmp["blockers"]
13
+ hd = {h["metric"]: h["delta"] for h in cmp["headline_deltas"]}
14
+ assert hd["recall"] < 0
15
+ assert hd["safety_recall"] < 0
16
+ # injected night+VRU penalty must surface night cohorts among worst regressions
17
+ assert any("night" in c["cohort"] for c in cmp["worst_cohorts"][:8])
18
+ assert any("REGRESSION" in b for b in cmp["blockers"])
19
+
20
+
21
+ def test_compare_promotes_equivalent_model(mega_env):
22
+ store, meta = mega_env["store"], mega_env["meta"]
23
+ good = mega_env["good"]
24
+ twin = store.create_run(population_id=meta["population_id"],
25
+ model_version=good.model_version, worker_delay_s=0.0)
26
+ store.execute_sync(twin)
27
+ cmp = analysis.compare_runs(store, twin, good)
28
+ assert cmp["recommendation"] == "PROMOTE"
29
+ assert cmp["blockers"] == []
30
+
31
+
32
+ def test_compare_writes_legacy_regression_result(mega_env):
33
+ from sensorflow.evaluation.records import get_store as legacy_store
34
+ analysis.compare_runs(mega_env["store"], mega_env["bad"], mega_env["good"])
35
+ results = legacy_store().all("regressions")
36
+ ours = [r for r in results if r.model_version == mega_env["bad"].model_version]
37
+ assert ours and ours[-1].regression_detected
38
+
39
+
40
+ def test_distribution_shift_flags_underrepresented_cohorts(mega_env):
41
+ sh = analysis.distribution_shift(mega_env["store"], mega_env["good"])
42
+ assert sh["shifts"], "training mix was deliberately shifted; must be detected"
43
+ top = sh["shifts"][0]
44
+ assert abs(top["relative_change"]) >= 0.35
45
+ assert top["eval_count"] >= 300
46
+ # the train mix under-represents night/bad weather, so eval share must be higher
47
+ assert any(s["relative_change"] > 0 and s["lighting"] == "night" for s in sh["shifts"])
48
+
49
+
50
+ def test_why_decomposition_sums_to_one(mega_env):
51
+ w = analysis.why(mega_env["store"], mega_env["good"],
52
+ {"class": ["pedestrian"]}, "recall")
53
+ assert w["failure_count"] > 0
54
+ total_share = sum(f["share"] for f in w["factors"])
55
+ assert abs(total_share - 1.0) < 0.01
56
+ assert sum(f["count"] for f in w["factors"]) == w["failure_count"]
57
+ names = {f["factor"] for f in w["factors"]}
58
+ assert names <= {"occlusion", "low_illumination", "long_range",
59
+ "sensor_disagreement", "other"}
60
+ assert w["top_cohorts"], "must list affected cohorts with metric values"
61
+ assert all("recall" in c for c in w["top_cohorts"])
62
+
63
+
64
+ def test_similarity_hybrid_search(mega_env):
65
+ store, run = mega_env["store"], mega_env["good"]
66
+ containers = store.artifacts(run.run_id)["containers"]
67
+ risky = containers.sort_values("risk_score", ascending=False).iloc[0]
68
+ cid = int(risky["container_id"])
69
+ res = analysis.similar_containers(store, run, cid, k=8)
70
+ assert len(res["results"]) == 8
71
+ sims = [r["similarity"] for r in res["results"]]
72
+ assert sims == sorted(sims, reverse=True)
73
+ assert all(r["container_id"] != cid for r in res["results"])
74
+ # hybrid: structured filter restricts results
75
+ filt = analysis.similar_containers(store, run, cid, filters={"lighting": ["night"]}, k=8)
76
+ assert all(r["lighting"] == "night" for r in filt["results"])
tests/test_megaeval/test_api_lifecycle.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Async run lifecycle, the query API (cache behavior + routing), 409 gating,
2
+ containers/forensic drill-down, review endpoints and performance SLOs —
3
+ all through the real HTTP API."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import time
8
+
9
+ import pytest
10
+ from fastapi.testclient import TestClient
11
+
12
+
13
+ @pytest.fixture(scope="module")
14
+ def client(mega_env):
15
+ from app_backend import app
16
+ with TestClient(app) as c:
17
+ yield c
18
+
19
+
20
+ @pytest.fixture(scope="module")
21
+ def api_run(client, mega_env):
22
+ """Create a fresh run via the API and wait for the async lifecycle to publish."""
23
+ pop_id = mega_env["meta"]["population_id"]
24
+ res = client.post("/api/megaeval/runs",
25
+ json={"population_id": pop_id, "model_version": "model-api",
26
+ "worker_delay_s": 0.05, "workers": 4})
27
+ assert res.status_code == 200
28
+ run = res.json()
29
+ assert run["status"] in ("queued", "running")
30
+
31
+ seen_statuses = {run["status"]}
32
+ deadline = time.time() + 60
33
+ while time.time() < deadline:
34
+ r = client.get(f"/api/megaeval/runs/{run['run_id']}").json()
35
+ seen_statuses.add(r["status"])
36
+ if r["status"] in ("published", "failed"):
37
+ break
38
+ time.sleep(0.2)
39
+ assert r["status"] == "published", r.get("error")
40
+ return {"run": r, "seen_statuses": seen_statuses}
41
+
42
+
43
+ def test_async_lifecycle_and_progress(api_run):
44
+ r = api_run["run"]
45
+ assert api_run["seen_statuses"] & {"queued", "running", "reducing", "materializing"}
46
+ assert r["percent"] == 100.0
47
+ assert r["objects_processed"] == r["objects_total"] > 0
48
+ assert r["throughput_objs_per_s"] > 0
49
+ assert r["headline"]["n"] == r["objects_total"]
50
+ lin = r["lineage"]
51
+ for field in ("evaluation_id", "dataset_version", "model_version", "model_checkpoint",
52
+ "label_version", "evaluator_code_version", "metric_version",
53
+ "threshold_config", "sampling_config", "seed", "hardware", "timestamp"):
54
+ assert field in lin
55
+
56
+
57
+ def test_results_blocked_until_published(client, mega_env):
58
+ pop_id = mega_env["meta"]["population_id"]
59
+ res = client.post("/api/megaeval/runs",
60
+ json={"population_id": pop_id, "model_version": "model-slow",
61
+ "worker_delay_s": 0.4, "workers": 2})
62
+ run_id = res.json()["run_id"]
63
+ blocked = client.get(f"/api/megaeval/runs/{run_id}/funnel")
64
+ assert blocked.status_code == 409
65
+ deadline = time.time() + 90
66
+ while time.time() < deadline:
67
+ if client.get(f"/api/megaeval/runs/{run_id}").json()["status"] == "published":
68
+ break
69
+ time.sleep(0.3)
70
+ assert client.get(f"/api/megaeval/runs/{run_id}/funnel").status_code == 200
71
+
72
+
73
+ def test_query_api_cache_and_latency_slos(client, api_run):
74
+ run_id = api_run["run"]["run_id"]
75
+ body = {"evaluation_id": run_id,
76
+ "filters": {"class": ["pedestrian"], "lighting": ["night"]},
77
+ "metrics": ["n", "recall", "precision", "mean_iou"],
78
+ "group_by": []}
79
+ r1 = client.post("/api/evaluations/query", json=body).json()
80
+ assert r1["meta"]["source"] == "cube"
81
+ assert r1["meta"]["cache_hit"] is False
82
+ assert r1["meta"]["exact"] is True
83
+ assert r1["rows"][0]["n"] > 0
84
+ assert r1["meta"]["latency_ms"] < 3000 # filtered cohort query SLO
85
+
86
+ r2 = client.post("/api/evaluations/query", json=body).json()
87
+ assert r2["meta"]["cache_hit"] is True
88
+ assert r2["meta"]["source"] == "cache"
89
+ assert r2["meta"]["latency_ms"] < 100 # cached query SLO
90
+ assert r2["rows"] == r1["rows"]
91
+
92
+ dash = client.post("/api/evaluations/query",
93
+ json={"evaluation_id": run_id, "group_by": ["class"],
94
+ "metrics": ["n", "precision", "recall", "f1"]}).json()
95
+ assert dash["meta"]["latency_ms"] < 2000 # aggregate dashboard SLO
96
+ assert len(dash["rows"]) == 6
97
+
98
+ stats = client.get("/api/megaeval/cache").json()
99
+ assert stats["hits"] >= 1
100
+
101
+
102
+ def test_query_sketch_metrics_marked_approximate(client, api_run):
103
+ run_id = api_run["run"]["run_id"]
104
+ res = client.post("/api/evaluations/query",
105
+ json={"evaluation_id": run_id,
106
+ "metrics": ["n", "recall", "confidence_p50", "iou_p90"]}).json()
107
+ assert res["meta"]["exact"] is False
108
+ assert set(res["meta"]["approximate_fields"]) == {"confidence_p50", "iou_p90"}
109
+ assert 0 <= res["rows"][0]["confidence_p50"] <= 1
110
+
111
+
112
+ def test_containers_sort_presets_and_forensic_drilldown(client, api_run):
113
+ run_id = api_run["run"]["run_id"]
114
+ worst = client.get(f"/api/megaeval/runs/{run_id}/containers",
115
+ params={"sort": "worst_recall", "limit": 20}).json()
116
+ assert worst["total"] > 0 and len(worst["rows"]) == 20
117
+ recalls = [r["recall"] for r in worst["rows"] if r["recall"] is not None]
118
+ assert recalls == sorted(recalls)
119
+
120
+ risky = client.get(f"/api/megaeval/runs/{run_id}/containers",
121
+ params={"sort": "highest_risk", "limit": 5}).json()["rows"]
122
+ risks = [r["risk_score"] for r in risky]
123
+ assert risks == sorted(risks, reverse=True)
124
+ assert risky[0]["status"] in ("ok", "warn", "critical")
125
+
126
+ t0 = time.perf_counter()
127
+ objs = client.get(
128
+ f"/api/megaeval/runs/{run_id}/containers/{risky[0]['container_id']}/objects").json()
129
+ elapsed_ms = (time.perf_counter() - t0) * 1000
130
+ assert elapsed_ms < 1000 # object investigation SLO
131
+ assert objs["objects"], "forensic drill-down must return annotation-level rows"
132
+ outcomes = {o["outcome"] for o in objs["objects"]}
133
+ assert outcomes <= {"TP", "FN", "FP", "LOCALIZATION"}
134
+ gt_objs = [o for o in objs["objects"] if o["outcome"] != "FP"]
135
+ assert len(gt_objs) == risky[0]["n_objects"]
136
+
137
+
138
+ def test_review_endpoints_full_cycle(client, api_run):
139
+ run_id = api_run["run"]["run_id"]
140
+ state = client.get(f"/api/megaeval/runs/{run_id}/review").json()
141
+ assert state["planned"] is False
142
+
143
+ plan = client.post(f"/api/megaeval/runs/{run_id}/review/plan",
144
+ json={"target_n": 600}).json()
145
+ assert plan["planned"] and not plan["executed"]
146
+ assert plan["funnel"]["statistically_selected"] > 0
147
+
148
+ done = client.post(f"/api/megaeval/runs/{run_id}/review/execute").json()
149
+ assert done["executed"]
150
+ res = done["results"]
151
+ for metric in ("precision", "recall"):
152
+ est = res[metric]
153
+ assert est["ci_low"] < est["estimate"] < est["ci_high"]
154
+
155
+ funnel = client.get(f"/api/megaeval/runs/{run_id}/funnel").json()
156
+ stages = {s["stage"]: s["count"] for s in funnel["stages"]}
157
+ assert stages["Human Verified"] == done["funnel"]["reviewed"]
158
+ assert funnel["estimated_precision"]["estimate"] is not None
159
+
160
+
161
+ def test_error_search_compare_shift_why_similarity_endpoints(client, api_run, mega_env):
162
+ run_id = api_run["run"]["run_id"]
163
+ es = client.post("/api/megaeval/errors/search",
164
+ json={"run_id": run_id, "error_types": ["FN"],
165
+ "filters": {"class": ["pedestrian"]},
166
+ "risk_min": 0.5}).json()
167
+ assert es["matched_errors"] > 0 and es["worst_containers"]
168
+
169
+ cmp = client.post("/api/megaeval/compare",
170
+ json={"candidate_run_id": mega_env["bad"].run_id,
171
+ "baseline_run_id": run_id}).json()
172
+ assert cmp["recommendation"] == "DO_NOT_PROMOTE"
173
+
174
+ sh = client.get(f"/api/megaeval/runs/{run_id}/shift").json()
175
+ assert sh["shifts"]
176
+
177
+ why = client.post("/api/megaeval/why",
178
+ json={"run_id": run_id, "filters": {"lighting": ["night"]},
179
+ "metric": "recall"}).json()
180
+ assert why["failure_count"] > 0 and why["factors"]
181
+
182
+ cid = es["worst_containers"][0]["container_id"]
183
+ sim = client.post("/api/megaeval/similarity",
184
+ json={"run_id": run_id, "container_id": cid, "k": 5}).json()
185
+ assert len(sim["results"]) == 5
186
+
187
+ dist = client.get(f"/api/megaeval/runs/{run_id}/distributions").json()
188
+ assert dist["exact"] is False
189
+ assert "confidence" in dist and "50" in dist["confidence"]["percentiles"]
190
+ assert dist["containers_exact"] > 0
tests/test_megaeval/test_cube.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cube correctness: cube aggregates must EXACTLY match brute-force computation
2
+ over the raw per-object records, and re-running with the same lineage must be
3
+ bit-identical (deterministic seeds)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import pytest
12
+
13
+ from sensorflow.megaeval import cube as cube_mod
14
+ from sensorflow.megaeval import population as pop_mod
15
+ from sensorflow.megaeval.population import DIM_NAMES, DIMENSIONS
16
+ from sensorflow.megaeval.runs import run_dir
17
+
18
+
19
+ def _brute_force(env, group_dims):
20
+ """Recompute tp/fp/fn/n per group directly from raw per-object outputs."""
21
+ run = env["good"]
22
+ frames = []
23
+ d = run_dir(run.run_id)
24
+ for p in range(run.partitions_total):
25
+ pop_cols = pop_mod.load_partition(run.population_id, p)
26
+ with np.load(os.path.join(d, f"objects-part-{p:04d}.npz")) as z:
27
+ df = pd.DataFrame({
28
+ **{k: pop_cols[k] for k in DIM_NAMES},
29
+ "tp": z["tp"].astype(np.int64),
30
+ "detected": z["detected"].astype(bool),
31
+ })
32
+ df["fn"] = 1 - df["tp"]
33
+ df["fp"] = (df["detected"] & (df["tp"] == 0)).astype(np.int64)
34
+ df["n"] = 1
35
+ frames.append(df[group_dims + ["n", "tp", "fp", "fn"]])
36
+ with np.load(os.path.join(d, f"fp-part-{p:04d}.npz")) as z:
37
+ if z["container_id"].size:
38
+ fpdf = pd.DataFrame({k: z[k] for k in DIM_NAMES})
39
+ fpdf["n"] = 0
40
+ fpdf["tp"] = 0
41
+ fpdf["fp"] = 1
42
+ fpdf["fn"] = 0
43
+ frames.append(fpdf[group_dims + ["n", "tp", "fp", "fn"]])
44
+ full = pd.concat(frames, ignore_index=True)
45
+ return full.groupby(group_dims, as_index=False).sum()
46
+
47
+
48
+ @pytest.mark.parametrize("group_dims", [["class"], ["class", "lighting"], ["scenario"]])
49
+ def test_cube_matches_brute_force_exactly(mega_env, group_dims):
50
+ run = mega_env["good"]
51
+ cube = mega_env["store"].artifacts(run.run_id)["cube"]
52
+ rows, _ = cube_mod.aggregate(cube, None, group_dims,
53
+ ["n", "tp", "fp", "fn", "precision", "recall"], 2000)
54
+ brute = _brute_force(mega_env, group_dims)
55
+ assert len(rows) == len(brute)
56
+ for _, b in brute.iterrows():
57
+ key = {d: DIMENSIONS[d][int(b[d])] for d in group_dims}
58
+ match = [r for r in rows if all(r[d] == key[d] for d in group_dims)]
59
+ assert len(match) == 1, f"missing cube row for {key}"
60
+ r = match[0]
61
+ assert r["n"] == int(b["n"])
62
+ assert r["tp"] == int(b["tp"])
63
+ assert r["fp"] == int(b["fp"])
64
+ assert r["fn"] == int(b["fn"])
65
+ # derived ratios match brute force (API rounds ratios to 6 decimals)
66
+ if b["tp"] + b["fp"] > 0:
67
+ assert r["precision"] == pytest.approx(b["tp"] / (b["tp"] + b["fp"]), abs=1e-6)
68
+ if b["tp"] + b["fn"] > 0:
69
+ assert r["recall"] == pytest.approx(b["tp"] / (b["tp"] + b["fn"]), abs=1e-6)
70
+
71
+
72
+ def test_rollup_consistency(mega_env):
73
+ """Any grouped rollup must sum to the global totals (no double counting)."""
74
+ run = mega_env["good"]
75
+ cube = mega_env["store"].artifacts(run.run_id)["cube"]
76
+ total_rows, _ = cube_mod.aggregate(cube, None, None, ["n", "tp", "fp", "fn"], 1)
77
+ total = total_rows[0]
78
+ for dim in ("class", "weather", "occlusion", "distance_band"):
79
+ rows, _ = cube_mod.aggregate(cube, None, [dim], ["n", "tp", "fp", "fn"], 2000)
80
+ for stat in ("n", "tp", "fp", "fn"):
81
+ assert sum(r[stat] for r in rows) == total[stat], f"{dim}/{stat} rollup mismatch"
82
+
83
+
84
+ def test_filtered_aggregate_equals_manual_subset(mega_env):
85
+ run = mega_env["good"]
86
+ cube = mega_env["store"].artifacts(run.run_id)["cube"]
87
+ filt = {"class": ["pedestrian"], "lighting": ["night"]}
88
+ rows, _ = cube_mod.aggregate(cube, filt, None, ["n", "tp", "fn", "recall"], 1)
89
+ sub = cube[(cube["class"] == DIMENSIONS["class"].index("pedestrian"))
90
+ & (cube["lighting"] == DIMENSIONS["lighting"].index("night"))]
91
+ assert rows[0]["n"] == int(sub["n"].sum())
92
+ assert rows[0]["tp"] == int(sub["tp"].sum())
93
+ assert rows[0]["recall"] == pytest.approx(sub["tp"].sum() / max(sub["tp"].sum() + sub["fn"].sum(), 1), abs=1e-6)
94
+
95
+
96
+ def test_reproducible_run_same_lineage(mega_env):
97
+ """Same population + model_version (same derived seed) -> identical cube."""
98
+ store, meta = mega_env["store"], mega_env["meta"]
99
+ r1 = store.create_run(population_id=meta["population_id"],
100
+ model_version="model-repro", worker_delay_s=0.0)
101
+ store.execute_sync(r1)
102
+ r2 = store.create_run(population_id=meta["population_id"],
103
+ model_version="model-repro", worker_delay_s=0.0)
104
+ store.execute_sync(r2)
105
+ assert r1.seed == r2.seed
106
+ c1 = mega_env["store"].artifacts(r1.run_id)["cube"].sort_values(DIM_NAMES).reset_index(drop=True)
107
+ c2 = mega_env["store"].artifacts(r2.run_id)["cube"].sort_values(DIM_NAMES).reset_index(drop=True)
108
+ pd.testing.assert_frame_equal(c1, c2)
109
+ assert r1.headline == {**r2.headline}
tests/test_megaeval/test_error_index.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Error index: multi-criteria search correctness and worst-container ranking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ from sensorflow.megaeval import errors as errors_mod
8
+ from sensorflow.megaeval.population import DIMENSIONS
9
+ from sensorflow.megaeval.runs import ERROR_TYPES
10
+
11
+
12
+ def test_multi_criteria_search_matches_brute_filter(mega_env):
13
+ store, run = mega_env["store"], mega_env["good"]
14
+ art = store.artifacts(run.run_id)
15
+ errs, containers = art["errors"], art["containers"]
16
+
17
+ res = errors_mod.search_errors(
18
+ errs, containers, error_types=["FN"],
19
+ filters={"class": ["pedestrian"], "lighting": ["night"]},
20
+ confidence_max=0.4, risk_min=0.5)
21
+
22
+ mask = ((errs["error_type"] == ERROR_TYPES.index("FN"))
23
+ & (errs["class"] == DIMENSIONS["class"].index("pedestrian"))
24
+ & (errs["lighting"] == DIMENSIONS["lighting"].index("night"))
25
+ & (errs["confidence"] <= 0.4)
26
+ & (errs["risk_score"] >= 0.5))
27
+ assert res["matched_errors"] == int(mask.sum())
28
+ assert res["matched_errors"] > 0
29
+
30
+ for ex in res["examples"]:
31
+ assert ex["error_type"] == "FN"
32
+ assert ex["class"] == "pedestrian"
33
+ assert ex["lighting"] == "night"
34
+ assert ex["confidence"] <= 0.4
35
+ assert ex["risk_score"] >= 0.5
36
+
37
+
38
+ def test_worst_containers_ranked_and_capped(mega_env):
39
+ store, run = mega_env["store"], mega_env["good"]
40
+ art = store.artifacts(run.run_id)
41
+ res = errors_mod.search_errors(art["errors"], art["containers"],
42
+ error_types=["FN", "FP"], limit_containers=10)
43
+ worst = res["worst_containers"]
44
+ assert 0 < len(worst) <= 10
45
+ scores = [w["error_count"] * w["mean_risk"] for w in worst]
46
+ assert scores == sorted(scores, reverse=True)
47
+ # returns aggregated containers with context dims, not raw record dumps
48
+ assert "weather" in worst[0] and "n_objects" in worst[0]
49
+
50
+
51
+ def test_safety_only_filter(mega_env):
52
+ store, run = mega_env["store"], mega_env["good"]
53
+ art = store.artifacts(run.run_id)
54
+ res = errors_mod.search_errors(art["errors"], art["containers"],
55
+ error_types=["FN"], safety_only=True)
56
+ assert res["matched_errors"] > 0
57
+ assert all(ex["safety_critical"] for ex in res["examples"])
58
+
59
+
60
+ def test_by_type_totals_are_consistent(mega_env):
61
+ store, run = mega_env["store"], mega_env["good"]
62
+ art = store.artifacts(run.run_id)
63
+ res = errors_mod.search_errors(art["errors"], art["containers"])
64
+ assert sum(res["by_type"].values()) == res["matched_errors"] == len(art["errors"])
tests/test_megaeval/test_sampling.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Review sampling: funnel structure, CI correctness (must cover the true
2
+ population metric), and cube review-counter updates."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import pytest
7
+
8
+ from sensorflow.megaeval import cube as cube_mod
9
+ from sensorflow.megaeval import sampling
10
+
11
+
12
+ @pytest.fixture(scope="module")
13
+ def reviewed(mega_env):
14
+ store, run = mega_env["store"], mega_env["good"]
15
+ plan = sampling.build_review_plan(store, run, target_n=1200)
16
+ result = sampling.execute_reviews(store, run)
17
+ return {"plan": plan, "result": result, "run": run, "store": store}
18
+
19
+
20
+ def test_sampling_funnel_shape(reviewed):
21
+ f = reviewed["result"]["funnel"]
22
+ assert f["population_objects"] > f["containers"] > 0
23
+ assert f["suspicious_containers"] <= f["containers"]
24
+ # per-stratum rounding can overshoot by at most one unit per stratum (10 strata)
25
+ assert f["statistically_selected"] <= reviewed["result"]["target_n"] + 10
26
+ assert f["reviewed"] == f["statistically_selected"]
27
+
28
+
29
+ def test_stratified_ci_covers_true_metrics(reviewed):
30
+ run = reviewed["run"]
31
+ res = reviewed["result"]["results"]
32
+ slack = 0.02 # human-fidelity noise (98.5%) can bias estimates slightly
33
+ for metric in ("precision", "recall"):
34
+ true = run.headline[metric]
35
+ est = res[metric]
36
+ assert est["ci_low"] < est["estimate"] < est["ci_high"]
37
+ assert est["ci_low"] - slack <= true <= est["ci_high"] + slack, (
38
+ f"{metric}: true {true} outside CI [{est['ci_low']}, {est['ci_high']}]")
39
+ assert abs(est["estimate"] - true) < 0.06
40
+ assert (est["ci_high"] - est["ci_low"]) < 0.25
41
+ assert est["n_reviewed"] > 0
42
+ assert "stratified" in est["method"]
43
+
44
+
45
+ def test_strata_are_risk_weighted(reviewed):
46
+ """Missed/safety strata must be oversampled relative to their share."""
47
+ rec = reviewed["result"]["results"]["recall"]
48
+ by_label = {s["stratum"]: s for s in rec["strata"]}
49
+ detected = by_label.get("detected")
50
+ missed = by_label.get("missed")
51
+ assert detected and missed
52
+ assert (missed["n"] / max(missed["N"], 1)) > (detected["n"] / max(detected["N"], 1))
53
+
54
+
55
+ def test_cube_review_counters_updated(reviewed):
56
+ store, run = reviewed["store"], reviewed["run"]
57
+ cube = store.artifacts(run.run_id)["cube"]
58
+ rows, _ = cube_mod.aggregate(cube, None, None, ["n", "reviewed", "verified"], 1)
59
+ total = rows[0]
60
+ assert total["reviewed"] > 0
61
+ assert 0 < total["verified"] <= total["reviewed"]
62
+ # containers table too
63
+ containers = store.artifacts(run.run_id)["containers"]
64
+ assert int(containers["reviewed"].sum()) == total["reviewed"]
tests/test_megaeval/test_sketches.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sketch accuracy: quantile histograms, HyperLogLog, reservoir, Wilson CIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pytest
7
+
8
+ from sensorflow.megaeval.sketches import HyperLogLog, QuantileHistogram, Reservoir
9
+ from sensorflow.megaeval.sampling import wilson_interval
10
+
11
+
12
+ def test_quantile_histogram_percentiles_close_to_exact():
13
+ rng = np.random.default_rng(3)
14
+ data = np.clip(rng.beta(5, 2, size=50_000), 0, 1)
15
+ h = QuantileHistogram(0, 1, 64)
16
+ h.add(data)
17
+ bin_width = 1 / 64
18
+ for q in (10, 25, 50, 75, 90, 99):
19
+ approx = h.percentile(q)
20
+ exact = float(np.percentile(data, q))
21
+ assert abs(approx - exact) <= bin_width + 1e-9, f"p{q}: {approx} vs {exact}"
22
+
23
+
24
+ def test_quantile_histogram_merge_equals_combined():
25
+ rng = np.random.default_rng(4)
26
+ a, b = rng.random(10_000), rng.random(10_000)
27
+ ha, hb, hc = QuantileHistogram(), QuantileHistogram(), QuantileHistogram()
28
+ ha.add(a)
29
+ hb.add(b)
30
+ hc.add(np.concatenate([a, b]))
31
+ merged = ha.merge(hb)
32
+ assert np.array_equal(merged.counts, hc.counts)
33
+
34
+
35
+ def test_hll_estimate_within_five_percent():
36
+ hll = HyperLogLog(12)
37
+ n = 20_000
38
+ hll.add_ids(np.arange(n, dtype=np.int64))
39
+ est = hll.estimate()
40
+ assert abs(est - n) / n < 0.05, est
41
+
42
+
43
+ def test_hll_merge_is_union():
44
+ h1, h2 = HyperLogLog(12), HyperLogLog(12)
45
+ h1.add_ids(np.arange(0, 8_000, dtype=np.int64))
46
+ h2.add_ids(np.arange(4_000, 12_000, dtype=np.int64))
47
+ merged = h1.merge(h2)
48
+ est = merged.estimate()
49
+ assert abs(est - 12_000) / 12_000 < 0.06, est
50
+
51
+
52
+ def test_reservoir_bounded_and_uniformish():
53
+ r = Reservoir(k=100, seed=1)
54
+ r.add_ids(np.arange(10_000))
55
+ assert len(r.items) == 100
56
+ assert r.seen == 10_000
57
+ # sampled ids should span the range, not just the head
58
+ assert max(r.items) > 5_000 and min(r.items) < 5_000
59
+
60
+
61
+ def test_wilson_interval_known_value():
62
+ lo, hi = wilson_interval(8, 10)
63
+ assert lo == pytest.approx(0.490, abs=0.01)
64
+ assert hi == pytest.approx(0.943, abs=0.01)
65
+ lo0, hi0 = wilson_interval(0, 0)
66
+ assert (lo0, hi0) == (0.0, 1.0)
67
+ # Wilson upper bound at p=1 is mathematically < 1 (that's the point of Wilson)
68
+ lo1, hi1 = wilson_interval(50, 50)
69
+ assert 0.9 < lo1 < hi1 <= 1.0
70
+ assert hi1 == pytest.approx(0.9928, abs=0.01)