Source: scripts/evaluate_rgbd_volume_abstention.py · Plain text

scripts/evaluate_rgbd_volume_abstention.py

#!/usr/bin/env python3
"""V13: abstain outside the state's volume (no training).

The frozen V11 C1 selected checkpoints render depth and opacity for queries far from the context
frames (EVAL-FAR) and for the near queries of EVAL-V3/V4. AHFILL8 uses REPROJ_HARMONIC on NEAR
pixels and on FAR pixels whose rendered opacity is below 0.5, the carrier elsewhere, as fixed by
PROTOCOL.md. Stages: `lock` freezes the protocol, code and inputs before any far query is rendered;
`seal --cohort` replays two sealed EVAL-V3 scenes, then renders and hashes every prediction and
geometric baseline of one cohort before any query depth is read; `score` needs every cohort's seal;
`analyze` computes the preregistered contrasts.
"""
# 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 generate_rays, intersect_aabb, transform_cameras
from mcss.measurements import FixedMeasurementRenderer
from mcss.mechanism_pilot import rgbd_completion_carrier as v11_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-V13"
V11 = Path("outputs/EXP-3D-RGBD-COMPLETION-V11")
INPAINT = Path("outputs/EXP-3D-RGBD-INPAINT-BASELINES-V1")
EVAL_FAR = Path("outputs/EXP-3D-RGBD-EVAL-FAR-DATA")
EXPLORATORY = "audit/exploratory_opacity_fallback"
COHORTS = {
    "EVAL_FAR": (EVAL_FAR / "manifest_eval_far.json", "far_query", "EVAL_FAR"),
    "EVAL_V3_NEAR": (
        Path("outputs/EXP-3D-RGBD-EVAL-V3-DATA/manifest_eval_v3.json"),
        "primary_query",
        "EVAL_V3",
    ),
    "EVAL_V4_NEAR": (
        Path("outputs/EXP-3D-RGBD-EVAL-V4-DATA/manifest_eval_v4.json"),
        "primary_query",
        "EVAL_V4",
    ),
}
PRIMARY = "EVAL_FAR"
THRESHOLD = 0.5
DESCRIPTIVE_THRESHOLDS = (0.3, 0.7, 0.9)
REGIONS = ("ALL", "NEAR", "FAR")
REPLAY_SCENES = 2
METHODS = (
    "CONSTANT",
    "REPROJ_NN",
    "REPROJ_HARMONIC",
    "C1",
    "HFILL8",
    "AHFILL8",
    *(f"AHFILL8_t{t}" for t in DESCRIPTIVE_THRESHOLDS),
)
CONTRASTS = {
    "ABSTAIN_GAIN": ("HFILL8", "ALL", "AHFILL8", "ALL"),
    "HARMONIC_VS_AHFILL8": ("REPROJ_HARMONIC", "ALL", "AHFILL8", "ALL"),
    "HARMONIC_VS_HFILL8": ("REPROJ_HARMONIC", "ALL", "HFILL8", "ALL"),
    "NN_VS_AHFILL8": ("REPROJ_NN", "ALL", "AHFILL8", "ALL"),
    "CONSTANT_VS_C1": ("CONSTANT", "ALL", "C1", "ALL"),
    "NN_VS_C1": ("REPROJ_NN", "ALL", "C1", "ALL"),
    "HARMONIC_VS_C1_FAR": ("REPROJ_HARMONIC", "FAR", "C1", "FAR"),
    "HARMONIC_VS_AHFILL8_FAR": ("REPROJ_HARMONIC", "FAR", "AHFILL8", "FAR"),
    **{
        f"HARMONIC_VS_AHFILL8_t{t}": ("REPROJ_HARMONIC", "ALL", f"AHFILL8_t{t}", "ALL")
        for t in DESCRIPTIVE_THRESHOLDS
    },
}
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("v13_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("v13_inpaint", "run_rgbd_inpaint_baselines.py")


def abstain(near, opacity, harmonic, carrier, threshold):
    """AHFILL8: harmonic on NEAR pixels and on FAR pixels the carrier renders with low opacity."""
    return np.where(near | (opacity < threshold), harmonic, carrier)


def inputs(root, v11=V11, inpaint=INPAINT):
    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",
        "v11_contract": v11 / "training_contract.json",
        "v11_selected": v11 / "selected_checkpoints.json",
        "v11_eval_v3_seal": v11 / "raw/eval_v3/prediction_seal.json",
        "inpaint_lock": inpaint / "lock.json",
        "eval_far_candidate_lock": EVAL_FAR / "candidate_lock.json",
        "eval_far_integrity": EVAL_FAR / "preparation_integrity.json",
    }
    for name, (manifest, _, _) in COHORTS.items():
        paths[f"{name}_manifest"] = manifest
    for cohort in ("EVAL_V3", "EVAL_V4", "REPLICA"):
        paths[f"exploratory_{cohort}"] = root / EXPLORATORY / f"fallback_{cohort}.json"
    return paths


# ---------------------------------------------------------------- stages
def lock(root, v11=V11, inpaint=INPAINT):
    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(EVAL_FAR / "preparation_integrity.json")["status"] != "PASS":
        raise PermissionError("EVAL-FAR data must be prepared with integrity PASS")
    paths = inputs(root, v11, inpaint)
    write(
        root / "lock.json",
        {
            "status": "FROZEN_BEFORE_ANY_FAR_QUERY_PREDICTION",
            "experiment": EXPERIMENT,
            "created_utc": datetime.datetime.now(datetime.UTC).isoformat(),
            "threshold": THRESHOLD,
            "primary": "EVAL_FAR: P1 AbsRel(HFILL8) - AbsRel(AHFILL8) (frozen V2 gate); P2 AbsRel(REPROJ_HARMONIC) - AbsRel(AHFILL8) (INPAINT-V1 label rule)",
            "input_sha256": {
                name: {"path": str(Path(p).resolve()), "sha256": sha(p)}
                for name, p in paths.items()
            },
            "far_query_rendered_before_lock": False,
            "final_holdout_opened": False,
        },
    )
    print("LOCKED", EXPERIMENT, flush=True)


def verify(root, v11=V11):
    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}")
    if read(v11 / "integrity.json")["status"] != "PASS":
        raise PermissionError("V11 must be finalized with integrity PASS")
    return lock_


def carriers(v11=V11):
    config = read(v11 / "training_contract.json")
    selected = read(v11 / "selected_checkpoints.json")["C1"]
    models = {}
    for seed in config["seeds"]:
        item = selected[str(seed)]
        if sha(Path(item["path"])) != item["sha256"]:
            raise PermissionError("Selected V11 C1 checkpoint changed")
        models[seed] = v11_carrier.load_checkpoint(item["path"], "cpu")[0].eval()
    return config, models


@torch.no_grad()
def predict(record, manifest, base, models, constant, role_name, access, tag, states=None):
    """Every sealed array of every query of one scene: carrier depth and opacity, box, geometry.

    `tag` only names the construction episode ("eval_v3" reproduces V11's own episode ids).
    """
    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 seed, model in models.items():
            state, state_anchor = v11_carrier.build_state(
                model, context, bounds, f"{tag}:{sid}:{role}"
            )
            built[seed] = (state, state_anchor)
            if states is not None:
                states[f"C1:{seed}:{sid}/{role}"] = hash_scene_state(state)
        box_state, box_anchor = next(iter(built.values()))
        if not all(
            torch.equal(s.bounds, box_state.bounds) and torch.equal(a, box_anchor)
            for s, a in built.values()
        ):
            raise AssertionError("All seeds of one context must share the volume and the anchor")
        for qid in record["roles"][role_name]:
            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
            )
            box_local = transform_cameras(batched, torch.linalg.inv(box_anchor))
            origins, directions = generate_rays(box_local.to(dtype=torch.float64))
            t_near, t_exit, box_hit = intersect_aabb(
                origins[0, 0], directions[0, 0], box_state.bounds[0].double()
            )
            arrays |= {
                "BOX_NEAR": t_near.numpy(),
                "BOX_EXIT": t_exit.numpy(),
                "BOX_HIT": box_hit.numpy(),
            }
            for seed, (state, state_anchor) in built.items():
                local = transform_cameras(batched, torch.linalg.inv(state_anchor))
                pred = renderer(state, local, ("depth", "visibility"))
                arrays[f"C1_{seed}"] = pred["depth"][0, 0, 0].numpy().astype(np.float64)
                arrays[f"OPACITY_{seed}"] = pred["visibility"][0, 0, 0].numpy()
            out[role, qid] = arrays
    return out


def replay(models, constant, v11=V11, inpaint=INPAINT):
    """Our code path on the first EVAL-V3 scenes must reproduce V11's and INPAINT-V1's sealed files."""
    manifest_path, role_name, _ = COHORTS["EVAL_V3_NEAR"]
    manifest = read(manifest_path)
    seal_ = read(v11 / "raw/eval_v3/prediction_seal.json")["predictions"]
    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, manifest_path.parent, models, constant, role_name, [], "eval_v3"
        )
        for (role, qid), arrays in mine.items():
            sealed = dict(np.load(seal_[f"{sid}/{role}/{qid}"]["path"]))
            filled = dict(np.load(inpaint_seal[f"EVAL_V3/{sid}/{role}/{qid}"]["path"]))
            if not (
                np.array_equal(arrays["hit"], sealed["hit"])
                and np.array_equal(arrays["near"], sealed["near"])
            ):
                raise AssertionError(f"Replay hit/near mask differs for {sid}/{role}/{qid}")
            pairs = [
                (arrays["REPROJ_NN"], sealed["REPROJ_NN"]),
                (arrays["REPROJ_HARMONIC"], filled["REPROJ_HARMONIC"]),
            ]
            pairs += [(arrays[f"C1_{seed}"], sealed[f"C1_{seed}"]) for seed in models]
            for a, b in pairs:
                worst = max(worst, float(np.max(np.abs(a - b))))
                compared += 1
    if worst > 1e-6:
        raise AssertionError(f"Replay of the sealed EVAL-V3 pipeline failed: {worst}")
    return {"scenes": REPLAY_SCENES, "arrays_compared": compared, "max_abs_deviation": worst}


def seal(root, cohort, v11=V11, inpaint=INPAINT):
    root = Path(root).resolve()
    verify(root, v11)
    out = root / "raw" / cohort
    if (out / "prediction_seal.json").exists():
        raise FileExistsError("Each cohort is sealed once")
    if (root / "raw" / "query_rows.json").exists():
        raise PermissionError("No cohort may be sealed after scoring")
    config, models = carriers(v11)
    constant = config["eval_v3"]["reprojection_fill_constant_m"]
    if config["eval_v3"]["near_px"] != inpainting().NEAR_PX:
        raise PermissionError("NEAR definition must be V11's")
    manifest_path, role_name, split = COHORTS[cohort]
    manifest = read(manifest_path)
    if any(r["split"] != split for r in manifest["scenes"]):
        raise PermissionError(f"{cohort}: {split} records only")
    replayed = replay(models, constant, v11, inpaint)
    print("REPLAY_OK", cohort, json.dumps(replayed), flush=True)
    access, entries, states = [], {}, {}
    for number, record in enumerate(manifest["scenes"], 1):
        sid = record["scene_id"]
        scene = predict(
            record,
            manifest,
            manifest_path.parent,
            models,
            constant,
            role_name,
            access,
            cohort.lower(),
            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", cohort, number, len(manifest["scenes"]), 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",
        {
            "cohort": cohort,
            "role": role_name,
            "predictions": entries,
            "state_hashes_sha256": sha(out / "state_hashes.json"),
            "query_depth_read": False,
            "near_px": config["eval_v3"]["near_px"],
            "reprojection_fill_constant_m": constant,
            "replay_on_eval_v3": replayed,
        },
    )
    print("PREDICTIONS_SEALED", cohort, 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()
    lock_ = verify(root)
    seals = {c: read(root / "raw" / c / "prediction_seal.json") for c in COHORTS}
    seeds = read(lock_["input_sha256"]["v11_contract"]["path"])["seeds"]
    rows, flags, access = [], [], []
    for cohort, (manifest_path, role_name, _) in COHORTS.items():
        seal_ = seals[cohort]
        constant = seal_["reprojection_fill_constant_m"]
        for record in read(manifest_path)["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"][role_name]:
                    entry = seal_["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(
                        {
                            "cohort": cohort,
                            "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)
                    far = valid & ~near
                    harmonic = arrays["REPROJ_HARMONIC"]
                    inside = (
                        arrays["BOX_HIT"].astype(bool)
                        & (gt >= arrays["BOX_NEAR"])
                        & (gt <= arrays["BOX_EXIT"])
                    )
                    masks = {"ALL": valid, "NEAR": valid & near, "FAR": far}
                    predictions = {
                        ("CONSTANT", 0): np.full(gt.shape, constant, dtype=np.float64),
                        ("REPROJ_NN", 0): arrays["REPROJ_NN"],
                        ("REPROJ_HARMONIC", 0): harmonic,
                    }
                    for seed in seeds:
                        carrier = arrays[f"C1_{seed}"]
                        opacity = arrays[f"OPACITY_{seed}"].astype(np.float64)
                        predictions["C1", seed] = carrier
                        predictions["HFILL8", seed] = np.where(near, harmonic, carrier)
                        predictions["AHFILL8", seed] = abstain(
                            near, opacity, harmonic, carrier, THRESHOLD
                        )
                        for t in DESCRIPTIVE_THRESHOLDS:
                            predictions[f"AHFILL8_t{t}", seed] = abstain(
                                near, opacity, harmonic, carrier, t
                            )
                        flagged = far & (opacity < THRESHOLD)
                        flags.append(
                            {
                                "cohort": cohort,
                                "scene_id": sid,
                                "role": role,
                                "query_id": qid,
                                "seed": seed,
                                "surface_in_volume": float(inside[valid].mean()),
                                "far_fraction": float(far.sum() / max(valid.sum(), 1)),
                                "flagged_share_of_far": float(flagged.sum() / max(far.sum(), 1)),
                                "flagged": float(flagged.sum()),
                                "flagged_outside": float((flagged & ~inside).sum()),
                                "far_outside": float((far & ~inside).sum()),
                            }
                        )
                    for (method, seed), value in predictions.items():
                        row = {
                            "cohort": cohort,
                            "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(far.sum() / max(valid.sum(), 1))
                        rows.append(row)
    write(root / "raw" / "query_rows.json", rows)
    write(root / "raw" / "flag_rows.json", flags)
    write(root / "raw" / "GT_access_after_seal.json", access)
    print("SCORED", len(rows), flush=True)
    return rows


def cohort_block(rows, flags):
    stage, label = host(), inpainting().geometry().label

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

    methods = sorted({r["method"] for r in rows})
    block = {
        "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}),
        "contrasts": {},
    }
    for name, (a, ra, b, rb) in CONTRASTS.items():
        stats = stage.contrast(values(a, ra), values(b, rb))
        entry = {"gain": stats, "definition": f"AbsRel {ra}({a}) - AbsRel {rb}({b})"}
        if stats is not None:
            entry["label"] = label(stats)
            entry["gate"], entry["gate_checks"] = stage.gate(stats)
        block["contrasts"][name] = entry
    per_scene = {}
    for f in flags:
        per_scene.setdefault(f["scene_id"], []).append(f)

    def scene_mean(key):
        return float(np.mean([np.mean([f[key] for f in v]) for v in per_scene.values()]))

    pooled = {
        key: sum(f[key] for f in flags) for key in ("flagged", "flagged_outside", "far_outside")
    }
    block["coverage"] = {
        "surface_in_volume": scene_mean("surface_in_volume"),
        "far_fraction": scene_mean("far_fraction"),
        "flagged_share_of_far": scene_mean("flagged_share_of_far"),
        "precision_outside_volume": pooled["flagged_outside"] / max(pooled["flagged"], 1),
        "recall_outside_volume": pooled["flagged_outside"] / max(pooled["far_outside"], 1),
    }
    return block


def analyze_rows(rows, flags):
    result = {"cohorts": {}}
    for cohort in COHORTS:
        subset = [r for r in rows if r["cohort"] == cohort]
        if subset:
            result["cohorts"][cohort] = cohort_block(
                subset, [f for f in flags if f["cohort"] == cohort]
            )
    primary = result["cohorts"][PRIMARY]["contrasts"]
    p1 = primary["ABSTAIN_GAIN"].get("gate", "UNDEFINED")
    p2 = primary["HARMONIC_VS_AHFILL8"].get("label", "UNDEFINED")
    first, second = p1 == "SUPPORTED", p2 == "ABOVE"
    result |= {
        "ABSTAIN_STATUS": p1,
        "HARMONIC_LABEL": p2,
        "INTERPRETATION_BRANCH": "A" if first and second else "B" if first or second else "C",
    }
    return result


def analyze(root):
    root = Path(root).resolve()
    rows_path, flags_path = root / "raw" / "query_rows.json", root / "raw" / "flag_rows.json"
    result = analyze_rows(read(rows_path), read(flags_path))
    result["replay_on_eval_v3"] = {
        c: read(root / "raw" / c / "prediction_seal.json")["replay_on_eval_v3"] for c in COHORTS
    }
    result["input_sha256"] = {str(p): sha(p) for p in (rows_path, flags_path)}
    result["final_holdout_opened"] = False
    write(root / "volume_results.json", result)
    report(root, result)
    return result


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


def report(root, result):
    primary = result["cohorts"][PRIMARY]["contrasts"]
    worst = max(r["max_abs_deviation"] for r in result["replay_on_eval_v3"].values())
    lines = [
        f"# {EXPERIMENT}",
        "",
        "状态体积之外的弃权回退(不训练):冻结的 V11 C1 在渲染不透明度 < 0.5 的 FAR 像素上弃权,改用调和插值补洞(AHFILL8)。主队列 EVAL-FAR 的 query 远离上下文帧,此前从未被任何模型读取。协议见 [PROTOCOL.md](PROTOCOL.md)。",
        "",
        f"- **P1 ABSTAIN_GAIN(HFILL8 − AHFILL8):** {fmt(primary['ABSTAIN_GAIN']['gain'])},{result['ABSTAIN_STATUS']}",
        f"- **P2 调和插值补洞 − AHFILL8:** {fmt(primary['HARMONIC_VS_AHFILL8']['gain'])},{result['HARMONIC_LABEL']}",
        f"- **分支 {result['INTERPRETATION_BRANCH']}**;复现 V11 封存预测的最大差 {worst}",
    ]
    for cohort, block in result["cohorts"].items():
        cov = block["coverage"]
        lines += [
            "",
            f"## {cohort}({block['n_scenes']} 个场景)",
            "",
            f"GT 表面落在体积内 {cov['surface_in_volume']:.1%};FAR 像素占 {cov['far_fraction']:.1%},其中被标记 {cov['flagged_share_of_far']:.1%};标记对体积外像素的精度 {cov['precision_outside_volume']:.2f}、召回 {cov['recall_outside_volume']:.2f}。",
            "",
            "| 方法 | ALL | NEAR | FAR |",
            "|---|---:|---:|---:|",
        ]
        for method in METHODS:
            cells = [block["absolute_absrel"].get(method, {}).get(region) for region in REGIONS]
            lines.append(
                f"| {method} | "
                + " | ".join("—" if not c else f"{c['mean']:.4f}" for c in cells)
                + " |"
            )
        lines += [
            "",
            "| 对比 | 定义 | 差值 [95% CI],场景 好/平/差 | 标签 | gate |",
            "|---|---|---|---|---|",
        ]
        for name, entry in block["contrasts"].items():
            lines.append(
                f"| {name} | {entry['definition']} | {fmt(entry['gain'])} | {entry.get('label', 'UNDEFINED')} | {entry.get('gate', 'UNDEFINED')} |"
            )
    lines += [
        "",
        "**限制:** EVAL-FAR 的场景已被用于近处 query,它是机制队列,不作资格验证;阈值 0.5 来自已用队列上的探索性分析;受保护的 final holdout 未打开。",
    ]
    (root / "README.md").write_text("\n".join(lines) + "\n")
    summary = [
        "RGBD VOLUME V13",
        f"ABSTAIN_STATUS={result['ABSTAIN_STATUS']}",
        f"HARMONIC_LABEL={result['HARMONIC_LABEL']}",
        f"INTERPRETATION_BRANCH={result['INTERPRETATION_BRANCH']}",
        f"REPLAY_MAX_ABS_DEVIATION={worst}",
    ]
    for cohort, block in result["cohorts"].items():
        for method in ("CONSTANT", "REPROJ_HARMONIC", "C1", "HFILL8", "AHFILL8"):
            value = block["absolute_absrel"].get(method, {}).get("ALL")
            if value:
                summary.append(f"{cohort}_{method}_ALL={value['mean']:.6f}")
        for name in ("ABSTAIN_GAIN", "HARMONIC_VS_AHFILL8", "CONSTANT_VS_C1"):
            entry = block["contrasts"][name]
            if entry["gain"]:
                summary.append(
                    f"{cohort}_{name}={entry['gain']['mean']:.6f} CI={entry['gain']['ci95']} LABEL={entry['label']} GATE={entry['gate']}"
                )
        summary.append(f"{cohort}_SURFACE_IN_VOLUME={block['coverage']['surface_in_volume']:.4f}")
    summary.append("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("--cohort", choices=tuple(COHORTS))
    parser.add_argument("--root", type=Path, default=Path("outputs") / EXPERIMENT)
    args = parser.parse_args()
    torch.set_num_threads(1)
    if args.stage == "seal":
        if args.cohort is None:
            parser.error("--stage seal needs --cohort")
        seal(args.root, args.cohort)
    else:
        {"lock": lock, "score": score, "analyze": analyze}[args.stage](args.root)


if __name__ == "__main__":
    main()