feat(lab): publish fail-closed TGS evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 21:32:35 +03:00
parent 67d5d6fa05
commit 6544d9e918
27 changed files with 2167 additions and 73 deletions
+12
View File
@@ -133,6 +133,7 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
build_m48s_fixed_class_detector_lab_router,
)
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.map_api import (
MapGatewayConfiguration,
MapGatewayProxy,
@@ -992,6 +993,17 @@ app.include_router(
),
)
)
app.include_router(
build_m49_tgs_fail_closed_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m49"
/ "tgs-fail-closed-results"
),
)
)
app.include_router(
build_m48s_fixed_class_detector_lab_router(
root_provider=lambda: (
+284
View File
@@ -0,0 +1,284 @@
"""Read-only API for sealed gravity-aligned M49 TGS evidence."""
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_fail_closed import (
M49_TGS_ANCHORS,
M49_TGS_PREFIX,
M49TgsFailClosedError,
M49TgsFailClosedResult,
read_m49_tgs_fail_closed,
)
RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(M49_TGS_PREFIX)}[a-f0-9]{{64}}$")
RESULT_VIEW_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-view/v1"
RESULT_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-catalog/v1"
ANCHOR_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-anchor-catalog/v1"
ANCHOR_SPATIAL_SCHEMA: Final = "missioncore.m49-tgs-anchor-spatial/v1"
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-fail-closed"
PROFILES: Final = ("current_increment", "causal_rolling_1s")
def build_m49_tgs_fail_closed_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
def result(result_id: str) -> M49TgsFailClosedResult:
candidate = _resolve_candidate(root_provider, result_id)
try:
return _read_result_cached(str(candidate), _signature(candidate))
except (M49TgsFailClosedError, OSError, ValueError):
raise HTTPException(status_code=404, detail="M49 TGS result 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 _empty_catalog(configured=False)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in sorted(root.iterdir()):
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
continue
try:
sealed = _read_result_cached(str(candidate.resolve()), _signature(candidate))
items.append(_project_result(sealed))
except (M49TgsFailClosedError, OSError, ValueError):
invalid_total += 1
items.sort(
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
reverse=True,
)
return {
"schema_version": RESULT_CATALOG_SCHEMA,
"configured": True,
"items": items[:limit],
"candidate_total": len(items) + invalid_total,
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/{result_id}")
def get_result(result_id: str) -> dict[str, object]:
return _project_result(result(result_id))
@router.get("/{result_id}/anchors")
def get_anchors(result_id: str) -> dict[str, object]:
sealed = result(result_id)
return {
"schema_version": ANCHOR_CATALOG_SCHEMA,
"result_id": result_id,
"linked_visual_result_id": sealed.report["visual_review"]["linked_visual_result_id"],
"anchors": copy.deepcopy(sealed.report["metrics"]["primary"]),
"anchor_count": len(M49_TGS_ANCHORS),
"profiles": list(PROFILES),
"default_profile": "causal_rolling_1s",
"access": "read-only",
}
@router.get("/{result_id}/anchors/{anchor_frame_index}/spatial")
def get_anchor_spatial(
result_id: str,
anchor_frame_index: int,
profile: str = Query(default="causal_rolling_1s"),
) -> Response:
if profile not in PROFILES or anchor_frame_index not in M49_TGS_ANCHORS:
raise HTTPException(status_code=404, detail="M49 TGS anchor not found")
sealed = result(result_id)
try:
content = _anchor_json_cached(
str(sealed.evidence_path),
result_id,
anchor_frame_index,
profile,
_evidence_signature(sealed.evidence_path),
)
except (KeyError, OSError, ValueError):
raise HTTPException(
status_code=503, detail="M49 TGS 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",
},
)
return router
@lru_cache(maxsize=4)
def _read_result_cached(result_root: str, signature: tuple[int, ...]) -> M49TgsFailClosedResult:
del signature
return read_m49_tgs_fail_closed(Path(result_root))
@lru_cache(maxsize=24)
def _anchor_json_cached(
evidence_path: str,
result_id: str,
anchor_frame_index: int,
profile: str,
signature: tuple[int, int],
) -> bytes:
del signature
slot = M49_TGS_ANCHORS.index(anchor_frame_index)
with np.load(evidence_path, allow_pickle=False) as evidence:
offsets = evidence[f"{profile}_point_offsets"]
start = int(offsets[slot])
end = int(offsets[slot + 1])
points = evidence[f"{profile}_points_xyz_m"][start:end]
point_states = evidence[f"{profile}_point_states"][start:end]
centers = evidence["costmap_cell_centers_xy_m"]
cell_states = evidence[f"{profile}_costmap_states"][slot]
z_bounds = evidence[f"{profile}_costmap_z_bounds_m"][slot]
if (
points.shape[1:] != (3,)
or point_states.shape != (points.shape[0],)
or centers.shape != (2244, 2)
or cell_states.shape != (2244,)
or z_bounds.shape != (2244, 2)
or not np.isfinite(points).all()
or not np.isfinite(centers).all()
or not np.isin(point_states, np.asarray([1, 2, 3], dtype=np.uint8)).all()
or not np.isin(cell_states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
):
raise ValueError("M49 TGS spatial shape changed")
payload = {
"schema_version": ANCHOR_SPATIAL_SCHEMA,
"result_id": result_id,
"anchor_frame_index": anchor_frame_index,
"source_sequence": anchor_frame_index,
"profile": profile,
"coordinate_frame": "map-gravity-local",
"points_xyz_m": points.astype(float).tolist(),
"point_states": point_states.astype(int).tolist(),
"costmap": {
"cell_size_m": 0.45,
"radius_m": 12.0,
"centers_xy_m": centers.astype(float).tolist(),
"states": cell_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
],
},
"state_codes": {
"UNOBSERVED": 0,
"GROUND_SUPPORT": 1,
"NONGROUND_OCCUPIED": 2,
"UNKNOWN_REJECTED": 3,
},
"all_points_accounted": True,
"aos_used": False,
"authority": {
"visual_quality_accepted": False,
"navigation_or_safety_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(result: M49TgsFailClosedResult) -> dict[str, object]:
report = result.report
return {
"schema_version": RESULT_VIEW_SCHEMA,
"result_id": result.result_id,
"created_at_utc": result.manifest["created_at_utc"],
"source": copy.deepcopy(report["source"]),
"configuration": copy.deepcopy(report["configuration"]),
"method": copy.deepcopy(report["method"]),
"execution": copy.deepcopy(report["execution"]),
"metrics": copy.deepcopy(report["metrics"]),
"acceptance": copy.deepcopy(report["acceptance"]),
"decision": copy.deepcopy(report["decision"]),
"limitations": copy.deepcopy(report["limitations"]),
"authority": copy.deepcopy(report["authority"]),
"visual_review": copy.deepcopy(report["visual_review"]),
"ground_truth": False,
"access": "read-only",
}
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
if RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="M49 TGS result not found")
root = _configured_root(provider)
if root is None:
raise HTTPException(status_code=404, detail="M49 TGS result not found")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="M49 TGS result not found")
resolved = candidate.resolve(strict=True)
if resolved.parent != root:
raise HTTPException(status_code=404, detail="M49 TGS result not found")
return resolved
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(candidate: Path) -> tuple[int, ...]:
result: list[int] = []
for name in (
"manifest.json",
"report.json",
"evidence.npz",
"worker-summary.json",
"input-manifest.json",
"tgs-timing.tsv",
):
path = candidate / name
if path.is_symlink() or not path.is_file():
raise ValueError("M49 TGS artifact unavailable")
stat = path.stat()
result.extend((stat.st_size, stat.st_mtime_ns))
return tuple(result)
def _evidence_signature(path: Path) -> tuple[int, int]:
stat = path.stat()
return stat.st_size, stat.st_mtime_ns
def _empty_catalog(*, configured: bool) -> dict[str, object]:
return {
"schema_version": RESULT_CATALOG_SCHEMA,
"configured": configured,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
__all__ = ["build_m49_tgs_fail_closed_router"]