src/mcss/data/hypersim_expansion.py
"""Deterministic Hypersim expansion planning with a sealed final holdout."""
from __future__ import annotations
import csv
import hashlib
import re
import shutil
from collections import Counter, defaultdict
from dataclasses import asdict, dataclass
from pathlib import Path
OFFICIAL_SPLITS = ("train", "val", "test")
PROTOCOL_PARTITIONS = ("train", "val", "diagnostic_test", "final_holdout")
_SCENE_NAME = re.compile(r"^ai_\d{3}_\d{3}$")
_CAMERA_NAME = re.compile(r"^cam_\d{2}$")
@dataclass(frozen=True)
class ExpansionScene:
scene_name: str
official_split: str
protocol_partition: str
selected_camera: str
frame_count: int
previously_observed: bool
@dataclass(frozen=True)
class ExpansionExclusion:
scene_name: str
selected_camera: str
frame_count: int
reason: str = "previously_evaluated_test_scene"
@dataclass(frozen=True)
class ExpansionPlan:
scenes: tuple[ExpansionScene, ...]
exclusions: tuple[ExpansionExclusion, ...]
metadata_sha256: str
@property
def counts(self) -> dict[str, int]:
counts = Counter(scene.protocol_partition for scene in self.scenes)
return {partition: counts[partition] for partition in PROTOCOL_PARTITIONS}
@property
def official_counts(self) -> dict[str, int]:
counts = Counter(scene.official_split for scene in self.scenes)
return {split: counts[split] for split in OFFICIAL_SPLITS}
@property
def final_holdout_scenes(self) -> frozenset[str]:
return frozenset(
scene.scene_name
for scene in self.scenes
if scene.protocol_partition == "final_holdout"
)
@dataclass(frozen=True)
class StorageEstimate:
current_free_bytes: int
required_free_bytes: int
local_raw_scenes: int
missing_raw_scenes: int
raw_bytes_per_frame: float
prepared_bytes_per_frame: float
estimated_incremental_raw_bytes: int
estimated_incremental_prepared_bytes: int
safety_factor: float
@property
def estimated_incremental_total_bytes(self) -> int:
return self.estimated_incremental_raw_bytes + self.estimated_incremental_prepared_bytes
@property
def estimated_free_after_bytes(self) -> int:
return self.current_free_bytes - self.estimated_incremental_total_bytes
@property
def fits_budget(self) -> bool:
return self.estimated_free_after_bytes >= self.required_free_bytes
def as_dict(self) -> dict[str, int | float | bool]:
result: dict[str, int | float | bool] = asdict(self)
result["estimated_incremental_total_bytes"] = self.estimated_incremental_total_bytes
result["estimated_free_after_bytes"] = self.estimated_free_after_bytes
result["fits_budget"] = self.fits_budget
return result
def build_expansion_plan(
metadata_path: str | Path,
observed_subset_path: str | Path,
) -> ExpansionPlan:
metadata = Path(metadata_path)
official_scenes = _load_official_scenes(metadata)
observed_splits = _load_observed_subset(Path(observed_subset_path))
unknown = sorted(set(observed_splits) - set(official_scenes))
if unknown:
raise ValueError(
f"observed subset contains scenes absent from official metadata: {unknown}"
)
for scene_name, observed_split in observed_splits.items():
official_split = official_scenes[scene_name][0]
if observed_split != official_split:
raise ValueError(
f"split mismatch for {scene_name}: observed={observed_split}, "
f"official={official_split}"
)
scenes: list[ExpansionScene] = []
exclusions: list[ExpansionExclusion] = []
for scene_name, (official_split, camera, frame_count) in sorted(official_scenes.items()):
previously_observed = scene_name in observed_splits
if official_split == "test":
protocol_partition = (
"diagnostic_test" if previously_observed else "final_holdout"
)
else:
protocol_partition = official_split
scene = ExpansionScene(
scene_name=scene_name,
official_split=official_split,
protocol_partition=protocol_partition,
selected_camera=camera,
frame_count=frame_count,
previously_observed=previously_observed,
)
scenes.append(scene)
if protocol_partition == "diagnostic_test":
exclusions.append(
ExpansionExclusion(
scene_name=scene_name,
selected_camera=camera,
frame_count=frame_count,
)
)
final_holdout = {
scene.scene_name for scene in scenes if scene.protocol_partition == "final_holdout"
}
excluded = {row.scene_name for row in exclusions}
if final_holdout & excluded:
raise RuntimeError("final holdout intersects previously evaluated test scenes")
return ExpansionPlan(tuple(scenes), tuple(exclusions), _sha256(metadata))
def write_expansion_plan(
plan: ExpansionPlan,
partitions_path: str | Path,
exclusions_path: str | Path,
) -> None:
_write_dataclass_csv(Path(partitions_path), plan.scenes)
_write_dataclass_csv(Path(exclusions_path), plan.exclusions)
def select_materialization_scenes(
plan: ExpansionPlan,
partitions: tuple[str, ...] | list[str],
*,
allow_final_holdout: bool = False,
) -> tuple[ExpansionScene, ...]:
requested = tuple(dict.fromkeys(partitions))
if not requested:
raise ValueError("at least one protocol partition is required")
invalid = sorted(set(requested) - set(PROTOCOL_PARTITIONS))
if invalid:
raise ValueError(f"unsupported protocol partitions: {invalid}")
if "final_holdout" in requested and not allow_final_holdout:
raise ValueError("final_holdout is sealed until explicitly released")
selected = tuple(scene for scene in plan.scenes if scene.protocol_partition in requested)
if not selected:
raise ValueError(f"no scenes found for protocol partitions: {requested}")
return selected
def estimate_expansion_storage(
plan: ExpansionPlan,
*,
raw_root: str | Path,
baseline_prepared_root: str | Path,
target_prepared_root: str | Path,
reserve_bytes: int,
safety_factor: float = 1.25,
) -> StorageEstimate:
if reserve_bytes < 0:
raise ValueError("reserve_bytes must be non-negative")
if safety_factor < 1.0:
raise ValueError("safety_factor must be at least one")
raw = Path(raw_root)
baseline_prepared = Path(baseline_prepared_root)
target_prepared = Path(target_prepared_root)
by_name = {scene.scene_name: scene for scene in plan.scenes}
local_raw = [scene for scene in plan.scenes if (raw / scene.scene_name).is_dir()]
raw_sample_bytes = sum(_directory_size(raw / scene.scene_name) for scene in local_raw)
raw_sample_frames = sum(scene.frame_count for scene in local_raw)
raw_bytes_per_frame = _rate(raw_sample_bytes, raw_sample_frames, "raw Hypersim")
missing_raw = [scene for scene in plan.scenes if not (raw / scene.scene_name).is_dir()]
prepared_samples: list[tuple[Path, ExpansionScene]] = []
for split in ("train", "val", "test"):
split_root = baseline_prepared / split
if not split_root.is_dir():
continue
for scene_path in split_root.iterdir():
if scene_path.is_dir() and scene_path.name in by_name:
prepared_samples.append((scene_path, by_name[scene_path.name]))
prepared_sample_bytes = sum(_directory_size(path) for path, _ in prepared_samples)
prepared_sample_frames = sum(scene.frame_count for _, scene in prepared_samples)
prepared_bytes_per_frame = _rate(
prepared_sample_bytes, prepared_sample_frames, "prepared Hypersim"
)
missing_prepared = [
scene
for scene in plan.scenes
if not (target_prepared / scene.protocol_partition / scene.scene_name).is_dir()
]
incremental_raw = int(
sum(scene.frame_count for scene in missing_raw) * raw_bytes_per_frame * safety_factor
)
incremental_prepared = int(
sum(scene.frame_count for scene in missing_prepared)
* prepared_bytes_per_frame
* safety_factor
)
current_free = shutil.disk_usage(raw.resolve().anchor).free
return StorageEstimate(
current_free_bytes=current_free,
required_free_bytes=reserve_bytes,
local_raw_scenes=len(local_raw),
missing_raw_scenes=len(missing_raw),
raw_bytes_per_frame=raw_bytes_per_frame,
prepared_bytes_per_frame=prepared_bytes_per_frame,
estimated_incremental_raw_bytes=incremental_raw,
estimated_incremental_prepared_bytes=incremental_prepared,
safety_factor=safety_factor,
)
def _load_official_scenes(path: Path) -> dict[str, tuple[str, str, int]]:
required = {
"scene_name",
"camera_name",
"frame_id",
"included_in_public_release",
"split_partition_name",
}
scene_splits: dict[str, set[str]] = defaultdict(set)
camera_frames: dict[str, dict[str, set[int]]] = defaultdict(lambda: defaultdict(set))
with path.open(newline="", encoding="utf-8-sig") as handle:
reader = csv.DictReader(handle)
_require_columns(path, reader.fieldnames, required)
for row in reader:
if row["included_in_public_release"].strip().lower() != "true":
continue
scene = row["scene_name"].strip()
camera = row["camera_name"].strip()
split = row["split_partition_name"].strip()
if not _SCENE_NAME.fullmatch(scene):
raise ValueError(f"invalid official scene name: {scene!r}")
if not _CAMERA_NAME.fullmatch(camera):
raise ValueError(f"invalid official camera name: {camera!r}")
if split not in OFFICIAL_SPLITS:
raise ValueError(f"unsupported official split for {scene}: {split!r}")
try:
frame_id = int(row["frame_id"])
except ValueError as error:
raise ValueError(f"invalid frame id for {scene}/{camera}") from error
if frame_id < 0:
raise ValueError(f"negative frame id for {scene}/{camera}")
scene_splits[scene].add(split)
camera_frames[scene][camera].add(frame_id)
scenes: dict[str, tuple[str, str, int]] = {}
for scene in sorted(scene_splits):
splits = scene_splits[scene]
if len(splits) != 1:
raise ValueError(f"official scene crosses splits: {scene} -> {sorted(splits)}")
cameras = camera_frames[scene]
selected_camera = "cam_00" if "cam_00" in cameras else min(
cameras,
key=lambda camera: (-len(cameras[camera]), camera),
)
scenes[scene] = (next(iter(splits)), selected_camera, len(cameras[selected_camera]))
if not scenes:
raise ValueError("official metadata contains no public scenes")
return scenes
def _load_observed_subset(path: Path) -> dict[str, str]:
required = {"scene_name", "split_partition_name"}
scenes: dict[str, str] = {}
with path.open(newline="", encoding="utf-8-sig") as handle:
reader = csv.DictReader(handle)
_require_columns(path, reader.fieldnames, required)
for row in reader:
scene = row["scene_name"].strip()
split = row["split_partition_name"].strip()
if scene in scenes:
raise ValueError(f"duplicate observed scene: {scene}")
if split not in OFFICIAL_SPLITS:
raise ValueError(f"unsupported observed split for {scene}: {split!r}")
scenes[scene] = split
return scenes
def _require_columns(path: Path, fieldnames: list[str] | None, required: set[str]) -> None:
missing = required - set(fieldnames or ())
if missing:
raise ValueError(f"{path} is missing columns: {sorted(missing)}")
def _write_dataclass_csv(path: Path, rows: tuple[object, ...]) -> None:
if not rows:
raise ValueError(f"cannot write an empty CSV: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".part")
first = asdict(rows[0]) # type: ignore[arg-type]
with temporary.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=list(first))
writer.writeheader()
for row in rows:
writer.writerow(asdict(row)) # type: ignore[arg-type]
temporary.replace(path)
def _directory_size(path: Path) -> int:
return sum(file.stat().st_size for file in path.rglob("*") if file.is_file())
def _rate(byte_count: int, frame_count: int, label: str) -> float:
if byte_count <= 0 or frame_count <= 0:
raise ValueError(f"cannot estimate {label} storage without a non-empty local sample")
return byte_count / frame_count
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
while chunk := handle.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()