Source: scripts/evaluate_rgbd_volume_disjoint.py · Plain text

scripts/evaluate_rgbd_volume_disjoint.py

#!/usr/bin/env python3
"""V16: does the carrier's completion advantage survive on volume-disjoint scenes? (no training)

The frozen TRAIN72 carriers (V11 C1 primary, V12 C1 secondary) and the TRAIN72+EXT carriers of V14
and V15 (descriptive) render FRESH-V2, whose 10 volumes hold no TRAIN72 or DEV scene, with V13's
code path. The completion advantage A = FAR AbsRel(REPROJ_HARMONIC) - FAR AbsRel(V11 C1) on FRESH-V2
is compared with V13's sealed and scored EVAL-V3/V4 rows (volumes shared with TRAIN72), as fixed by
PROTOCOL.md. Stages: `lock`; `seal` (replay of two sealed EVAL-V3 scenes for every carrier family,
then every FRESH-V2 prediction hashed before any query depth is read); `score`; `analyze`.
"""
# ruff: noqa: E501 -- Chinese scientific report prose

from __future__ import annotations

import argparse
import datetime
import functools
import hashlib
import importlib.util
import json
from pathlib import Path

import numpy as np
import torch

from mcss.dynamic.types import hash_scene_state
from mcss.geometry import transform_cameras
from mcss.measurements import FixedMeasurementRenderer
from mcss.mechanism_pilot import rgbd_completion_carrier as v11_carrier
from mcss.mechanism_pilot import rgbd_data_scale_carrier as v14_carrier
from mcss.mechanism_pilot import rgbd_wide_scale_carrier as v15_carrier
from mcss.mechanism_pilot import rgbd_width_carrier as v12_carrier
from mcss.mechanism_pilot.readout_reanalysis import depth_metrics
from mcss.mechanism_pilot.rgbd_depth_bounds import context_depth_bounds
from mcss.types import Cameras

EXPERIMENT = "EXP-3D-RGBD-VOLUME-DISJOINT-V16"
FRESH = Path("outputs/EXP-3D-RGBD-FRESH-V2-DATA/manifest_fresh_v2.json")
EVAL_V3 = Path("outputs/EXP-3D-RGBD-EVAL-V3-DATA/manifest_eval_v3.json")
V11 = Path("outputs/EXP-3D-RGBD-COMPLETION-V11")
V12 = Path("outputs/EXP-3D-RGBD-WIDTH-V12")
V13 = Path("outputs/EXP-3D-RGBD-VOLUME-V13")
V14 = Path("outputs/EXP-3D-RGBD-DATA-SCALE-V14")
V15 = Path("outputs/EXP-3D-RGBD-WIDTH-DATA-V15")
INPAINT = Path("outputs/EXP-3D-RGBD-INPAINT-BASELINES-V1")
# family -> (carrier module, selected checkpoints, sealed EVAL-V3 replay: seal file, key prefix, array prefix)
FAMILIES = {
    "V11C1": (
        v11_carrier,
        V11 / "selected_checkpoints.json",
        (V11 / "raw/eval_v3/prediction_seal.json", "", "C1"),
    ),
    "V12C1": (
        v12_carrier,
        V12 / "selected_checkpoints.json",
        (V15 / "raw/eval/prediction_seal.json", "EVAL_V3/", "W64T72"),
    ),
    "V14C1": (
        v14_carrier,
        V14 / "selected_checkpoints.json",
        (V14 / "raw/eval/prediction_seal.json", "EVAL_V3/", "C1"),
    ),
    "V15C1": (
        v15_carrier,
        V15 / "selected_checkpoints.json",
        (V15 / "raw/eval/prediction_seal.json", "EVAL_V3/", "C1"),
    ),
}
TRAIN72_FAMILIES = ("V11C1", "V12C1")
THRESHOLD = 0.5
REGIONS = ("ALL", "NEAR", "FAR")
REPLAY_SCENES = 2
DRAWS, SEED = 10000, 20260928
SCRIPTS = Path(__file__).resolve().parent


def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def write(path, value):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2, sort_keys=True, allow_nan=False) + "\n")


def read(path):
    return json.loads(Path(path).read_text())


def module(name, filename):
    spec = importlib.util.spec_from_file_location(name, SCRIPTS / filename)
    loaded = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(loaded)
    return loaded


@functools.cache
def host():
    """V11's frozen EVAL-V3 module: context loader, aggregation, bootstrap and V2 gate."""
    return module("v16_v11_eval", "evaluate_rgbd_completion_eval_v3.py")


@functools.cache
def inpainting():
    """INPAINT-V1's frozen module: ray cosine, harmonic filling, NEAR mask, label rule."""
    return module("v16_inpaint", "run_rgbd_inpaint_baselines.py")


def inputs(root):
    paths = {
        "protocol": root / "PROTOCOL.md",
        "script": Path(__file__).resolve(),
        "v11_eval_script": SCRIPTS / "evaluate_rgbd_completion_eval_v3.py",
        "inpaint_script": SCRIPTS / "run_rgbd_inpaint_baselines.py",
        "geometric_script": SCRIPTS / "run_rgbd_geometric_baselines.py",
        "fresh_v2_manifest": FRESH,
        "v11_contract": V11 / "training_contract.json",
        "v13_query_rows": V13 / "raw/query_rows.json",
        "v14_contract": V14 / "training_contract.json",
    }
    for family, (_, selected, (seal, _, _)) in FAMILIES.items():
        paths[f"{family}_selected"] = selected
        paths[f"{family}_replay_seal"] = seal
    return paths


def lock(root):
    root = Path(root).resolve()
    if root.name != EXPERIMENT:
        raise PermissionError("Exact experiment directory required")
    if (root / "lock.json").exists():
        raise FileExistsError("Never re-lock")
    if not (root / "PROTOCOL.md").is_file():
        raise PermissionError("Protocol text required before locking")
    if (root / "raw").exists():
        raise PermissionError("No prediction may exist before the lock")
    if read(V15 / "integrity.json")["status"] != "PASS":
        raise PermissionError("V15 must be finalized with integrity PASS")
    write(
        root / "lock.json",
        {
            "status": "FROZEN_BEFORE_ANY_V16_PREDICTION",
            "experiment": EXPERIMENT,
            "created_utc": datetime.datetime.now(datetime.UTC).isoformat(),
            "primary": "FRESH-V2 A = FAR AbsRel(REPROJ_HARMONIC) - FAR AbsRel(V11 C1) (INPAINT-V1 label rule); P2 Delta = mean A(EVAL-V3/V4, V13 rows) - mean A(FRESH-V2), independent scene bootstraps",
            "input_sha256": {
                name: {"path": str(Path(p).resolve()), "sha256": sha(p)}
                for name, p in inputs(root).items()
            },
            "fresh_v2_use": "third use (V7 qualification, GB-V1 geometric baselines); mechanism only",
            "final_holdout_opened": False,
        },
    )
    print("LOCKED", EXPERIMENT, flush=True)


def verify(root):
    lock_ = read(root / "lock.json")
    for name, entry in lock_["input_sha256"].items():
        if sha(entry["path"]) != entry["sha256"]:
            raise PermissionError(f"Locked input changed: {name}")
    return lock_


def carriers():
    """{(family, seed): (model, carrier module)} with every selected checkpoint hash-checked."""
    seeds = read(V11 / "training_contract.json")["seeds"]
    models = {}
    for family, (carrier, selected_path, _) in FAMILIES.items():
        selected = read(selected_path)["C1"]
        for seed in seeds:
            item = selected[str(seed)]
            if sha(Path(item["path"])) != item["sha256"]:
                raise PermissionError(f"Selected {family} checkpoint changed")
            models[family, seed] = (carrier.load_checkpoint(item["path"], "cpu")[0].eval(), carrier)
    return seeds, models


@torch.no_grad()
def predict(record, manifest, base, models, constant, access, tag, states=None):
    """Every sealed array of every primary query of one scene (V13's code path, every family)."""
    stage, inp = host(), inpainting()
    geo = stage.geometry()
    renderer = FixedMeasurementRenderer(n_samples=64, ray_chunk_size=2048)
    out, sid = {}, record["scene_id"]
    for role, context in stage.context_of(record, manifest, base, access).items():
        anchor = context[0].camera.c2w
        bounds = context_depth_bounds(context, anchor)
        points = geo.context_points(context)
        built = {}
        for (family, seed), (model, carrier) in models.items():
            state, state_anchor = carrier.build_state(model, context, bounds, f"{tag}:{sid}:{role}")
            built[family, seed] = (state, state_anchor)
            if states is not None:
                states[f"{family}:{seed}:{sid}/{role}"] = hash_scene_state(state)
        for qid in record["roles"]["primary_query"]:
            access.append({"scene_id": sid, "frame_id": qid, "purpose": "QUERY_CAMERA_ONLY"})
            camera = geo.query_camera(record, qid, manifest["image_size"])
            depth, hit = geo.reproject(points, camera)
            reproj = geo.fill_nearest(depth, hit, constant) if hit.any() else None
            if reproj is None:
                reproj = np.full(camera.image_size, constant, dtype=np.float64)
            arrays = {
                "REPROJ_NN": reproj,
                "hit": hit,
                "near": inp.near_mask(hit),
                "REPROJ_HARMONIC": inp.harmonic_fill(depth, hit, inp.ray_cosine(camera), constant),
            }
            batched = Cameras(
                camera.intrinsics[None, None], camera.c2w[None, None], camera.image_size
            )
            for (family, seed), (state, state_anchor) in built.items():
                local = transform_cameras(batched, torch.linalg.inv(state_anchor))
                pred = renderer(state, local, ("depth", "visibility"))
                arrays[f"{family}_{seed}"] = pred["depth"][0, 0, 0].numpy().astype(np.float64)
                arrays[f"OPACITY_{family}_{seed}"] = pred["visibility"][0, 0, 0].numpy()
            out[role, qid] = arrays
    return out


def replay(models, constant):
    """Every family on the first EVAL-V3 scenes must reproduce its own sealed predictions."""
    manifest = read(EVAL_V3)
    seals = {family: read(seal)["predictions"] for family, (_, _, (seal, _, _)) in FAMILIES.items()}
    inpaint_seal = read(INPAINT / "raw/inpaint_seal.json")
    worst, compared = 0.0, 0
    for record in manifest["scenes"][:REPLAY_SCENES]:
        sid = record["scene_id"]
        mine = predict(record, manifest, EVAL_V3.parent, models, constant, [], "eval_v3")
        for (role, qid), arrays in mine.items():
            filled = dict(np.load(inpaint_seal[f"EVAL_V3/{sid}/{role}/{qid}"]["path"]))
            worst = max(
                worst, float(np.max(np.abs(arrays["REPROJ_HARMONIC"] - filled["REPROJ_HARMONIC"])))
            )
            for family, (_, _, (_, prefix, array)) in FAMILIES.items():
                sealed = dict(np.load(seals[family][f"{prefix}{sid}/{role}/{qid}"]["path"]))
                for seed in {s for (f, s) in models if f == family}:
                    worst = max(
                        worst,
                        float(
                            np.max(np.abs(arrays[f"{family}_{seed}"] - sealed[f"{array}_{seed}"]))
                        ),
                    )
                    compared += 1
    if worst > 1e-6:
        raise AssertionError(f"Replay of the sealed EVAL-V3 predictions failed: {worst}")
    return {"scenes": REPLAY_SCENES, "arrays_compared": compared, "max_abs_deviation": worst}


def seal(root):
    root = Path(root).resolve()
    verify(root)
    out = root / "raw"
    if (out / "prediction_seal.json").exists():
        raise FileExistsError("Predictions are sealed once")
    seeds, models = carriers()
    constant = read(V11 / "training_contract.json")["eval_v3"]["reprojection_fill_constant_m"]
    replayed = replay(models, constant)
    print("REPLAY_OK", json.dumps(replayed), flush=True)
    manifest = read(FRESH)
    if any(r["split"] != "FRESH_QUALIFICATION" for r in manifest["scenes"]):
        raise PermissionError("FRESH-V2 records only")
    access, entries, states = [], {}, {}
    for record in manifest["scenes"]:
        sid = record["scene_id"]
        scene = predict(
            record, manifest, FRESH.parent, models, constant, access, "fresh_v2", states
        )
        for (role, qid), arrays in scene.items():
            path = out / "predictions" / f"{sid}_{role}_{qid}.npz"
            path.parent.mkdir(parents=True, exist_ok=True)
            np.savez_compressed(path, **arrays)
            entries[f"{sid}/{role}/{qid}"] = {"path": str(path), "sha256": sha(path)}
        print("SEALED_SCENE", sid, flush=True)
    if any(
        "depth" in e.get("channels", []) and e.get("purpose") != "CONTEXT_ONLY_RGBD" for e in access
    ):
        raise AssertionError("Only context depth may be read before the seal")
    write(out / "state_hashes.json", states)
    write(out / "GT_access_before_seal.json", access)
    write(
        out / "prediction_seal.json",
        {
            "predictions": entries,
            "state_hashes_sha256": sha(out / "state_hashes.json"),
            "query_depth_read": False,
            "reprojection_fill_constant_m": constant,
            "seeds": seeds,
            "replay_on_eval_v3": replayed,
        },
    )
    print("PREDICTIONS_SEALED", len(entries), flush=True)


def absrel(pred, gt, mask):
    return float(np.mean(np.abs(pred[mask] - gt[mask]) / gt[mask])) if mask.any() else None


def score(root):
    root = Path(root).resolve()
    seal_file = read(root / "raw" / "prediction_seal.json")
    constant, seeds = seal_file["reprojection_fill_constant_m"], seal_file["seeds"]
    rows, access = [], []
    for record in read(FRESH)["scenes"]:
        sid = record["scene_id"]
        frames = {f["frame_id"]: f for f in record["frames"]}
        for role in ("A", "B"):
            for qid in record["roles"]["primary_query"]:
                entry = seal_file["predictions"][f"{sid}/{role}/{qid}"]
                if sha(entry["path"]) != entry["sha256"]:
                    raise PermissionError("A sealed prediction changed")
                arrays = dict(np.load(entry["path"]))
                access.append(
                    {"scene_id": sid, "frame_id": qid, "purpose": "POST_SEAL_QUERY_DEPTH"}
                )
                gt = np.load(frames[qid]["depth"]).squeeze().astype(np.float64)
                valid = np.isfinite(gt) & (gt > 0)
                near = arrays["near"].astype(bool)
                masks = {"ALL": valid, "NEAR": valid & near, "FAR": valid & ~near}
                harmonic = arrays["REPROJ_HARMONIC"]
                predictions = {
                    ("CONSTANT", 0): np.full(gt.shape, constant, dtype=np.float64),
                    ("REPROJ_NN", 0): arrays["REPROJ_NN"],
                    ("REPROJ_HARMONIC", 0): harmonic,
                }
                for family in FAMILIES:
                    for seed in seeds:
                        carrier = arrays[f"{family}_{seed}"]
                        low = arrays[f"OPACITY_{family}_{seed}"].astype(np.float64) < THRESHOLD
                        predictions[family, seed] = carrier
                        predictions[f"HFILL8_{family}", seed] = np.where(near, harmonic, carrier)
                        predictions[f"AHFILL8_{family}", seed] = np.where(
                            near | low, harmonic, carrier
                        )
                for (method, seed), value in predictions.items():
                    row = {
                        "cohort": "FRESH_V2",
                        "scene_id": sid,
                        "role": role,
                        "query_id": qid,
                        "method": method,
                        "seed": seed,
                    }
                    for region, mask in masks.items():
                        row[f"absrel_{region}"] = absrel(value, gt, mask)
                    row["depth_absrel"] = depth_metrics(value, gt)["depth_absrel"]
                    row["far_fraction"] = float(masks["FAR"].sum() / max(valid.sum(), 1))
                    rows.append(row)
    write(root / "raw" / "query_rows.json", rows)
    write(root / "raw" / "GT_access_after_seal.json", access)
    print("SCORED", len(rows), flush=True)


def advantage(rows, carrier, baseline="REPROJ_HARMONIC"):
    """{scene: FAR AbsRel(baseline) - FAR AbsRel(carrier)} with the V11 aggregation."""
    stage = host()
    a, b = (
        stage.scene_values(rows, baseline, "absrel_FAR"),
        stage.scene_values(rows, carrier, "absrel_FAR"),
    )
    return {s: a[s] - b[s] for s in sorted(set(a) & set(b))}


def unpaired(first, second):
    """mean(first) - mean(second), independent scene bootstraps (10,000 draws, seed 20260928)."""
    rng = np.random.default_rng(SEED)
    x, y = np.array(list(first.values())), np.array(list(second.values()))
    draws = rng.choice(x, (DRAWS, len(x))).mean(1) - rng.choice(y, (DRAWS, len(y))).mean(1)
    return {
        "mean": float(x.mean() - y.mean()),
        "ci95": [float(np.percentile(draws, 2.5)), float(np.percentile(draws, 97.5))],
        "n_first": len(x),
        "n_second": len(y),
    }


def analyze_rows(rows, eval_rows, ext_volumes):
    stage, label = host(), inpainting().geometry().label

    def values(method, region, subset=rows):
        return stage.scene_values(subset, method, f"absrel_{region}")

    methods = sorted({r["method"] for r in rows})
    contrasts = {}
    definitions = {
        "A_V11C1": ("REPROJ_HARMONIC", "FAR", "V11C1", "FAR"),
        "A_V12C1": ("REPROJ_HARMONIC", "FAR", "V12C1", "FAR"),
        "NN_MINUS_V11C1_FAR": ("REPROJ_NN", "FAR", "V11C1", "FAR"),
        "HARMONIC_MINUS_HFILL8_V11C1": ("REPROJ_HARMONIC", "ALL", "HFILL8_V11C1", "ALL"),
        "HARMONIC_MINUS_AHFILL8_V11C1": ("REPROJ_HARMONIC", "ALL", "AHFILL8_V11C1", "ALL"),
        "HARMONIC_MINUS_HFILL8_V12C1": ("REPROJ_HARMONIC", "ALL", "HFILL8_V12C1", "ALL"),
        "CONSTANT_MINUS_V11C1": ("CONSTANT", "ALL", "V11C1", "ALL"),
        "NN_MINUS_V11C1_ALL": ("REPROJ_NN", "ALL", "V11C1", "ALL"),
    }
    for name, (a, ra, b, rb) in definitions.items():
        stats = stage.contrast(values(a, ra), values(b, rb))
        contrasts[name] = {"gain": stats, "definition": f"AbsRel {ra}({a}) - AbsRel {rb}({b})"}
        if stats is not None:
            contrasts[name]["label"] = label(stats)
    a_fresh = advantage(rows, "V11C1")
    a_eval = advantage(eval_rows, "C1")
    delta = unpaired(a_eval, a_fresh)
    p1 = contrasts["A_V11C1"].get("label", "UNDEFINED")
    gap = delta["ci95"][0] > 0
    branch = {(True, False): "A", (True, True): "B", (False, True): "C", (False, False): "D"}[
        (p1 == "ABOVE", gap)
    ]
    seen = {r["scene_id"] for r in rows if r["scene_id"].split("_")[1] in ext_volumes}
    split = {}
    for family in ("V14C1", "V15C1", "V11C1"):
        adv = advantage(rows, family)
        split[family] = {
            name: {"n": len(v), "mean_A": float(np.mean(list(v.values()))) if v else None}
            for name, v in (
                ("volume_has_train_ext_scene", {s: x for s, x in adv.items() if s in seen}),
                (
                    "volume_unseen_by_every_training_set",
                    {s: x for s, x in adv.items() if s not in seen},
                ),
            )
        }
    return {
        "absolute_absrel": {
            m: {region: stage.summary(values(m, region)) for region in REGIONS} for m in methods
        },
        "n_scenes": len({r["scene_id"] for r in rows}),
        "far_pixel_fraction": float(
            np.mean([r["far_fraction"] for r in rows if r["method"] == "REPROJ_NN"])
        ),
        "contrasts": contrasts,
        "A_EVAL_V3_V4": {"mean": float(np.mean(list(a_eval.values()))), "n_scenes": len(a_eval)},
        "A_FRESH_V2": {"mean": float(np.mean(list(a_fresh.values()))), "n_scenes": len(a_fresh)},
        "FAMILIARITY_DELTA": delta,
        "P1_LABEL": p1,
        "FAMILIARITY_GAP": gap,
        "INTERPRETATION_BRANCH": branch,
        "train_ext_volume_split": split,
    }


def analyze(root):
    root = Path(root).resolve()
    rows_path = root / "raw" / "query_rows.json"
    eval_rows = [
        r
        for r in read(V13 / "raw/query_rows.json")
        if r["cohort"] in ("EVAL_V3_NEAR", "EVAL_V4_NEAR")
    ]
    config = read(V14 / "training_contract.json")
    ext = set(config["train_sets"]["TRAIN_EXT"]) - set(config["train_sets"]["TRAIN72"])
    result = analyze_rows(read(rows_path), eval_rows, {s.split("_")[1] for s in ext})
    result["replay_on_eval_v3"] = read(root / "raw" / "prediction_seal.json")["replay_on_eval_v3"]
    result["input_sha256"] = {
        str(rows_path): sha(rows_path),
        str(V13 / "raw/query_rows.json"): sha(V13 / "raw/query_rows.json"),
    }
    result["final_holdout_opened"] = False
    write(root / "volume_disjoint_results.json", result)
    report(root, result)
    return result


def fmt(stats):
    if not stats or stats.get("mean") is None:
        return "UNDEFINED"
    lo, hi = stats["ci95"]
    tail = (
        f",场景 {stats['positive']}/{stats['tied']}/{stats['negative']}"
        if "positive" in stats
        else ""
    )
    return f"{stats['mean']:+.4f} [{lo:+.4f}, {hi:+.4f}]{tail}"


def report(root, result):
    absolute, contrasts = result["absolute_absrel"], result["contrasts"]

    def cell(method, region):
        value = (absolute.get(method, {}).get(region) or {}).get("mean")
        return "—" if value is None else f"{value:.4f}"

    lines = [
        f"# {EXPERIMENT}",
        "",
        f"不训练的机制检验:只用 TRAIN72 训练的 carrier,在与训练集 volume 完全不相交的 FRESH-V2({result['n_scenes']} 个场景)上,相对调和插值补洞的补全优势是否还在,并与共享 volume 的 EVAL-V3/V4(V13 的封存结果)比较。协议见 [PROTOCOL.md](PROTOCOL.md)。",
        "",
        f"- **P1(FRESH-V2 上的补全优势 A = 调和插值 FAR − V11 C1 FAR):** {fmt(contrasts['A_V11C1']['gain'])},{result['P1_LABEL']}",
        f"- **P2(熟悉度差 Δ = A(EVAL-V3/V4) − A(FRESH-V2)):** {fmt(result['FAMILIARITY_DELTA'])};A(EVAL) = {result['A_EVAL_V3_V4']['mean']:+.4f}({result['A_EVAL_V3_V4']['n_scenes']} 个场景),A(FRESH) = {result['A_FRESH_V2']['mean']:+.4f}",
        f"- **分支 {result['INTERPRETATION_BRANCH']}**;复现封存预测的最大差 {result['replay_on_eval_v3']['max_abs_deviation']}",
        "",
        f"FAR 像素占 {result['far_pixel_fraction']:.1%}。",
        "",
        "| FRESH-V2 AbsRel | ALL | NEAR | FAR |",
        "|---|---:|---:|---:|",
    ]
    for method in (
        "CONSTANT",
        "REPROJ_NN",
        "REPROJ_HARMONIC",
        "V11C1",
        "V12C1",
        "V14C1",
        "V15C1",
        "HFILL8_V11C1",
        "AHFILL8_V11C1",
        "HFILL8_V12C1",
    ):
        lines.append(
            f"| {method} | {cell(method, 'ALL')} | {cell(method, 'NEAR')} | {cell(method, 'FAR')} |"
        )
    lines += ["", "| 对比 | 定义 | 差值 [95% CI],场景 好/平/差 | 标签 |", "|---|---|---|---|"]
    for name, entry in contrasts.items():
        lines.append(
            f"| {name} | {entry['definition']} | {fmt(entry['gain'])} | {entry.get('label', 'UNDEFINED')} |"
        )
    lines += [
        "",
        "**TRAIN72+EXT 的模型(描述性):** 按 FRESH 场景所在 volume 是否有 TRAIN-EXT 场景分组的平均 A:",
    ]
    for family, groups in result["train_ext_volume_split"].items():
        text = ";".join(
            f"{name} {g['n']} 个场景 {g['mean_A']:+.4f}"
            if g["mean_A"] is not None
            else f"{name} 0 个场景"
            for name, g in groups.items()
        )
        lines.append(f"- {family}:{text}")
    lines += [
        "",
        "**限制:** FRESH-V2 是第三次使用,只作机制检验;场景只有 17 个、10 个 volume;两个队列的难度也可能不同;受保护的 final holdout 未打开。",
    ]
    (root / "README.md").write_text("\n".join(lines) + "\n")
    summary = [
        "RGBD VOLUME DISJOINT V16",
        f"P1_LABEL={result['P1_LABEL']}",
        f"A_FRESH_V2={result['A_FRESH_V2']['mean']:.6f}",
        f"A_EVAL_V3_V4={result['A_EVAL_V3_V4']['mean']:.6f}",
        f"FAMILIARITY_DELTA={result['FAMILIARITY_DELTA']['mean']:.6f} CI={result['FAMILIARITY_DELTA']['ci95']}",
        f"FAMILIARITY_GAP={str(result['FAMILIARITY_GAP']).lower()}",
        f"INTERPRETATION_BRANCH={result['INTERPRETATION_BRANCH']}",
        f"REPLAY_MAX_ABS_DEVIATION={result['replay_on_eval_v3']['max_abs_deviation']}",
        "FINAL_HOLDOUT_TOUCHED=false",
    ]
    (root / "terminal_summary.txt").write_text("\n".join(summary) + "\n")
    print("\n".join(summary), flush=True)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--stage", choices=("lock", "seal", "score", "analyze"), required=True)
    parser.add_argument("--root", type=Path, default=Path("outputs") / EXPERIMENT)
    args = parser.parse_args()
    torch.set_num_threads(1)
    {"lock": lock, "seal": seal, "score": score, "analyze": analyze}[args.stage](args.root)


if __name__ == "__main__":
    main()