Source: scripts/evaluate_rgbd_replication_eval_v4.py · Plain text

scripts/evaluate_rgbd_replication_eval_v4.py

#!/usr/bin/env python3
"""Replication of V11 and INPAINT-V1 on the never-read EVAL-V4 cohort (no training).

The frozen V11 C0/C1 selected checkpoints, V11's state construction and rendering, V11's NEAR/FAR
regions and INPAINT-V1's inpainting are applied unchanged to EVAL-V4, as fixed by
outputs/EXP-3D-RGBD-REPLICATION-EVAL-V4/PLAN_BEFORE_V11_RESULTS.md (written before any V11
result). Stages: `lock` freezes this protocol and code; `seal` renders every prediction and
computes every geometric baseline from context frames and query cameras only, hashes them, and
replays the pipeline on two EVAL-V3 scenes against V11's and INPAINT-V1's sealed files; `score`
reads query depth only after the 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 transform_cameras
from mcss.measurements import FixedMeasurementRenderer
from mcss.mechanism_pilot.readout_reanalysis import depth_metrics
from mcss.mechanism_pilot.rgbd_completion_carrier import PRIMARY_PAIR, build_state, load_checkpoint
from mcss.mechanism_pilot.rgbd_depth_bounds import context_depth_bounds
from mcss.types import Cameras

EXPERIMENT = "EXP-3D-RGBD-REPLICATION-EVAL-V4"
V11 = Path("outputs/EXP-3D-RGBD-COMPLETION-V11")
INPAINT = Path("outputs/EXP-3D-RGBD-INPAINT-BASELINES-V1")
EVAL_V4 = Path("outputs/EXP-3D-RGBD-EVAL-V4-DATA")
PLAN = "PLAN_BEFORE_V11_RESULTS.md"
PLAN_SHA256 = "b726d336a6b6377f49d75c991e228a21380092312d6ee5e9b6f57828f639fb1c"
SCRIPTS = Path(__file__).resolve().parent
REPLAY_SCENES = 2
REGIONS = ("ALL", "NEAR", "FAR")
CONTRASTS = {
    "HARMONIC_HYBRID_GAIN": ("REPROJ_HARMONIC", "ALL", "HFILL8_C1", "ALL"),
    "HARMONIC_HYBRID_GAIN_C0": ("REPROJ_HARMONIC", "ALL", "HFILL8_C0", "ALL"),
    "HARMONIC_FAR_GAIN_C1": ("REPROJ_HARMONIC", "FAR", "C1", "FAR"),
    "HARMONIC_FAR_GAIN_C0": ("REPROJ_HARMONIC", "FAR", "C0", "FAR"),
    "TELEA_HYBRID_GAIN": ("REPROJ_TELEA", "ALL", "TFILL8_C1", "ALL"),
    "TELEA_FAR_GAIN_C1": ("REPROJ_TELEA", "FAR", "C1", "FAR"),
    "NN_MINUS_HARMONIC_ALL": ("REPROJ_NN", "ALL", "REPROJ_HARMONIC", "ALL"),
    "NN_MINUS_HARMONIC_NEAR": ("REPROJ_NN", "NEAR", "REPROJ_HARMONIC", "NEAR"),
    "NN_MINUS_HARMONIC_FAR": ("REPROJ_NN", "FAR", "REPROJ_HARMONIC", "FAR"),
    "NN_MINUS_TELEA_ALL": ("REPROJ_NN", "ALL", "REPROJ_TELEA", "ALL"),
    "NN_HYBRID_GAIN_V11": ("REPROJ_NN", "ALL", "FILL8_C1", "ALL"),
    "HARMONIC_MINUS_NN_HYBRID": ("REPROJ_HARMONIC", "ALL", "FILL8_C1", "ALL"),
}


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, gates."""
    return module("replication_v11_eval", "evaluate_rgbd_completion_eval_v3.py")


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


def inputs(root, v11=V11, inpaint=INPAINT, eval_v4=EVAL_V4):
    return {
        "protocol": root / "PROTOCOL.md",
        "plan": root / PLAN,
        "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",
        "inpaint_lock": inpaint / "lock.json",
        "eval_v4_manifest": eval_v4 / "manifest_eval_v4.json",
        "eval_v4_integrity": eval_v4 / "preparation_integrity.json",
        "eval_v4_candidate_lock": eval_v4 / "candidate_lock.json",
    }


# ---------------------------------------------------------------- stages
def lock(root, v11=V11, inpaint=INPAINT, eval_v4=EVAL_V4):
    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 sha(root / PLAN) != PLAN_SHA256:
        raise PermissionError("The pre-V11 plan changed")
    if read(eval_v4 / "preparation_integrity.json")["status"] != "PASS":
        raise PermissionError("EVAL-V4 data must be prepared with integrity PASS")
    if (root / "raw").exists():
        raise PermissionError("No prediction may exist before the lock")
    paths = inputs(root, v11, inpaint, eval_v4)
    write(
        root / "lock.json",
        {
            "status": "FROZEN_BEFORE_ANY_EVAL_V4_PREDICTION",
            "experiment": EXPERIMENT,
            "created_utc": datetime.datetime.now(datetime.UTC).isoformat(),
            "primary": "AbsRel(REPROJ_HARMONIC) - AbsRel(HFILL8 of V11 C1) on EVAL-V4 (INPAINT-V1 label rule)",
            "secondary": "V11 COMPLETION_GAIN and HYBRID_GAIN on EVAL-V4 (frozen V2 gate)",
            "input_sha256": {
                name: {"path": str(Path(p).resolve()), "sha256": sha(p)}
                for name, p in paths.items()
            },
            "v11_results_existed_at_lock": (v11 / "eval_v3_results.json").exists(),
            "inpaint_results_existed_at_lock": (inpaint / "inpaint_results.json").exists(),
            "eval_v4_media_scored_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")
    models = {}
    for variant in PRIMARY_PAIR:
        for seed in config["seeds"]:
            item = selected[variant][str(seed)]
            if sha(Path(item["path"])) != item["sha256"]:
                raise PermissionError("Selected V11 checkpoint changed")
            model, _ = load_checkpoint(item["path"], "cpu")
            models[variant, seed] = model.eval()
    return config, 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 (V11's code path plus inpainting).

    `tag` only names the construction episode ("eval_v3" reproduces V11's own episode ids).
    """
    v11, inp, geo = host(), inpainting(), host().geometry()
    renderer = FixedMeasurementRenderer(n_samples=64, ray_chunk_size=2048)
    out = {}
    sid = record["scene_id"]
    for role, context in v11.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 (variant, seed), carrier in models.items():
            state, state_anchor = build_state(carrier, context, bounds, f"{tag}:{sid}:{role}")
            built[variant, seed] = (state, state_anchor)
            if states is not None:
                states[f"{variant}:{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)
            cosine = inp.ray_cosine(camera)
            telea, fallback = inp.telea_fill(depth, hit, cosine, constant)
            arrays = {
                "REPROJ_NN": reproj,
                "hit": hit,
                "near": inp.near_mask(hit),
                "REPROJ_HARMONIC": inp.harmonic_fill(depth, hit, cosine, constant),
                "REPROJ_TELEA": telea,
                "telea_fallback": np.array(fallback),
            }
            batched = Cameras(
                camera.intrinsics[None, None], camera.c2w[None, None], camera.image_size
            )
            for (variant, seed), (state, state_anchor) in built.items():
                local = transform_cameras(batched, torch.linalg.inv(state_anchor))
                pred = renderer(state, local, ("depth",))["depth"][0, 0, 0].numpy()
                arrays[f"{variant}_{seed}"] = pred.astype(np.float64)
            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."""
    config = read(v11 / "training_contract.json")
    manifest_path = Path(config["eval_v3"]["manifest"])
    manifest = read(manifest_path)
    seal = read(v11 / "raw" / "eval_v3" / "prediction_seal.json")
    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, [], "eval_v3")
        for (role, qid), arrays in mine.items():
            sealed = dict(np.load(seal["predictions"][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"])]
            pairs += [(arrays[k], sealed[k]) for k in sealed if k[:2] in ("C0", "C1")]
            pairs += [(arrays[k], filled[k]) for k in ("REPROJ_HARMONIC", "REPROJ_TELEA")]
            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, v11=V11, inpaint=INPAINT, eval_v4=EVAL_V4):
    root = Path(root).resolve()
    lock_ = verify(root, v11)
    out = root / "raw"
    if (out / "prediction_seal.json").exists():
        raise FileExistsError("EVAL-V4 predictions are sealed once")
    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 = Path(lock_["input_sha256"]["eval_v4_manifest"]["path"])
    manifest = read(manifest_path)
    if any(r["split"] != "EVAL_V4" for r in manifest["scenes"]):
        raise PermissionError("EVAL-V4 records only")
    replayed = replay(models, constant, v11, inpaint)
    access, entries, states = [], {}, {}
    for record in manifest["scenes"]:
        sid = record["scene_id"]
        scene = predict(
            record, manifest, manifest_path.parent, models, constant, access, "eval_v4", 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)}
    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,
            "near_px": config["eval_v3"]["near_px"],
            "reprojection_fill_constant_m": constant,
            "replay_on_eval_v3": replayed,
        },
    )
    print("EVAL_V4_PREDICTIONS_SEALED", len(entries), json.dumps(replayed), flush=True)


def score(root):
    root = Path(root).resolve()
    lock_ = read(root / "lock.json")
    seal_file = read(root / "raw" / "prediction_seal.json")
    manifest = read(lock_["input_sha256"]["eval_v4_manifest"]["path"])
    seeds = read(lock_["input_sha256"]["v11_contract"]["path"])["seeds"]
    rows, access = [], []
    for record in manifest["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}
                predictions = {
                    ("REPROJ_NN", 0): arrays["REPROJ_NN"],
                    ("REPROJ_HARMONIC", 0): arrays["REPROJ_HARMONIC"],
                    ("REPROJ_TELEA", 0): arrays["REPROJ_TELEA"],
                }
                for variant in PRIMARY_PAIR:
                    for seed in seeds:
                        carrier = arrays[f"{variant}_{seed}"]
                        predictions[variant, seed] = carrier
                        predictions[f"FILL8_{variant}", seed] = np.where(
                            near, arrays["REPROJ_NN"], carrier
                        )
                        predictions[f"HFILL8_{variant}", seed] = np.where(
                            near, arrays["REPROJ_HARMONIC"], carrier
                        )
                        predictions[f"TFILL8_{variant}", seed] = np.where(
                            near, arrays["REPROJ_TELEA"], carrier
                        )
                for (method, seed), value in predictions.items():
                    row = {
                        "cohort": "EVAL_V4",
                        "scene_id": sid,
                        "role": role,
                        "query_id": qid,
                        "method": method,
                        "seed": seed,
                    }
                    for region, mask in masks.items():
                        row[f"absrel_{region}"] = (
                            float(np.mean(np.abs(value[mask] - gt[mask]) / gt[mask]))
                            if mask.any()
                            else None
                        )
                    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("EVAL_V4_SCORED", len(rows), flush=True)
    return rows


def inpaint_block(rows):
    """INPAINT-V1's per-cohort analysis block (same contrasts, aggregation and labels)."""
    v11, label = host(), inpainting().geometry().label
    methods = sorted({r["method"] for r in rows})

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

    block = {
        "absolute_absrel": {
            m: {region: v11.summary(values(m, region)) for region in REGIONS} for m in methods
        },
        "contrasts": {},
        "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"])
        ),
    }
    for name, (a, ra, b, rb) in CONTRASTS.items():
        stats = v11.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["strict_gate"], entry["strict_checks"] = v11.gate(stats)
        block["contrasts"][name] = entry
    return block


def analyze(root):
    root = Path(root).resolve()
    rows_path = root / "raw" / "query_rows.json"
    rows = read(rows_path)
    block = inpaint_block(rows)
    v11_rows = [r for r in rows if r["method"] in {"REPROJ_NN", "C0", "C1", "FILL8_C0", "FILL8_C1"}]
    v11_gates = host().analyze_rows(v11_rows)
    primary = block["contrasts"]["HARMONIC_HYBRID_GAIN"]
    result = {
        "eval_v4": block,
        "v11_gates_on_eval_v4": v11_gates,
        "PRIMARY_LABEL": primary.get("label", "UNDEFINED"),
        "PRIMARY_STRICT_GATE": primary.get("strict_gate", "UNDEFINED"),
        "REPLICATION_BRANCH": {"ABOVE": "A", "NOT_DISTINGUISHABLE": "B", "BELOW": "C"}.get(
            primary.get("label"), "UNDEFINED"
        ),
        "V11_BRANCH_ON_EVAL_V4": v11_gates["INTERPRETATION_BRANCH"],
        "replay_on_eval_v3": read(root / "raw" / "prediction_seal.json")["replay_on_eval_v3"],
        "input_sha256": {str(rows_path): sha(rows_path)},
        "final_holdout_opened": False,
    }
    write(root / "replication_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):
    block, gates = result["eval_v4"], result["v11_gates_on_eval_v4"]
    lines = [
        f"# {EXPERIMENT}",
        "",
        f"在从未读取过的 EVAL-V4({block['n_scenes']} 个场景,FAR 像素占 {block['far_pixel_fraction']:.1%})上复现 V11 与 INPAINT-V1。V11 已选中的 checkpoint、区域定义、补洞方法与统计规则全部冻结,由 V11 出结果之前写定的计划规定。不训练。",
        "",
        f"- 主对比:AbsRel(REPROJ_HARMONIC) − AbsRel(HFILL8_C1) = {fmt(block['contrasts']['HARMONIC_HYBRID_GAIN']['gain'])};标签 {result['PRIMARY_LABEL']},严格 gate {result['PRIMARY_STRICT_GATE']},复现分支 {result['REPLICATION_BRANCH']}。",
        f"- V11 的 COMPLETION_GAIN = {fmt(gates['COMPLETION_GAIN'])}({gates['statuses']['COMPLETION_STATUS']});HYBRID_GAIN = {fmt(gates['HYBRID_GAIN'])}({gates['statuses']['HYBRID_STATUS']});V11 分支 {result['V11_BRANCH_ON_EVAL_V4']}。",
        f"- 流程复现:在 EVAL-V3 的前 {result['replay_on_eval_v3']['scenes']} 个场景上重算的 {result['replay_on_eval_v3']['arrays_compared']} 个数组,与 V11 和 INPAINT-V1 的封存文件最大差 {result['replay_on_eval_v3']['max_abs_deviation']:.1e}。",
        "",
        "| 方法 | ALL | NEAR | FAR |",
        "|---|---:|---:|---:|",
    ]
    for method in (
        "REPROJ_NN",
        "REPROJ_HARMONIC",
        "REPROJ_TELEA",
        "C0",
        "C1",
        "FILL8_C1",
        "HFILL8_C0",
        "HFILL8_C1",
        "TFILL8_C1",
    ):
        cells = [block["absolute_absrel"].get(method, {}).get(r) for r in REGIONS]
        lines.append(
            f"| {method} | "
            + " | ".join("—" if not c else f"{c['mean']:.4f}" for c in cells)
            + " |"
        )
    lines += ["", "| 对比 | 定义 | 差值 [95% CI],场景 好/平/差 | 标签 |", "|---|---|---|---|"]
    for name, entry in block["contrasts"].items():
        lines.append(
            f"| {name} | {entry['definition']} | {fmt(entry['gain'])} | {entry.get('label', 'UNDEFINED')} |"
        )
    lines += [
        "",
        "说明:EVAL-V4 与 EVAL-V3 一样是机制队列(按场景和源资产与训练集及 EVAL-V3 不相交,可能共享 volume),不是资格验证;受保护的 final holdout 未打开。",
    ]
    (root / "README.md").write_text("\n".join(lines) + "\n")
    summary = [
        "RGBD REPLICATION EVAL-V4",
        f"PRIMARY_LABEL={result['PRIMARY_LABEL']}",
        f"PRIMARY_STRICT_GATE={result['PRIMARY_STRICT_GATE']}",
        f"REPLICATION_BRANCH={result['REPLICATION_BRANCH']}",
        f"V11_BRANCH_ON_EVAL_V4={result['V11_BRANCH_ON_EVAL_V4']}",
        f"COMPLETION_STATUS={gates['statuses']['COMPLETION_STATUS']}",
        f"HYBRID_STATUS={gates['statuses']['HYBRID_STATUS']}",
        f"N_SCENES={block['n_scenes']}",
    ]
    for name, entry in block["contrasts"].items():
        if entry["gain"]:
            summary.append(
                f"{name}={entry['gain']['mean']:.6f} CI={entry['gain']['ci95']} LABEL={entry['label']}"
            )
    summary += [
        f"REPLAY_MAX_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()