Source: scripts/prepare_geometry_carrier_data.py · Plain text

scripts/prepare_geometry_carrier_data.py

#!/usr/bin/env python3
"""Lock and materialize 24 TRAIN + 8 exposed DEV scenes, without model forward."""

from __future__ import annotations

import argparse
import hashlib
import itertools
import json
import time
import zipfile
import zlib
from pathlib import Path

import h5py
import numpy as np
import requests
import torch
from PIL import Image

from mcss.data.hypersim import convert_hypersim_pose
from mcss.mechanism_pilot.calibration import calibrated_intrinsics, published_scene_metadata
from mcss.mechanism_pilot.data_prep import BoundedRangeReader, check_geometry, resize_rgb_depth
from mcss.mechanism_pilot.optimization_bounds_contracts import (
    ContextCameraBundle,
    FrozenTrainingPrior,
    frozen_gt_free_bounds,
)
from mcss.mechanism_pilot.spatial import observed_camera_support
from mcss.mechanism_pilot.support_redesign_data import safe_source_assets
from mcss.mechanism_pilot.unseen_data import asset_identity, csv_rows, frame_quality, select_members
from mcss.types import Cameras

OLD_THREE = ("ai_001_001", "ai_002_001", "ai_003_001")


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 select_cohort(old_ids, partition_rows, assets, allowed, trajectories, protected, count=12):
    """Metadata-only selection, prioritizing documented old100 membership."""
    if len(old_ids) != 20 or not set(OLD_THREE) <= set(old_ids):
        raise ValueError("Exactly twenty old exposed scenes including bounds-prior TRAIN3 required")
    if set(old_ids) & set(protected):
        raise PermissionError("Protected physical scene cannot become TRAIN or DEV")
    identities = {asset_identity(assets[s]) for s in old_ids}
    if len(identities) != len(old_ids):
        raise ValueError("Duplicated old physical asset")
    blocked_assets = identities | {asset_identity(assets[s]) for s in protected}
    volumes = {s.split("_")[1] for s in old_ids}
    selected = []
    for row in sorted(
        partition_rows, key=lambda r: (r["previously_observed"] != "True", r["scene_name"])
    ):
        sid = row["scene_name"]
        if (
            sid in protected
            or sid not in assets
            or row["protocol_partition"] != "train"
            or sid.split("_")[1] in volumes
            or asset_identity(assets[sid]) in blocked_assets
            or len(allowed.get(sid, [])) < 16
            or "BAD" in trajectories.get(sid + "_cam_00", {}).get("Scene type", "BAD").upper()
        ):
            continue
        selected.append(sid)
        volumes.add(sid.split("_")[1])
        blocked_assets.add(asset_identity(assets[sid]))
        if len(selected) == count:
            break
    if len(selected) != count:
        raise ValueError("Insufficient metadata-eligible candidates; no replacement search")
    dev = sorted(
        (s for s in old_ids if s not in OLD_THREE),
        key=lambda s: hashlib.sha256(f"20260928:{s}".encode()).hexdigest(),
    )[:8]
    train = sorted(set(old_ids) - set(dev)) + selected
    if {s.split("_")[1] for s in train} & {s.split("_")[1] for s in dev}:
        raise ValueError("TRAIN/DEV volume overlap")
    return train, dev, selected


def camera_only_roles(scene_id, frames, quality, prior):
    """First lexicographically feasible disjoint pairs; no score/depth input to support."""
    valid = sorted(f["frame_id"] for f in frames if quality[f["frame_id"]]["eligible"])
    if len(valid) < 7:
        raise ValueError("Fewer than seven valid frames in frozen first16; no substitutions")
    anchor, queries = valid[0], valid[-2:]
    rows = {f["frame_id"]: f for f in frames}
    ids = torch.div(torch.arange(128) * (16**3 - 1), 127, rounding_mode="floor")
    fractions = torch.stack(
        ((ids % 16 + 0.5) / 16, (ids // 16 % 16 + 0.5) / 16, (ids // 256 + 0.5) / 16), -1
    ).double()
    options = []
    for pair in itertools.combinations([i for i in valid if i != anchor and i not in queries], 2):
        frame_ids = (anchor, *pair)
        cameras = tuple(
            Cameras(
                torch.tensor(rows[i]["intrinsics"], dtype=torch.float64),
                torch.tensor(rows[i]["c2w"], dtype=torch.float64),
                (128, 160),
            )
            for i in frame_ids
        )
        bundle = ContextCameraBundle(scene_id, "A", frame_ids, cameras, cameras[0].c2w.clone())
        bounds = frozen_gt_free_bounds(bundle, prior)
        points = bounds[0] + fractions * (bounds[1] - bounds[0])
        support = observed_camera_support(points, cameras)["supported_candidates"]
        if support >= 2:
            options.append((pair, support))
    for a, b in itertools.combinations(options, 2):
        if set(a[0]) & set(b[0]):
            continue
        return {
            "context_a": [anchor, *a[0]],
            "context_b": [anchor, *b[0]],
            "primary_query": queries,
            "query": queries,
            "secondary_query": [],
            "stream": [],
            "anchor": anchor,
        }, {
            "status": "ELIGIBLE",
            "A_supported_candidates": a[1],
            "B_supported_candidates": b[1],
            "meaning": "Two-frustum overlap only; not occlusion/true surface visibility",
            "grid": 16,
            "tokens": 128,
        }
    raise ValueError("No two disjoint context pairs with >=2 camera-supported candidates")


def source_paths(repo):
    static = repo / "outputs/EXP-3D-STATIC-CARRIER-ATTRIBUTION-V1"
    return {
        "assets": static / "metadata/upstream_dataset_config.py",
        "partitions": repo / "configs/hypersim_er_partitions.csv",
        "old100": repo / "configs/hypersim_subset_v1.csv",
        "official_split": static / "rgb_audit/official_metadata/metadata_images_split_scene_v1.csv",
        "trajectories": static / "rgb_audit/official_metadata/metadata_camera_trajectories.csv",
        "old17": repo / "outputs/EXP-3D-DIRECT-STATE-CAPACITY-V1/scene_manifest.json",
        "old3": repo / "outputs/EXP-3D-20260927-small-training-v1/data/manifest.json",
        "b_final_lock": repo
        / "outputs/EXP-3D-20260927-centered-training-1000a-600b-v1/training/lock.json",
        "historical_audit": repo
        / "outputs/EXP-3D-SUPPORT-BOTTLENECK-REDESIGN-V1/data/historical_exposure_audit.json",
        "calibration": repo
        / "outputs/EXP-3D-20260927-training-preparation-v1/metadata/metadata_camera_parameters.csv",
        "bounds": repo / "outputs/EXP-3D-16G-OPTIMIZATION-BOUNDS-V1/bounds_contract.json",
        "preparation_source": Path(__file__).resolve(),
    }


def prepare(repo, output, lock_only=False, source_validity_revision=False):
    repo, output = Path(repo).resolve(), Path(output).resolve()
    output.mkdir(parents=True, exist_ok=True)
    paths = source_paths(repo)
    old17 = json.loads(paths["old17"].read_text())
    old3 = json.loads(paths["old3"].read_text())
    old_records = old3["scenes"] + old17["scenes"]
    assets, partitions = safe_source_assets(paths["assets"]), csv_rows(paths["partitions"])
    protected = sorted(
        set(old17["FINAL_HOLDOUT_PROHIBITED"])
        | {
            r["scene_name"]
            for r in partitions
            if r["protocol_partition"] in ("final_holdout", "diagnostic_test")
        }
    )
    allowed = {}
    for r in csv_rows(paths["official_split"]):
        if (
            r["camera_name"] == "cam_00"
            and r["included_in_public_release"] == "True"
            and r["split_partition_name"] == "train"
        ):
            allowed.setdefault(r["scene_name"], []).append(int(r["frame_id"]))
    trajectories = {r["Animation"]: r for r in csv_rows(paths["trajectories"])}
    train, dev, new = select_cohort(
        [s["scene_id"] for s in old_records],
        partitions,
        assets,
        allowed,
        trajectories,
        protected,
        count=26 if source_validity_revision else 12,
    )
    prior_data = json.loads(paths["bounds"].read_text())
    prior = FrozenTrainingPrior(
        prior_data["near_m"], prior_data["far_m"], prior_data["source_sha256"]
    )
    lock = {
        "schema": "mcss.geometry_carrier.cohort.v1",
        "TRAIN": train,
        "DEV": dev,
        "new_candidates": new,
        "FRESH_QUALIFICATION": [],
        "FRESH_QUALIFICATION_STATUS": "BLOCKED_INDEPENDENCE_UNRESOLVED",
        "protected_scene_ids": protected,
        "assets": {s: assets[s] for s in train + dev},
        "previously_observed": {
            r["scene_name"]: r["previously_observed"]
            for r in partitions
            if r["scene_name"] in train + dev
        },
        "source_sha256": {str(p): sha(p) for p in paths.values()},
        "selection_seed": 20260928,
        "dev_rule": "sha256('20260928:'+scene_id), lowest8 among exposed17; old3 TRAIN",
        "selection_rule": (
            "historical100 first, then lexicographic; TRAIN cam00 >=16 publicly included"
            " frames, nonBAD; new volume unique vs old20 and new peers; protected "
            "scene/asset blocked; no replacement"
        ),
        "new_role_rule": (
            "first valid anchor, last2 valid query; earliest feasible disjoint context "
            "pairs with >=2/128 frustum support; grid16 GTfree prior bounds; no "
            "score/depth geometry selection"
        ),
        "old_role_erratum": (
            "Old20 context roles unchanged. Old3 primary_query=[12,13], legacy "
            "query=[12,13,14,15] retained;14/15 unused. Old17 primary_query unchanged. "
            "No old file overwritten."
        ),
        "quality_rule": (
            "first16 official included train frames; finite RGB, nearblack<=.999, "
            "positivefinite native depth>=.95; old20 retained unchanged"
        ),
        "historical_independence_limit": (
            "Old365 scene-level execution logs unrecovered; absence from old100 is not "
            "virginity. All selected scenes exposed or historical-training-pool, not "
            "fresh. Official source archive/asset defines physical identity proxy; "
            "cross-asset vendor reuse cannot be excluded."
        ),
        "data_budget_bytes": 2 * 1024**3,
        "model_forward_run": False,
        "protected_media_read": False,
    }
    if source_validity_revision:
        previous = output / "candidate_lock_v2.json"
        failed_path = output / "scene_failures_v2.json"
        failed = json.loads(failed_path.read_text())
        lock["schema"] = "mcss.geometry_carrier.cohort.v3"
        lock["source_validity_amendment"] = {
            "previous_lock_sha256": sha(previous),
            "original_candidates": new[:12],
            "excluded_original": sorted(failed),
            "excluded_evidence_sha256": sha(failed_path),
            "supplement_candidates_locked": new[12:],
            "supplement_budget": 14,
            "selection": (
                "Retain original5 valid; first7 source/camera-valid supplementary scenes "
                "in locked order. No model scores. Stop at 7, no more than14 candidates."
            ),
            "camera_validity": (
                "For all used context/query frames: max(abs(R.T@R-I))<=0.001 and "
                "determinant>0; never orthonormalize or alter renderer."
            ),
            "original_geometry_p95_m": 0.01,
            "TRAIN_field_is_candidate_pool_not_final_split": True,
        }
    lockpath = output / (
        "candidate_lock_v3.json" if source_validity_revision else "candidate_lock.json"
    )
    if lockpath.exists():
        if json.loads(lockpath.read_text()) != lock:
            raise ValueError(
                "Existing candidate lock differs; preserve prior attempt, refuse overwrite"
            )
    else:
        write(lockpath, lock)
    if lock_only:
        return lock
    if (output / "manifest.json").exists():
        raise FileExistsError("Completed manifest exists; no overwrite")
    network = [0]
    plans, sessions, readers, archives = [], [], [], []
    try:
        for sid in new:
            session = requests.Session()
            sessions.append(session)
            url = f"https://docs-assets.developer.apple.com/ml-research/datasets/hypersim/v1/scenes/{sid}.zip"
            reader = BoundedRangeReader(url, session)
            reader.shared_budget = network
            readers.append(reader)
            archive = zipfile.ZipFile(reader)
            archives.append(archive)
            plan = select_members(sid, archive.infolist(), allowed[sid])
            plan.update(url=url, etag=reader.etag, archive_bytes=reader.size)
            plans.append(plan)
            print("MEMBER_PLAN", sid, flush=True)
        if (
            sum(p["compressed_bytes"] + p["uncompressed_bytes"] for p in plans)
            > lock["data_budget_bytes"]
        ):
            raise ValueError("Selected compressed+uncompressed data exceeds frozen budget")
        dataplan = {"candidate_lock_sha256": sha(lockpath), "scenes": plans}
        dp = output / ("data_lock_v3.json" if source_validity_revision else "data_lock.json")
        if dp.exists() and json.loads(dp.read_text()) != dataplan:
            raise ValueError("Remote metadata changed; refuse resumptive data modification")
        write(dp, dataplan)
        calibration = published_scene_metadata(paths["calibration"])
        records, hashes, qualities, geometry, feasibility = [], {}, {}, [], {}
        scene_failures = {}
        with (output / "access.jsonl").open("a") as log:

            def access(sid, path, operation):
                if sid in protected:
                    raise PermissionError("Protected scene access prohibited")
                log.write(
                    json.dumps(
                        {
                            "time": time.time(),
                            "scene_id": sid,
                            "path": str(path),
                            "operation": operation,
                            "purpose": "TRAIN_DEV_DATA_PREPARATION_NO_FORWARD",
                        }
                    )
                    + "\n"
                )
                log.flush()

            for record in old_records:
                sid = record["scene_id"]
                item = json.loads(json.dumps(record))
                item.update(
                    split="DEV" if sid in dev else "TRAIN",
                    historically_exposed=True,
                    physical_scene_id="|".join(asset_identity(assets[sid])),
                )
                item["roles"]["primary_query"] = (
                    [12, 13] if sid in OLD_THREE else item["roles"]["primary_query"]
                )
                item["roles"]["anchor"] = item["roles"]["context_a"][0]
                for frame in item["frames"]:
                    for kind in ("rgb", "depth"):
                        path = Path(frame[kind]).resolve()
                        access(sid, path, "hash_existing_prepared_" + kind)
                        hashes[str(path)] = sha(path)
                        frame[kind] = str(path)
                records.append(item)
            for plan, archive in zip(plans, archives, strict=True):
                sid = plan["scene_id"]
                if source_validity_revision and sid in failed:
                    scene_failures[sid] = failed[sid]
                    continue
                if source_validity_revision and len(records) == 32:
                    break
                for member in plan["member_metadata"]:
                    name = member["name"]
                    dest = output / "raw" / name
                    access(sid, name, "read_or_download_locked_member")
                    if dest.exists():
                        content = dest.read_bytes()
                        if (
                            len(content) != member["bytes"]
                            or zlib.crc32(content) != member["crc32"]
                        ):
                            raise ValueError("Existing download failed CRC; preserve and stop")
                    else:
                        dest.parent.mkdir(parents=True, exist_ok=True)
                        content = archive.read(name)
                        dest.write_bytes(content)
                    hashes[str(dest)] = sha(dest)
                raw = output / "raw" / sid
                units = {
                    r["parameter_name"]: r["parameter_value"]
                    for r in csv_rows(raw / "_detail/metadata_scene.csv")
                }
                scale = float(units["meters_per_asset_unit"])
                if not np.isclose(
                    scale, float(calibration[sid]["settings_units_info_meters_scale"]), rtol=1e-6
                ):
                    raise ValueError("Calibration scale mismatch")

                def hdf(path, sid=sid):
                    access(sid, path, "decode_hdf5")
                    with h5py.File(path) as handle:
                        return np.array(handle["dataset"])

                ids = hdf(raw / "_detail/cam_00/camera_keyframe_frame_indices.hdf5").reshape(-1)
                poses = hdf(raw / "_detail/cam_00/camera_keyframe_positions.hdf5")
                rots = hdf(raw / "_detail/cam_00/camera_keyframe_orientations.hdf5")
                mapping = {int(fid): j for j, fid in enumerate(ids)}
                frames, quality = [], {}
                prepared = output / "prepared" / sid
                prepared.mkdir(parents=True, exist_ok=True)
                for fid in plan["frame_ids"]:
                    rgbpath = raw / f"images/scene_cam_00_final_preview/frame.{fid:04d}.color.jpg"
                    access(sid, rgbpath, "decode_RGB")
                    with Image.open(rgbpath) as im:
                        rgb = np.asarray(im.convert("RGB"))
                    depth = hdf(
                        raw / f"images/scene_cam_00_geometry_hdf5/frame.{fid:04d}.depth_meters.hdf5"
                    )
                    if rgb.shape[:2] != depth.shape:
                        raise ValueError("RGB/depth dimensions differ")
                    quality[fid] = frame_quality(rgb, depth)
                    if source_validity_revision:
                        rotation = rots[mapping[fid]]
                        orth_error = float(np.max(np.abs(rotation.T @ rotation - np.eye(3))))
                        determinant = float(np.linalg.det(rotation))
                        quality[fid]["camera_orthogonality_error"] = orth_error
                        quality[fid]["camera_determinant"] = determinant
                        quality[fid]["eligible"] = (
                            quality[fid]["eligible"] and orth_error <= 0.001 and determinant > 0
                        )
                    pose = convert_hypersim_pose(
                        rots[mapping[fid]], poses[mapping[fid]], meters_per_asset_unit=scale
                    )
                    if fid == plan["frame_ids"][0]:
                        position = hdf(
                            raw / f"images/scene_cam_00_geometry_hdf5/frame.{fid:04d}.position.hdf5"
                        )
                        try:
                            geometry.append(
                                check_geometry(
                                    sid,
                                    depth,
                                    position,
                                    pose,
                                    calibrated_intrinsics(calibration[sid], depth.shape),
                                    scale,
                                )
                            )
                        except ValueError as exc:
                            scene_failures[sid] = {
                                "status": "INELIGIBLE_SOURCE_GEOMETRY",
                                "error": str(exc),
                            }
                            geometry.append({"scene_id": sid, **scene_failures[sid]})
                        write(output / "geometry_checks.json", geometry)
                        write(output / "scene_failures.json", scene_failures)
                    rgb_small, depth_small = resize_rgb_depth(rgb, depth)
                    rgbout, depthout = prepared / f"{fid:04d}.png", prepared / f"{fid:04d}.npy"
                    Image.fromarray(rgb_small).save(rgbout)
                    np.save(depthout, depth_small)
                    hashes[str(rgbout)], hashes[str(depthout)] = sha(rgbout), sha(depthout)
                    frames.append(
                        {
                            "frame_id": fid,
                            "rgb": str(rgbout),
                            "depth": str(depthout),
                            "intrinsics": calibrated_intrinsics(
                                calibration[sid], (128, 160)
                            ).tolist(),
                            "c2w": pose.tolist(),
                        }
                    )
                qualities[sid] = quality
                write(output / "frame_quality.json", qualities)
                if sid in scene_failures:
                    print("INELIGIBLE", sid, scene_failures[sid], flush=True)
                    continue
                try:
                    roles, diagnostic = camera_only_roles(sid, frames, quality, prior)
                except ValueError as exc:
                    scene_failures[sid] = {
                        "status": "INELIGIBLE_FRAME_ROLES",
                        "error": str(exc),
                    }
                    write(output / "scene_failures.json", scene_failures)
                    print("INELIGIBLE", sid, scene_failures[sid], flush=True)
                    continue
                feasibility[sid] = diagnostic
                records.append(
                    {
                        "scene_id": sid,
                        "split": "TRAIN",
                        "historically_exposed": True,
                        "historical_exposure_basis": (
                            "Old100 pool membership or historical365 train pool; precise "
                            "executed model "
                            "exposure unresolved"
                        ),
                        "physical_scene_id": "|".join(asset_identity(assets[sid])),
                        "roles": roles,
                        "frames": frames,
                    }
                )
                write(
                    output / "progress.json",
                    {
                        "completed_scene_ids": [s["scene_id"] for s in records],
                        "support": feasibility,
                    },
                )
                print("PREPARED", sid, flush=True)
        manifest = {
            "schema": "mcss.geometry_carrier.data.v1",
            "image_size": [128, 160],
            "depth_semantics": "ray_distance_meters",
            "scenes": records,
            "candidate_lock_sha256": sha(lockpath),
            "candidate_lock_path": str(lockpath),
            "FRESH_QUALIFICATION_STATUS": "BLOCKED_INDEPENDENCE_UNRESOLVED",
            "fresh_status": "BLOCKED_INDEPENDENCE_UNRESOLVED",
            "protected_scene_ids": protected,
        }
        if len(records) != 32 or sum(s["split"] == "TRAIN" for s in records) != 24:
            manifest["status"] = "BLOCKED_INSUFFICIENT_VALID_LOCKED_SCENES"
            manifest["scene_failures"] = scene_failures
            write(output / "diagnostic_manifest.json", manifest)
            write(output / "hashes.json", hashes)
            write(output / "support_feasibility.json", feasibility)
            write(
                output / "preparation_integrity.json",
                {
                    "status": "BLOCKED_INSUFFICIENT_VALID_LOCKED_SCENES",
                    "TRAIN": sum(s["split"] == "TRAIN" for s in records),
                    "DEV": 8,
                    "fresh": 0,
                    "protected_media_read": False,
                    "model_forward_run": False,
                    "failures": scene_failures,
                    "candidate_replacement": False,
                },
            )
            return manifest
        write(output / "manifest.json", manifest)
        write(output / "hashes.json", hashes)
        write(output / "geometry_checks.json", geometry)
        write(output / "support_feasibility.json", feasibility)
        write(output / "frame_role_lock.json", {s["scene_id"]: s["roles"] for s in records})
        write(
            output / "preparation_integrity.json",
            {
                "status": "PASS",
                "TRAIN": 24,
                "DEV": 8,
                "fresh": 0,
                "protected_media_read": False,
                "model_forward_run": False,
                "manifest_sha256": sha(output / "manifest.json"),
                "candidate_lock_sha256": sha(lockpath),
            },
        )
        return manifest
    except Exception as exc:
        write(
            output / "failure.json",
            {
                "status": "BLOCKED_DATA_PREPARATION",
                "error_type": type(exc).__name__,
                "error": str(exc),
                "candidate_replacement": False,
                "completed_data_preserved": True,
            },
        )
        raise
    finally:
        write(
            output / "download_cost.json",
            {
                "network_bytes_this_attempt": network[0],
                "full_archives_downloaded": False,
                "budget_bytes": 2 * 1024**3,
            },
        )
        for archive in archives:
            archive.close()
        for reader in readers:
            reader.close()
        for session in sessions:
            session.close()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1])
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--lock-only", action="store_true")
    parser.add_argument("--source-validity-revision", action="store_true")
    args = parser.parse_args()
    prepare(args.repo, args.output, args.lock_only, args.source_validity_revision)