Source: src/mcss/geometry_supplier.py · Plain text

src/mcss/geometry_supplier.py

"""Label-free geometry supplier contracts and structured atomic caches."""

from __future__ import annotations

import hashlib
import json
import os
import shutil
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Self

import numpy as np
import torch
from torch import Tensor

from mcss.types import Cameras

SUPPLIER_CACHE_SCHEMA_VERSION = 2


@dataclass(frozen=True)
class CodeDependency:
    name: str
    repository: str
    revision: str
    source_sha256: str

    def __post_init__(self) -> None:
        for name in ("name", "repository", "revision"):
            value = getattr(self, name)
            if not isinstance(value, str) or not value.strip():
                raise ValueError(f"{name} must be a non-empty string")
        if not _is_sha256(self.source_sha256):
            raise ValueError("source_sha256 must be a lowercase SHA-256 digest")


@dataclass(frozen=True)
class SupplierIdentity:
    model_id: str
    model_revision: str
    code_repository: str
    code_revision: str
    dependencies: tuple[CodeDependency, ...] = ()

    def __post_init__(self) -> None:
        for name in ("model_id", "model_revision", "code_repository", "code_revision"):
            value = getattr(self, name)
            if not isinstance(value, str) or not value.strip():
                raise ValueError(f"{name} must be a non-empty string")
        if not isinstance(self.dependencies, tuple) or not all(
            isinstance(value, CodeDependency) for value in self.dependencies
        ):
            raise ValueError("dependencies must be a tuple of CodeDependency values")
        names = [value.name for value in self.dependencies]
        if len(names) != len(set(names)):
            raise ValueError("dependency names must be unique")

    def to_dict(self) -> dict[str, Any]:
        return {
            "model_id": self.model_id,
            "model_revision": self.model_revision,
            "code_repository": self.code_repository,
            "code_revision": self.code_revision,
            "dependencies": [asdict(value) for value in self.dependencies],
        }


@dataclass(frozen=True)
class SupplierRunConfig:
    memory_efficient_inference: bool = True
    minibatch_size: int = 1
    use_amp: bool = True
    amp_dtype: str = "bf16"
    apply_mask: bool = True
    mask_edges: bool = True
    apply_confidence_mask: bool = False
    use_multiview_confidence: bool = False

    def __post_init__(self) -> None:
        if self.minibatch_size < 1:
            raise ValueError("minibatch_size must be positive")
        if self.amp_dtype not in {"bf16", "fp16", "fp32"}:
            raise ValueError("amp_dtype must be bf16, fp16, or fp32")

    def inference_kwargs(self) -> dict[str, object]:
        return asdict(self)


@dataclass
class GeometrySupplierRequest:
    """The complete and deliberately label-free supplier input."""

    frame_ids: tuple[int, ...]
    rgb: Tensor
    cameras: Cameras

    def __post_init__(self) -> None:
        self.frame_ids = tuple(int(value) for value in self.frame_ids)
        if not self.frame_ids or any(value < 0 for value in self.frame_ids):
            raise ValueError("frame_ids must be non-empty nonnegative integers")
        if len(set(self.frame_ids)) != len(self.frame_ids):
            raise ValueError("frame_ids must be unique")
        if self.rgb.ndim != 4 or self.rgb.shape[1] != 3:
            raise ValueError("rgb must have shape [V, 3, H, W]")
        view_count = len(self.frame_ids)
        if self.rgb.shape[0] != view_count or self.cameras.leading_shape != (view_count,):
            raise ValueError("frame IDs, RGB, and cameras must share the view dimension")
        if tuple(self.rgb.shape[-2:]) != self.cameras.image_size:
            raise ValueError("RGB and cameras must share image size")
        if not torch.isfinite(self.rgb).all():
            raise ValueError("rgb must contain only finite values")
        if (self.rgb < 0).any() or (self.rgb > 1).any():
            raise ValueError("rgb must be normalized to [0, 1]")

    def canonical(self) -> Self:
        order = sorted(range(len(self.frame_ids)), key=self.frame_ids.__getitem__)
        if order == list(range(len(order))):
            return self
        indices = torch.tensor(order, dtype=torch.long, device=self.rgb.device)
        return type(self)(
            tuple(self.frame_ids[index] for index in order),
            self.rgb.index_select(0, indices),
            self.cameras.select_views(indices),
        )

    @property
    def input_sha256(self) -> str:
        canonical = self.canonical()
        digest = hashlib.sha256()
        digest.update(b"mcss-geometry-supplier-input-v1\0")
        digest.update(np.asarray(canonical.frame_ids, dtype="<i8").tobytes())
        for value in (
            canonical.rgb,
            canonical.cameras.intrinsics,
            canonical.cameras.c2w,
        ):
            _hash_tensor(digest, value)
        digest.update(np.asarray(canonical.cameras.image_size, dtype="<i8").tobytes())
        return digest.hexdigest()


@dataclass
class GeometrySupplierResult:
    frame_ids: tuple[int, ...]
    depth_along_ray: np.ndarray
    confidence: np.ndarray
    valid_mask: np.ndarray
    intrinsics: np.ndarray
    c2w: np.ndarray

    def __post_init__(self) -> None:
        self.frame_ids = tuple(int(value) for value in self.frame_ids)
        self.depth_along_ray = np.asarray(self.depth_along_ray, dtype=np.float32)
        self.confidence = np.asarray(self.confidence, dtype=np.float32)
        self.valid_mask = np.asarray(self.valid_mask, dtype=np.bool_)
        self.intrinsics = np.asarray(self.intrinsics, dtype=np.float32)
        self.c2w = np.asarray(self.c2w, dtype=np.float32)
        if not self.frame_ids or len(set(self.frame_ids)) != len(self.frame_ids):
            raise ValueError("result frame_ids must be non-empty and unique")
        view_count = len(self.frame_ids)
        reference_shape = self.depth_along_ray.shape
        if len(reference_shape) != 3 or reference_shape[0] != view_count:
            raise ValueError("depth_along_ray must have shape [V, H, W]")
        if self.confidence.shape != reference_shape or self.valid_mask.shape != reference_shape:
            raise ValueError("confidence and valid_mask shape must match depth_along_ray")
        if self.intrinsics.shape != (view_count, 3, 3):
            raise ValueError("intrinsics shape must be [V, 3, 3]")
        if self.c2w.shape != (view_count, 4, 4):
            raise ValueError("c2w shape must be [V, 4, 4]")
        finite = (
            np.isfinite(self.depth_along_ray).all()
            and np.isfinite(self.confidence).all()
            and np.isfinite(self.intrinsics).all()
            and np.isfinite(self.c2w).all()
        )
        if not finite:
            raise ValueError("supplier arrays must contain only finite values")
        if np.any(self.intrinsics[:, (0, 1), (0, 1)] <= 0):
            raise ValueError("supplier intrinsics require positive focal lengths")
        if not np.allclose(self.c2w[:, 3], np.asarray([0.0, 0.0, 0.0, 1.0]), atol=1e-5):
            raise ValueError("supplier c2w requires homogeneous final rows")

    @property
    def image_size(self) -> tuple[int, int]:
        return int(self.depth_along_ray.shape[1]), int(self.depth_along_ray.shape[2])

    def canonical(self) -> Self:
        order = np.argsort(np.asarray(self.frame_ids), kind="stable")
        if np.array_equal(order, np.arange(len(order))):
            return self
        return type(self)(
            tuple(self.frame_ids[int(index)] for index in order),
            self.depth_along_ray[order],
            self.confidence[order],
            self.valid_mask[order],
            self.intrinsics[order],
            self.c2w[order],
        )


def write_supplier_cache(
    destination: str | Path,
    result: GeometrySupplierResult,
    identity: SupplierIdentity,
    input_sha256: str,
    run_config: SupplierRunConfig,
) -> Path:
    """Write an overwrite-refusing JSON+NPZ cache through an atomic directory rename."""

    target = Path(destination).resolve()
    if target.exists():
        raise FileExistsError(f"refusing to overwrite supplier cache: {target}")
    if not _is_sha256(input_sha256):
        raise ValueError("input_sha256 must be a lowercase SHA-256 digest")
    target.parent.mkdir(parents=True, exist_ok=True)
    temporary = Path(
        tempfile.mkdtemp(prefix=f".{target.name}.tmp-", dir=str(target.parent))
    )
    canonical = result.canonical()
    try:
        arrays_path = temporary / "arrays.npz"
        np.savez_compressed(
            arrays_path,
            frame_ids=np.asarray(canonical.frame_ids, dtype=np.int64),
            depth_along_ray=canonical.depth_along_ray,
            confidence=canonical.confidence,
            valid_mask=canonical.valid_mask,
            intrinsics=canonical.intrinsics,
            c2w=canonical.c2w,
        )
        metadata = {
            "schema_version": SUPPLIER_CACHE_SCHEMA_VERSION,
            "identity": identity.to_dict(),
            "run_config": asdict(run_config),
            "input_sha256": input_sha256,
            "frame_ids": list(canonical.frame_ids),
            "arrays_file": arrays_path.name,
            "arrays_sha256": _sha256_file(arrays_path),
            "arrays": {
                name: {"shape": list(value.shape), "dtype": str(value.dtype)}
                for name, value in _result_arrays(canonical).items()
            },
        }
        (temporary / "metadata.json").write_text(
            json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8"
        )
        os.replace(temporary, target)
    except BaseException:
        shutil.rmtree(temporary, ignore_errors=True)
        raise
    return target


def read_supplier_cache(
    source: str | Path,
) -> tuple[GeometrySupplierResult, dict[str, Any]]:
    root = Path(source).resolve()
    metadata_path = root / "metadata.json"
    if not metadata_path.is_file():
        raise FileNotFoundError(metadata_path)
    metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
    if (
        not isinstance(metadata, dict)
        or metadata.get("schema_version") != SUPPLIER_CACHE_SCHEMA_VERSION
    ):
        raise ValueError("supplier cache metadata has an unsupported schema")
    arrays_path = root / str(metadata.get("arrays_file", ""))
    if not arrays_path.is_file():
        raise FileNotFoundError(arrays_path)
    if _sha256_file(arrays_path) != metadata.get("arrays_sha256"):
        raise ValueError("supplier cache arrays hash does not match metadata")
    with np.load(arrays_path, allow_pickle=False) as arrays:
        required = {
            "frame_ids",
            "depth_along_ray",
            "confidence",
            "valid_mask",
            "intrinsics",
            "c2w",
        }
        if set(arrays.files) != required:
            raise ValueError("supplier cache array keys do not match schema")
        frame_ids = tuple(int(value) for value in arrays["frame_ids"].tolist())
        result = GeometrySupplierResult(
            frame_ids,
            arrays["depth_along_ray"].copy(),
            arrays["confidence"].copy(),
            arrays["valid_mask"].copy(),
            arrays["intrinsics"].copy(),
            arrays["c2w"].copy(),
        )
    if list(frame_ids) != metadata.get("frame_ids"):
        raise ValueError("supplier cache metadata frame IDs do not match arrays")
    expected_schema = metadata.get("arrays")
    actual_schema = {
        name: {"shape": list(value.shape), "dtype": str(value.dtype)}
        for name, value in _result_arrays(result).items()
    }
    if expected_schema != actual_schema:
        raise ValueError("supplier cache metadata array schema does not match arrays")
    return result, metadata


def _result_arrays(result: GeometrySupplierResult) -> dict[str, np.ndarray]:
    return {
        "depth_along_ray": result.depth_along_ray,
        "confidence": result.confidence,
        "valid_mask": result.valid_mask,
        "intrinsics": result.intrinsics,
        "c2w": result.c2w,
    }


def _hash_tensor(digest: Any, value: Tensor) -> None:
    array = value.detach().cpu().contiguous().numpy()
    digest.update(str(array.dtype).encode("ascii"))
    digest.update(np.asarray(array.shape, dtype="<i8").tobytes())
    digest.update(array.tobytes())


def _sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def _is_sha256(value: str) -> bool:
    return len(value) == 64 and all(character in "0123456789abcdef" for character in value)