feat(lab): publish full TGS shadow evidence
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
"""Seal and verify the complete source-paced TRAVEL TGS shadow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
RESULT_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||
REPORT_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-report/v1"
|
||||
WORKER_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-result/v1"
|
||||
PREFIX: Final = "m49-tgs-full-shadow-"
|
||||
PROFILE_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-profile/v1"
|
||||
EVIDENCE_FILES: Final = (
|
||||
"costmap-cell-centers-xy-m.npy",
|
||||
"costmap-cell-indices-xy.npy",
|
||||
"costmap-states.npy",
|
||||
"costmap-z-bounds-m.npy",
|
||||
"frames.ndjson",
|
||||
)
|
||||
_HASH_CHUNK_BYTES: Final = 1024 * 1024
|
||||
_MAX_JSON_BYTES: Final = 4 * 1024 * 1024
|
||||
|
||||
|
||||
class M49TgsFullShadowError(RuntimeError):
|
||||
"""The full TGS shadow is unavailable or violates its immutable contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M49TgsFullShadowResult:
|
||||
result_id: str
|
||||
root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
content = json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
|
||||
|
||||
def _json(path: Path, label: str) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||
raise M49TgsFullShadowError(f"{label} is unavailable")
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise M49TgsFullShadowError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str, media_type: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"media_type": media_type,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def seal_m49_tgs_full_shadow(
|
||||
*,
|
||||
source_root: Path,
|
||||
destination_root: Path,
|
||||
profile_path: Path,
|
||||
linked_visual_result_id: str,
|
||||
created_at_utc: str | None = None,
|
||||
) -> M49TgsFullShadowResult:
|
||||
source = source_root.expanduser().resolve(strict=True)
|
||||
if source.is_symlink() or not source.is_dir():
|
||||
raise M49TgsFullShadowError("Worker evidence root is unavailable")
|
||||
destination = destination_root.expanduser().absolute()
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
if destination.is_symlink():
|
||||
raise M49TgsFullShadowError("destination must not be a symlink")
|
||||
profile = _json(profile_path, "full-shadow profile")
|
||||
worker = _json(source / "result.json", "Worker result")
|
||||
summary = _json(source / "worker-summary.json", "Worker summary")
|
||||
timeline = worker.get("timeline")
|
||||
if (
|
||||
profile.get("schema_version") != PROFILE_SCHEMA
|
||||
or worker.get("schema_version") != WORKER_SCHEMA
|
||||
or not isinstance(timeline, dict)
|
||||
or (
|
||||
timeline.get("frame_count") != 4489
|
||||
or timeline.get("available_lidar_frame_count") != 3928
|
||||
or timeline.get("missing_lidar_frame_count") != 561
|
||||
)
|
||||
or worker.get("point_accounting", {}).get("unaccounted") != 0
|
||||
or summary.get("schema_version") != "missioncore.m49-tgs-full-shadow-worker-summary/v1"
|
||||
or summary.get("gpu_requested") is not False
|
||||
or summary.get("aos_used") is not False
|
||||
or summary.get("all_timeline_frames_accounted") is not True
|
||||
or summary.get("all_eligible_points_accounted") is not True
|
||||
or summary.get("canonical_triton_health") != "healthy"
|
||||
):
|
||||
raise M49TgsFullShadowError("Worker full-shadow contract changed")
|
||||
if (
|
||||
not linked_visual_result_id.startswith("m4-threat-replay-")
|
||||
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||
):
|
||||
raise M49TgsFullShadowError("linked visual result is invalid")
|
||||
for name in EVIDENCE_FILES:
|
||||
path = source / name
|
||||
proof = worker.get("files", {}).get(name, {})
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or proof.get("bytes") != path.stat().st_size
|
||||
or proof.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise M49TgsFullShadowError(f"Worker evidence changed: {name}")
|
||||
identity = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"source_pack_sha256": worker["source_pack_sha256"],
|
||||
"input_manifest_sha256": worker["input_manifest_sha256"],
|
||||
"config_sha256": worker["config_sha256"],
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
"files": {name: worker["files"][name]["sha256"] for name in EVIDENCE_FILES},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"{PREFIX}{identity_sha256}"
|
||||
target = destination / result_id
|
||||
if target.exists():
|
||||
return read_m49_tgs_full_shadow(target)
|
||||
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||
performance_accepted = worker.get("status") == "passed"
|
||||
report = {
|
||||
"schema_version": REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"source": {
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"source_pack_sha256": worker["source_pack_sha256"],
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
},
|
||||
"configuration": {
|
||||
"profile_id": profile["profile_id"],
|
||||
"config_sha256": worker["config_sha256"],
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"history_seconds": profile["profile"]["history_seconds"],
|
||||
"cell_size_m": worker["costmap"]["cell_size_m"],
|
||||
"radius_m": worker["costmap"]["radius_m"],
|
||||
"state_priority": profile["costmap"]["state_priority"],
|
||||
},
|
||||
"execution": {
|
||||
"worker": "Worker 006",
|
||||
"device": "cpu",
|
||||
"gpu_used": False,
|
||||
"aos_used": False,
|
||||
"wrapper_elapsed_seconds": summary["wall_seconds"],
|
||||
"canonical_triton_id": summary["canonical_triton_id"],
|
||||
"canonical_triton_health": summary["canonical_triton_health"],
|
||||
},
|
||||
"timeline": worker["timeline"],
|
||||
"point_accounting": worker["point_accounting"],
|
||||
"performance": worker["performance"],
|
||||
"acceptance": {
|
||||
**worker["acceptance"],
|
||||
"representation_complete": True,
|
||||
"visual_quality_accepted": False,
|
||||
"integrated_graph_performance_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"state": (
|
||||
"source-paced-qualified-visual-review-required"
|
||||
if performance_accepted
|
||||
else "performance-rejected"
|
||||
),
|
||||
"candidate_retained": performance_accepted,
|
||||
"next_action": (
|
||||
"Review the complete camera-synchronised TGS costmap timeline; "
|
||||
"then measure the integrated graph regression separately."
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"The run proves recorded source-paced CPU shadow performance, not live sensor transport.",
|
||||
"No independent traversability truth or vehicle envelope is present.",
|
||||
"Missing LiDAR frames are explicit all-cell UNOBSERVED and never inferred free.",
|
||||
"Camera projection, navigation and actuation remain disabled.",
|
||||
],
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"commands_enabled": False,
|
||||
"realtime_shadow_accepted": performance_accepted,
|
||||
"integrated_graph_performance_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
"visual_review": {
|
||||
"instrument": "m4-canonical-reference-graph",
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
"frame_count": 4489,
|
||||
"state_codes": profile["state_codes"],
|
||||
},
|
||||
}
|
||||
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-full-", dir=destination) as raw:
|
||||
staging = Path(raw) / result_id
|
||||
staging.mkdir()
|
||||
for name in EVIDENCE_FILES:
|
||||
shutil.copyfile(source / name, staging / name)
|
||||
shutil.copyfile(source / "worker-summary.json", staging / "worker-summary.json")
|
||||
(staging / "report.json").write_text(
|
||||
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
artifacts = [
|
||||
_artifact(staging / "report.json", "report", "application/json"),
|
||||
_artifact(staging / "worker-summary.json", "runtime-summary", "application/json"),
|
||||
]
|
||||
artifacts.extend(
|
||||
_artifact(
|
||||
staging / name,
|
||||
"frame-catalog" if name == "frames.ndjson" else "spatial-evidence",
|
||||
"application/x-ndjson" if name == "frames.ndjson" else "application/x-npy",
|
||||
)
|
||||
for name in EVIDENCE_FILES
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
staging.replace(target)
|
||||
return read_m49_tgs_full_shadow(target)
|
||||
|
||||
|
||||
def read_m49_tgs_full_shadow(root: Path) -> M49TgsFullShadowResult:
|
||||
candidate = root.expanduser().resolve(strict=True)
|
||||
if candidate.is_symlink() or not candidate.is_dir() or not candidate.name.startswith(PREFIX):
|
||||
raise M49TgsFullShadowError("full-shadow result root is invalid")
|
||||
manifest = _json(candidate / "manifest.json", "full-shadow manifest")
|
||||
report = _json(candidate / "report.json", "full-shadow report")
|
||||
identity = manifest.get("identity")
|
||||
if (
|
||||
manifest.get("schema_version") != RESULT_SCHEMA
|
||||
or report.get("schema_version") != REPORT_SCHEMA
|
||||
or manifest.get("result_id") != candidate.name
|
||||
or report.get("result_id") != candidate.name
|
||||
or not isinstance(identity, dict)
|
||||
or manifest.get("identity_sha256") != _canonical_sha256(identity)
|
||||
or candidate.name != f"{PREFIX}{manifest['identity_sha256']}"
|
||||
):
|
||||
raise M49TgsFullShadowError("full-shadow identity changed")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise M49TgsFullShadowError("full-shadow artifact catalog changed")
|
||||
for artifact in artifacts:
|
||||
if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str):
|
||||
raise M49TgsFullShadowError("full-shadow artifact entry changed")
|
||||
path = candidate / artifact["path"]
|
||||
if (
|
||||
path.parent != candidate
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or artifact.get("byte_length") != path.stat().st_size
|
||||
or artifact.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise M49TgsFullShadowError("full-shadow artifact digest changed")
|
||||
return M49TgsFullShadowResult(candidate.name, candidate, manifest, report)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M49TgsFullShadowError",
|
||||
"M49TgsFullShadowResult",
|
||||
"PREFIX",
|
||||
"read_m49_tgs_full_shadow",
|
||||
"seal_m49_tgs_full_shadow",
|
||||
]
|
||||
@@ -134,6 +134,7 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
|
||||
)
|
||||
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
||||
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
|
||||
from k1link.web.map_api import (
|
||||
MapGatewayConfiguration,
|
||||
MapGatewayProxy,
|
||||
@@ -1004,6 +1005,17 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m49_tgs_full_shadow_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "m49"
|
||||
/ "tgs-full-shadow-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m48s_fixed_class_detector_lab_router(
|
||||
root_provider=lambda: (
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
"""Read-only API for the sealed complete TGS shadow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException, Query, Response
|
||||
|
||||
from k1link.laboratory.m49_tgs_full_shadow import (
|
||||
M49TgsFullShadowError,
|
||||
M49TgsFullShadowResult,
|
||||
PREFIX,
|
||||
read_m49_tgs_full_shadow,
|
||||
)
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
RESULT_ID: Final = re.compile(rf"^{re.escape(PREFIX)}[a-f0-9]{{64}}$")
|
||||
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-full-shadow"
|
||||
|
||||
|
||||
def build_m49_tgs_full_shadow_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
|
||||
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
|
||||
|
||||
def sealed(result_id: str) -> M49TgsFullShadowResult:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None or RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found")
|
||||
candidate = root / result_id
|
||||
if candidate.is_symlink() or not candidate.is_dir():
|
||||
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
if resolved.parent != root:
|
||||
raise ValueError("result escaped configured root")
|
||||
return _read_cached(str(resolved), _signature(resolved))
|
||||
except (M49TgsFullShadowError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
return _catalog([], configured=False, invalid_total=0)
|
||||
results: list[dict[str, object]] = []
|
||||
invalid = 0
|
||||
for candidate in sorted(root.iterdir()):
|
||||
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
|
||||
continue
|
||||
try:
|
||||
results.append(_project(_read_cached(str(candidate.resolve()), _signature(candidate))))
|
||||
except (M49TgsFullShadowError, OSError, ValueError):
|
||||
invalid += 1
|
||||
results.sort(key=lambda row: (str(row["created_at_utc"]), str(row["result_id"])), reverse=True)
|
||||
return _catalog(results[:limit], configured=True, invalid_total=invalid)
|
||||
|
||||
@router.get("/{result_id}")
|
||||
def get_result(result_id: str) -> dict[str, object]:
|
||||
return _project(sealed(result_id))
|
||||
|
||||
@router.get("/{result_id}/frames/{source_sequence}/spatial")
|
||||
def get_spatial(result_id: str, source_sequence: int) -> Response:
|
||||
if source_sequence < 0 or source_sequence >= 4489:
|
||||
raise HTTPException(status_code=404, detail="M49 TGS full-shadow frame not found")
|
||||
result = sealed(result_id)
|
||||
try:
|
||||
content = _frame_json_cached(
|
||||
str(result.root), result_id, source_sequence, _evidence_signature(result.root)
|
||||
)
|
||||
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
||||
raise HTTPException(status_code=503, detail="M49 TGS full-shadow spatial evidence failed verification") from None
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
headers={"Cache-Control": "private, max-age=31536000, immutable", "X-Content-Type-Options": "nosniff"},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/spatial/chunk")
|
||||
def get_spatial_chunk(
|
||||
result_id: str,
|
||||
start: int = Query(ge=0, lt=4489),
|
||||
count: int = Query(default=24, ge=1, le=24),
|
||||
) -> Response:
|
||||
result = sealed(result_id)
|
||||
bounded_count = min(count, 4489 - start)
|
||||
try:
|
||||
content = _chunk_json_cached(
|
||||
str(result.root), result_id, start, bounded_count, _evidence_signature(result.root)
|
||||
)
|
||||
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="M49 TGS full-shadow spatial chunk failed verification",
|
||||
) from None
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _read_cached(root: str, signature: tuple[int, ...]) -> M49TgsFullShadowResult:
|
||||
del signature
|
||||
return read_m49_tgs_full_shadow(Path(root))
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _frames(root: str, signature: tuple[int, int]) -> tuple[dict[str, object], ...]:
|
||||
del signature
|
||||
path = Path(root) / "frames.ndjson"
|
||||
rows = tuple(json.loads(line) for line in path.read_text(encoding="utf-8").splitlines())
|
||||
if len(rows) != 4489:
|
||||
raise ValueError("full-shadow frame catalog changed")
|
||||
return rows
|
||||
|
||||
|
||||
@lru_cache(maxsize=96)
|
||||
def _frame_json_cached(
|
||||
root: str, result_id: str, source_sequence: int, signature: tuple[int, ...]
|
||||
) -> bytes:
|
||||
root_path = Path(root)
|
||||
frame_signature = (signature[-2], signature[-1])
|
||||
frame = _frames(root, frame_signature)[source_sequence]
|
||||
centers = np.load(root_path / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False)
|
||||
states_all = np.load(root_path / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||
z_all = np.load(root_path / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False)
|
||||
states = states_all[source_sequence]
|
||||
z_bounds = z_all[source_sequence]
|
||||
if (
|
||||
centers.shape != (2244, 2)
|
||||
or states_all.shape != (4489, 2244)
|
||||
or z_all.shape != (4489, 2244, 2)
|
||||
or not np.isfinite(centers).all()
|
||||
or not np.isin(states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
|
||||
):
|
||||
raise ValueError("full-shadow spatial shape changed")
|
||||
payload = {
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-spatial/v1",
|
||||
"result_id": result_id,
|
||||
"source_sequence": source_sequence,
|
||||
"source_frame_index": frame["source_frame_index"],
|
||||
"session_seconds": frame["session_seconds"],
|
||||
"sample_available": frame["sample_available"],
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"costmap": {
|
||||
"cell_size_m": 0.45,
|
||||
"radius_m": 12.0,
|
||||
"centers_xy_m": centers.astype(float).tolist(),
|
||||
"states": states.astype(int).tolist(),
|
||||
"z_bounds_m": [
|
||||
[
|
||||
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||
]
|
||||
for row in z_bounds
|
||||
],
|
||||
},
|
||||
"metrics": copy.deepcopy(frame),
|
||||
"state_codes": {"UNOBSERVED": 0, "GROUND_SUPPORT": 1, "NONGROUND_OCCUPIED": 2, "UNKNOWN_REJECTED": 3},
|
||||
"aos_used": False,
|
||||
"gpu_used": False,
|
||||
"authority": {"navigation_or_safety_accepted": False, "visual_quality_accepted": False},
|
||||
"access": "read-only",
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _chunk_json_cached(
|
||||
root: str,
|
||||
result_id: str,
|
||||
start: int,
|
||||
count: int,
|
||||
signature: tuple[int, ...],
|
||||
) -> bytes:
|
||||
root_path = Path(root)
|
||||
frame_signature = (signature[-2], signature[-1])
|
||||
frames = _frames(root, frame_signature)
|
||||
centers = np.load(root_path / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False)
|
||||
states_all = np.load(root_path / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||
z_all = np.load(root_path / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False)
|
||||
if (
|
||||
centers.shape != (2244, 2)
|
||||
or states_all.shape != (4489, 2244)
|
||||
or z_all.shape != (4489, 2244, 2)
|
||||
or not np.isfinite(centers).all()
|
||||
):
|
||||
raise ValueError("full-shadow spatial chunk shape changed")
|
||||
rows: list[dict[str, object]] = []
|
||||
for source_sequence in range(start, start + count):
|
||||
frame = frames[source_sequence]
|
||||
states = states_all[source_sequence]
|
||||
z_bounds = z_all[source_sequence]
|
||||
if not np.isin(states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all():
|
||||
raise ValueError("full-shadow spatial state changed")
|
||||
rows.append(
|
||||
{
|
||||
"source_sequence": source_sequence,
|
||||
"source_frame_index": frame["source_frame_index"],
|
||||
"session_seconds": frame["session_seconds"],
|
||||
"sample_available": frame["sample_available"],
|
||||
"states": states.astype(int).tolist(),
|
||||
"z_bounds_m": [
|
||||
[
|
||||
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||
]
|
||||
for row in z_bounds
|
||||
],
|
||||
"metrics": copy.deepcopy(frame),
|
||||
}
|
||||
)
|
||||
payload = {
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-spatial-chunk/v1",
|
||||
"result_id": result_id,
|
||||
"start": start,
|
||||
"count": count,
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"costmap": {
|
||||
"cell_size_m": 0.45,
|
||||
"radius_m": 12.0,
|
||||
"centers_xy_m": centers.astype(float).tolist(),
|
||||
},
|
||||
"frames": rows,
|
||||
"state_codes": {
|
||||
"UNOBSERVED": 0,
|
||||
"GROUND_SUPPORT": 1,
|
||||
"NONGROUND_OCCUPIED": 2,
|
||||
"UNKNOWN_REJECTED": 3,
|
||||
},
|
||||
"aos_used": False,
|
||||
"gpu_used": False,
|
||||
"authority": {
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
"access": "read-only",
|
||||
}
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _project(result: M49TgsFullShadowResult) -> dict[str, object]:
|
||||
return {
|
||||
**copy.deepcopy(result.report),
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-view/v1",
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": result.manifest["created_at_utc"],
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _catalog(items: list[dict[str, object]], *, configured: bool, invalid_total: int) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-catalog/v1",
|
||||
"configured": configured,
|
||||
"items": items,
|
||||
"candidate_total": len(items) + invalid_total,
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None or value.is_symlink() or not value.is_dir():
|
||||
return None
|
||||
return value.resolve(strict=True)
|
||||
|
||||
|
||||
def _signature(root: Path) -> tuple[int, ...]:
|
||||
values: list[int] = []
|
||||
for name in ("manifest.json", "report.json", "worker-summary.json", *EVIDENCE_FILES):
|
||||
path = root / name
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("full-shadow artifact unavailable")
|
||||
stat = path.stat()
|
||||
values.extend((stat.st_size, stat.st_mtime_ns))
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _evidence_signature(root: Path) -> tuple[int, ...]:
|
||||
return _signature(root)
|
||||
|
||||
|
||||
EVIDENCE_FILES: Final = (
|
||||
"costmap-cell-centers-xy-m.npy",
|
||||
"costmap-cell-indices-xy.npy",
|
||||
"costmap-states.npy",
|
||||
"costmap-z-bounds-m.npy",
|
||||
"frames.ndjson",
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["build_m49_tgs_full_shadow_router"]
|
||||
Reference in New Issue
Block a user