tests/test_sealed_queries.py
"""Regression tests for the post-seal query-vault boundary."""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
import numpy as np
import pytest
import torch
from PIL import Image
from mcss.dynamic.types import SealedScene, hash_scene_state
from mcss.evaluation import sealed_queries
from mcss.evaluation.sealed_queries import evaluate_sealed
from mcss.measurements import FixedMeasurementRenderer
from mcss.types import SceneState
PROJECT = Path(__file__).resolve().parents[1]
@pytest.fixture
def artifact_dir() -> Path:
output_root = PROJECT / "outputs"
output_root.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix="test_sealed_queries_", dir=output_root) as directory:
yield Path(directory)
def test_evaluate_sealed_reports_per_query_metrics_and_preserves_hash(artifact_dir: Path) -> None:
sealed = _sealed_scene()
vault_path = _write_vault(artifact_dir, sealed)
before = hash_scene_state(sealed.scene_state)
result = evaluate_sealed(
sealed,
vault_path,
FixedMeasurementRenderer(n_samples=2, ray_chunk_size=8),
)
assert set(result) == {"per_query", "averages"}
assert result["per_query"][0]["frame_id"] == 5
assert set(result["averages"]) == {
"rgb_mse",
"rgb_psnr",
"depth_abs_rel",
"opacity_mean",
"coverage",
}
assert hash_scene_state(sealed.scene_state) == before
def test_mismatched_scene_is_rejected_before_query_image_io(
artifact_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
sealed = _sealed_scene()
vault_path = _write_vault(artifact_dir, sealed, scene_id="other_scene")
def unexpected_open(*_args: object, **_kwargs: object) -> None:
raise AssertionError("query RGB must not be opened before identity validation")
monkeypatch.setattr(sealed_queries.Image, "open", unexpected_open)
with pytest.raises(ValueError, match="scene_id does not match"):
evaluate_sealed(sealed, vault_path, FixedMeasurementRenderer(n_samples=2))
def test_altered_sealed_state_is_rejected_before_query_image_io(
artifact_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
sealed = _sealed_scene()
vault_path = _write_vault(artifact_dir, sealed)
sealed.scene_state.color.add_(0.1)
def unexpected_open(*_args: object, **_kwargs: object) -> None:
raise AssertionError("query RGB must not be opened after a failed seal hash")
monkeypatch.setattr(sealed_queries.Image, "open", unexpected_open)
with pytest.raises(ValueError, match="state_hash"):
evaluate_sealed(sealed, vault_path, FixedMeasurementRenderer(n_samples=2))
def test_query_camera_is_expressed_relative_to_sealed_anchor(artifact_dir: Path) -> None:
anchor = torch.eye(4)
anchor[0, 3] = 1.0
sealed = _sealed_scene(anchor_c2w=anchor)
vault_path = _write_vault(artifact_dir, sealed, query_x=2.0)
measurement = _CaptureMeasurement()
evaluate_sealed(sealed, vault_path, measurement)
assert measurement.c2w is not None
assert measurement.c2w[0, 0, 0, 3].item() == pytest.approx(1.0)
def test_measurement_state_mutation_is_detected_after_query_io(artifact_dir: Path) -> None:
sealed = _sealed_scene()
vault_path = _write_vault(artifact_dir, sealed)
with pytest.raises(RuntimeError, match="modified during query evaluation"):
evaluate_sealed(sealed, vault_path, _MutatingMeasurement())
def _sealed_scene(*, anchor_c2w: torch.Tensor | None = None) -> SealedScene:
bounds = torch.tensor([[[-1.0, -1.0, 0.1], [1.0, 1.0, 1.0]]])
state = SceneState(
density_logits=torch.full((1, 1, 2, 2, 2), 2.0),
color=torch.full((1, 3, 2, 2, 2), 0.5),
log_variance=torch.zeros((1, 1, 2, 2, 2)),
bounds=bounds,
)
return SealedScene(
episode_id="dev--scene_a--cam_00",
scene_id="scene_a",
split_id="dev",
query_vault_id="qv-test",
scene_state=state,
state_hash=hash_scene_state(state),
observed_ids=(0, 1, 2),
checkpoint_hash="checkpoint",
config_hash="config",
fast_state_hash="fast",
anchor_c2w=torch.eye(4) if anchor_c2w is None else anchor_c2w,
)
def _write_vault(
directory: Path,
sealed: SealedScene,
*,
scene_id: str | None = None,
query_x: float = 0.0,
) -> Path:
rgb_path = directory / "query.png"
depth_path = directory / "query.npy"
Image.new("RGB", (2, 2), (128, 128, 128)).save(rgb_path)
np.save(depth_path, np.ones((2, 2), dtype=np.float32))
c2w = torch.eye(4)
c2w[0, 3] = query_x
vault = {
"schema_version": "mcss.dynamic.query_vault.v1",
"episodes": [
{
"episode_id": sealed.episode_id,
"scene_id": sealed.scene_id if scene_id is None else scene_id,
"split_id": sealed.split_id,
"query_vault_id": sealed.query_vault_id,
"image_size": [2, 2],
"query": [
{
"frame_id": 5,
"intrinsics": [[2.0, 0.0, 0.5], [0.0, 2.0, 0.5], [0.0, 0.0, 1.0]],
"c2w": c2w.tolist(),
"rgb": str(rgb_path),
"depth": str(depth_path),
}
],
}
],
}
path = directory / "vault.json"
path.write_text(json.dumps(vault), encoding="utf-8")
return path
class _CaptureMeasurement:
def __init__(self) -> None:
self.c2w: torch.Tensor | None = None
def __call__(
self, state: SceneState, cameras, _measurements: set[str]
) -> dict[str, torch.Tensor]:
self.c2w = cameras.c2w.clone()
height, width = cameras.image_size
shape = (1, 1, 1, height, width)
return {
"rgb": torch.zeros((1, 1, 3, height, width), device=state.color.device),
"depth": torch.ones(shape, device=state.color.device),
"visibility": torch.ones(shape, device=state.color.device),
}
class _MutatingMeasurement(_CaptureMeasurement):
def __call__(
self, state: SceneState, cameras, measurements: set[str]
) -> dict[str, torch.Tensor]:
state.color.add_(0.1)
return super().__call__(state, cameras, measurements)