"""Paired evaluation harness with a fingerprint-keyed prediction cache. Core design: the SAME sampled units are run through both models. The paired difference d_i = c_i - b_i removes the between-unit difficulty variance that dominates two-independent-sample comparisons (Var(d) ~ discordance rate, an order of magnitude below p(1-p) for adjacent model versions), which is where most of the sample-size savings in this system come from. Models are simulated through the megaeval synthetic machinery's population (dimension codes + difficulty + container structure) with configurable per-stratum performance effects, so a "2pp pedestrian-night regression" candidate is constructible deterministically: SimulatedModel("cand-v2", effects={"pedestrian|night": -0.02, "__global__": 0.004}) Determinism/pairing: * a per-OBJECT pairing latent u_i drawn once per dataset (seeded by the population seed) is shared by every model -> outcomes are paired; * each model adds a small independent "flip" noise stream seeded by its own fingerprint -> realistic discordance even at Delta = 0; * a container-level scene effect (shared by both models) induces honest intra-cluster correlation for the clustering machinery to handle. Prediction cache: full-population success vectors keyed by (dataset fingerprint, model fingerprint) and persisted under runs/seqeval/cache/. Baseline predictions are computed once and reused across candidate updates; COMPUTE_COUNTS lets tests assert no recompute happened. Invalidation is by construction: a new dataset or new checkpoint changes the fingerprint and therefore the key; stale entries never collide. """ from __future__ import annotations import hashlib import json import os import threading from typing import Dict, Optional, Tuple import numpy as np from sensorflow.megaeval import population as pop_mod from sensorflow.seqeval import ledger as ledger_mod # Simulation profile (baseline physics of the synthetic detector). BASE_DETECT = 0.945 DIFFICULTY_WEIGHT = 0.16 NIGHT_PENALTY = 0.030 VRU_PENALTY = 0.020 SCENE_EFFECT_SD = 0.030 FLIP_NOISE = 0.04 # per-model chance of drawing a fresh latent VRU_CLASSES = (1, 2, 3) # pedestrian, cyclist, motorcycle GLOBAL_EFFECT_KEY = "__global__" # fingerprint -> number of actual (non-cache) prediction computations COMPUTE_COUNTS: Dict[str, int] = {} _COUNT_LOCK = threading.Lock() class SimulatedModel: """A model version plus per-stratum performance effects. effects keys: "class|condition" (e.g. "pedestrian|night") in absolute detection-probability points, or "__global__" applied everywhere. """ def __init__(self, model_version: str, effects: Optional[Dict[str, float]] = None): self.model_version = model_version self.effects = dict(effects or {}) def fingerprint(self) -> str: blob = json.dumps({"v": self.model_version, "e": {k: round(float(v), 6) for k, v in sorted(self.effects.items())}}, sort_keys=True) return hashlib.sha256(blob.encode()).hexdigest()[:16] def to_dict(self) -> Dict: return {"model_version": self.model_version, "effects": self.effects, "fingerprint": self.fingerprint()} def dataset_fingerprint(meta: Dict) -> str: blob = json.dumps({"pop": meta["population_id"], "seed": meta["seed"], "n": meta["num_objects"]}, sort_keys=True) return hashlib.sha256(blob.encode()).hexdigest()[:16] # ------------------------------------------------------------------ frame _FRAME_CACHE: Dict[str, Dict[str, np.ndarray]] = {} _FRAME_LOCK = threading.Lock() def load_frame(meta: Dict) -> Dict[str, np.ndarray]: """Full-population sampling frame (dimension codes only), sorted by object_id so that object_id == row index. Cached in memory per population.""" pid = meta["population_id"] with _FRAME_LOCK: if pid in _FRAME_CACHE: return _FRAME_CACHE[pid] cols_needed = ["object_id", "container_id", "class", "lighting", "scenario", "weather", "difficulty", "safety_critical"] parts = {k: [] for k in cols_needed} for p in range(meta["num_partitions"]): part = pop_mod.load_partition(pid, p) for k in cols_needed: parts[k].append(part[k]) frame = {k: np.concatenate(v) for k, v in parts.items()} order = np.argsort(frame["object_id"], kind="stable") frame = {k: v[order] for k, v in frame.items()} with _FRAME_LOCK: if len(_FRAME_CACHE) > 4: _FRAME_CACHE.clear() _FRAME_CACHE[pid] = frame return frame # ------------------------------------------------------------------ latents def _pair_latents(meta: Dict) -> np.ndarray: """Per-object pairing latent, shared by all models on this dataset.""" rng = np.random.default_rng([int(meta["seed"]), 0x5E9, 424242]) return rng.random(int(meta["num_objects"])) def _scene_effects(meta: Dict) -> np.ndarray: """Container-level 'scene hardness' shared by all models (drives ICC).""" rng = np.random.default_rng([int(meta["seed"]), 777]) return rng.normal(0.0, SCENE_EFFECT_SD, size=int(meta["num_containers"])) _LATENT_CACHE: Dict[str, Tuple[np.ndarray, np.ndarray]] = {} def _latents(meta: Dict) -> Tuple[np.ndarray, np.ndarray]: pid = meta["population_id"] if pid not in _LATENT_CACHE: if len(_LATENT_CACHE) > 4: _LATENT_CACHE.clear() _LATENT_CACHE[pid] = (_pair_latents(meta), _scene_effects(meta)) return _LATENT_CACHE[pid] # ------------------------------------------------------------------ prediction def _detect_probability(frame: Dict[str, np.ndarray], meta: Dict, model: SimulatedModel, condition_dim: str) -> np.ndarray: difficulty = frame["difficulty"].astype(np.float64) p = np.full(difficulty.shape, BASE_DETECT) p -= DIFFICULTY_WEIGHT * difficulty p -= NIGHT_PENALTY * (frame["lighting"] == 1) p -= VRU_PENALTY * np.isin(frame["class"], VRU_CLASSES) _, scene = _latents(meta) p += scene[frame["container_id"]] p += float(model.effects.get(GLOBAL_EFFECT_KEY, 0.0)) cond_vocab = pop_mod.DIMENSIONS[condition_dim] cls_vocab = pop_mod.DIMENSIONS["class"] for key, eff in model.effects.items(): if key == GLOBAL_EFFECT_KEY or "|" not in key: continue cls_name, cond_name = key.split("|", 1) if cls_name not in cls_vocab or cond_name not in cond_vocab: continue mask = ((frame["class"] == cls_vocab.index(cls_name)) & (frame[condition_dim] == cond_vocab.index(cond_name))) p[mask] += float(eff) return np.clip(p, 0.01, 0.995) def _compute_predictions(meta: Dict, model: SimulatedModel, condition_dim: str) -> np.ndarray: frame = load_frame(meta) p = _detect_probability(frame, meta, model, condition_dim) u_pair, _ = _latents(meta) fp_int = int(model.fingerprint(), 16) rng = np.random.default_rng([fp_int % (2**32), int(meta["seed"]), 7]) n = u_pair.size swap = rng.random(n) < FLIP_NOISE fresh = rng.random(n) u = np.where(swap, fresh, u_pair) return (u < p).astype(np.uint8) class PredictionCache: """Disk + memory cache of full-population success vectors. Key = (dataset fingerprint, model fingerprint). Both fingerprints cover everything the predictions depend on, so invalidation is automatic: any change to the dataset (new population/seed/size) or the model (version or effects) produces a new key. """ def __init__(self): self._mem: Dict[str, np.ndarray] = {} self._lock = threading.Lock() def _path(self, key: str) -> str: return os.path.join(ledger_mod.seq_root(), "cache", f"{key}.npz") def get_or_compute(self, meta: Dict, model: SimulatedModel, condition_dim: str = "lighting") -> np.ndarray: key = f"{dataset_fingerprint(meta)}-{model.fingerprint()}" with self._lock: if key in self._mem: return self._mem[key] path = self._path(key) if os.path.exists(path): with np.load(path) as z: success = z["success"] else: success = _compute_predictions(meta, model, condition_dim) with _COUNT_LOCK: COMPUTE_COUNTS[key] = COMPUTE_COUNTS.get(key, 0) + 1 os.makedirs(os.path.dirname(path), exist_ok=True) np.savez_compressed(path, success=success, model_version=np.array([model.model_version])) with self._lock: if len(self._mem) > 16: self._mem.clear() self._mem[key] = success return success def clear_memory(self) -> None: with self._lock: self._mem.clear() _CACHE: Optional[PredictionCache] = None _CACHE_LOCK = threading.Lock() def get_prediction_cache() -> PredictionCache: global _CACHE with _CACHE_LOCK: if _CACHE is None: _CACHE = PredictionCache() return _CACHE def reset_prediction_cache() -> None: global _CACHE with _CACHE_LOCK: _CACHE = None with _FRAME_LOCK: _FRAME_CACHE.clear() _LATENT_CACHE.clear() with _COUNT_LOCK: COMPUTE_COUNTS.clear() def paired_outcomes(meta: Dict, baseline: SimulatedModel, candidate: SimulatedModel, object_ids: np.ndarray, condition_dim: str = "lighting" ) -> Dict[str, np.ndarray]: """Per-unit paired outcomes for the given objects (baseline served from cache; the same sampled units go through both models).""" cache = get_prediction_cache() b_full = cache.get_or_compute(meta, baseline, condition_dim) c_full = cache.get_or_compute(meta, candidate, condition_dim) idx = np.asarray(object_ids, dtype=np.int64) b = b_full[idx].astype(np.int8) c = c_full[idx].astype(np.int8) return {"baseline": b, "candidate": c, "d": (c - b).astype(np.float64)}