Source: src/mcss/mechanism_pilot/geometry_carrier.py · Plain text

src/mcss/mechanism_pilot/geometry_carrier.py

"""Matched static carrier construction and offline surface supervision.

Carrier inputs are RGB observations and context-camera-derived bounds only. Loss
labels are accepted by a separate function after the shared state exists.
"""

from __future__ import annotations

import torch
from torch import nn

from mcss.dynamic.carrier import DynamicSceneCarrier
from mcss.dynamic.types import OnlineObservation
from mcss.measurements import FixedMeasurementRenderer
from mcss.mechanism_pilot.direct_capacity_contracts import ObservationBatch
from mcss.mechanism_pilot.direct_capacity_optimization import cache_rays, render_rays
from mcss.mechanism_pilot.geometric_supervision_optimization import (
    geometry_losses,
    ray_geometry,
    surface_tau,
)
from mcss.training.grounded import build_grounded_state


class _Materializer(nn.Module):
    def __init__(self, carrier):
        super().__init__()
        self.carrier = carrier

    def forward(self, cache, fast):
        return self.carrier.materialize(cache, fast)


def build_state(carrier, observations, bounds, episode_id):
    """Return (differentiable shared state, raw anchor pose); no labels/query arguments.

    The caller supplies only the frozen context-camera bounds rule. Geometry buffers
    are scoped to this call using functional_call, and do not enter learned parameters.
    No writes or nonzero fast offsets are used.
    """
    if not isinstance(carrier, DynamicSceneCarrier):
        raise TypeError("Existing DynamicSceneCarrier architecture required")
    config = carrier.config
    if (
        tuple(config.grid_size),
        config.token_count,
        config.feature_dim,
        config.hidden_dim,
        config.expansion_dim,
    ) != ((16, 16, 16), 128, 8, 8, 16):
        raise ValueError("Frozen matched 16-grid carrier architecture required")
    observations = tuple(observations)
    if not observations or any(type(o) is not OnlineObservation for o in observations):
        raise TypeError("Only RGB-camera OnlineObservation values may construct state")
    bounds = torch.as_tensor(bounds, device=carrier._bounds.device, dtype=carrier._bounds.dtype)
    if bounds.shape not in ((2, 3), (1, 2, 3)) or bounds.requires_grad:
        raise ValueError("Frozen nonlearned bounds must have shape [2,3] or [1,2,3]")
    bounds = bounds.reshape(1, 2, 3).detach().clone()
    if not torch.isfinite(bounds).all() or not (bounds[:, 1] > bounds[:, 0]).all():
        raise ValueError("Finite ordered bounds required")
    fractions = (carrier._candidate_normalized_xyz + 1) * 0.5
    points = bounds[:, 0] + fractions * (bounds[:, 1] - bounds[:, 0])
    wrapped = _Materializer(carrier)
    replacements = {"carrier._bounds": bounds, "carrier._candidate_points": points}

    class ContextOnlyAdapter:
        encode = carrier.encode
        initial_fast = carrier.initial_fast

        @staticmethod
        def materialize(cache, fast):
            return torch.func.functional_call(wrapped, replacements, (cache, fast), strict=False)

    return build_grounded_state(ContextOnlyAdapter(), observations, episode_id)


def training_loss(state, local_cameras, rgb, depth, indices, variant):
    """Offline TRAIN labels only; caller enforces scene-role provenance.

    C0 retains the old carrier RGB Charbonnier (epsilon .001) plus masked depth
    AbsRel. Its explicit regularization was zero. C1 adds only .1 surface loss;
    optional C2 also adds .1 free-space. Index sampling is externally frozen.
    """
    if variant not in ("C0", "C1", "C2"):
        raise ValueError("Only preregistered C0/C1/C2 loss variants allowed")
    if tuple(state.spatial_shape) != (16, 16, 16):
        raise ValueError("Only 16-grid state allowed")
    if len(local_cameras.leading_shape) != 2 or local_cameras.leading_shape[0] != 1:
        raise ValueError("Single-scene cameras must have shape [1,V]")
    views = local_cameras.leading_shape[1]
    height, width = local_cameras.image_size
    if rgb.shape != (1, views, 3, height, width) or depth.shape != (1, views, 1, height, width):
        raise ValueError("Exact camera-aligned RGB/depth target shapes required")
    if not torch.isfinite(rgb).all() or (rgb < 0).any() or (rgb > 1).any():
        raise ValueError("Finite RGB targets in [0,1] required")
    if indices.ndim != 1 or indices.numel() == 0 or indices.dtype != torch.long:
        raise ValueError("Nonempty one-dimensional int64 sampled ray indices required")
    if indices.min() < 0 or indices.max() >= views * height * width:
        raise ValueError("Sampled ray index outside TRAIN labels")
    valid = torch.isfinite(depth) & (depth > 0)
    clean_depth = torch.where(valid, depth, torch.zeros_like(depth))
    batch = ObservationBatch(
        "offline_train",
        tuple(range(views)),
        rgb,
        local_cameras,
        clean_depth,
        "OFFLINE_TRAIN_LABELS_NOT_CARRIER_INPUT",
    )
    cache = cache_rays(batch, state.bounds)
    renderer = FixedMeasurementRenderer(n_samples=64, ray_chunk_size=2048).to(state.color.device)
    pred = render_rays(renderer, state, cache, indices)
    rgb_loss = torch.sqrt((pred["rgb"] - cache["rgb"][:, indices]).square() + 1e-6).mean()
    target = cache["depth"][:, indices]
    valid = target > 0
    relative = (pred["depth"] - target).abs() / target.abs().clamp_min(1e-3)
    depth_loss = (relative * valid).sum() / valid.sum().clamp_min(1)
    base = rgb_loss + depth_loss
    with torch.set_grad_enabled(torch.is_grad_enabled() and variant != "C0"):
        free, surface, counts = geometry_losses(
            *ray_geometry(renderer, state, cache, indices), target, surface_tau(state.bounds)
        )
    loss = base
    if variant in ("C1", "C2"):
        loss = loss + 0.1 * surface
    if variant == "C2":
        loss = loss + 0.1 * free
    terms = {
        "loss_rgb": rgb_loss,
        "loss_depth": depth_loss,
        "regularization": base.new_zeros(()),
        "base_loss": base,
        "loss_free": free,
        "loss_surface": surface,
        "loss": loss,
        "opacity": pred["visibility"].mean(),
        **counts,
    }
    return loss, terms