feat(perception): qualify E35 degradation recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 17:53:59 +03:00
parent 621084fcd6
commit b894945344
21 changed files with 3985 additions and 17 deletions
+106 -3
View File
@@ -26,7 +26,7 @@ def _endpoint(router: APIRouter, path: str) -> object:
def test_advanced_catalogs_are_empty_when_not_configured() -> None:
router = build_advanced_laboratory_router()
for name in ("e31", "e32", "e33", "e34"):
for name in ("e31", "e32", "e33", "e34", "e35"):
route = _endpoint(router, f"/api/v1/laboratory/{name}/results")
catalog = route(limit=1) # type: ignore[operator]
assert catalog == {
@@ -46,20 +46,23 @@ def test_advanced_catalogs_fail_closed_on_incomplete_results(
e32 = tmp_path / "e32"
e33 = tmp_path / "e33"
e34 = tmp_path / "e34"
for root in (e31, e32, e33, e34):
e35 = tmp_path / "e35"
for root in (e31, e32, e33, e34, e35):
root.mkdir()
(e31 / f"e31-source-qualification-{'1' * 64}").mkdir()
(e32 / f"e32-track-geometry-{'2' * 64}").mkdir()
(e33 / f"e33-worker-shadow-{'3' * 64}").mkdir()
(e34 / f"e34-temporal-occupied-{'4' * 64}").mkdir()
(e35 / f"e35-degradation-recovery-{'5' * 64}").mkdir()
router = build_advanced_laboratory_router(
e31_root_provider=lambda: e31,
e32_root_provider=lambda: e32,
e33_root_provider=lambda: e33,
e34_root_provider=lambda: e34,
e35_root_provider=lambda: e35,
)
for name in ("e31", "e32", "e33", "e34"):
for name in ("e31", "e32", "e33", "e34", "e35"):
route = _endpoint(router, f"/api/v1/laboratory/{name}/results")
catalog = route(limit=1) # type: ignore[operator]
assert catalog["configured"] is True
@@ -209,3 +212,103 @@ def test_e34_catalog_projects_only_accepted_read_only_evidence(
assert item["metrics"]["free_cell_rows"] == 0
assert item["authority"] == authority
assert item["access"] == "read-only"
def test_e35_catalog_projects_recovery_and_review(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
result_id = f"e35-degradation-recovery-{'5' * 64}"
root = tmp_path / "e35"
candidate = root / result_id
candidate.mkdir(parents=True)
(candidate / "manifest.json").write_text("{}", encoding="utf-8")
authority = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
result = SimpleNamespace(
result_id=result_id,
accepted=True,
manifest={
"created_at_utc": "2026-07-27T15:00:00Z",
"acceptance_state": (
"accepted-deterministic-degradation-recovery"
),
"identity": {
"source_session_id": "source-session",
"e32_result_id": f"e32-track-geometry-{'2' * 64}",
"e33_result_id": f"e33-worker-shadow-{'3' * 64}",
"e34_result_id": f"e34-temporal-occupied-{'4' * 64}",
"pipeline": (
"track-geometry/temporal-layer/"
"degradation-recovery/v1"
),
"coordinate_frame": "map",
"profile": {
"profile_id": (
"e35-six-channel-degradation-recovery/v1"
),
"acceptance": {
"maximum_recovery_seconds": 0.25,
"expected_scenario_count": 6,
},
},
},
},
report={
"metrics": {
"accounting": {
"source_frames": 4489,
"variant_count": 6,
"variant_frame_outcomes": 26934,
"injection_records": 360,
},
"scenarios": [
{
"scenario_id": "camera-loss",
"recovery_seconds": 0.086,
}
],
"runtime": {
"build_elapsed_ms": 18618.0,
"variant_frame_processing_ms": {"p95": 1.08},
},
},
"acceptance": {"accepted": True, "requirements": {}},
"decision": {"next_gate": "A9/E36"},
"authority": authority,
},
review={
"schema_version": "missioncore.e35-recovery-review/v1",
"result_id": result_id,
"scenarios": [],
},
)
def fake_read(
root_text: str,
signature: tuple[int, ...],
) -> SimpleNamespace:
assert root_text == str(candidate.resolve())
assert signature
return result
monkeypatch.setattr(advanced_api, "_read_e35_cached", fake_read)
router = build_advanced_laboratory_router(
e35_root_provider=lambda: root,
)
route = _endpoint(router, "/api/v1/laboratory/e35/results")
catalog = route(limit=1) # type: ignore[operator]
assert catalog["candidate_total"] == 1
assert catalog["invalid_total"] == 0
item = catalog["items"][0]
assert item["status"] == (
"accepted-deterministic-degradation-recovery"
)
assert item["metrics"]["variant_frame_outcomes"] == 26934
assert item["configuration"]["maximum_recovery_seconds"] == 0.25
assert item["review"]["scenarios"] == []
assert item["authority"] == authority
assert item["access"] == "read-only"
+315
View File
@@ -0,0 +1,315 @@
from __future__ import annotations
import copy
import json
from pathlib import Path
import numpy as np
import pytest
from k1link.compute.degradation_recovery import (
DegradationKind,
DegradationRecoveryError,
DegradationScenario,
transform_track_geometry_frame,
)
from k1link.compute.e35_degradation_replay import (
E35DegradationReplayError,
read_e35_degradation_profile,
)
from k1link.compute.track_geometry import (
PointSlab,
TrackGeometry,
TrackGeometryCurrentness,
TrackGeometryEvidenceState,
TrackGeometryFrame,
TrackGeometryMetricBasis,
TrackGeometryOwnerKind,
TrackGeometrySourceBinding,
)
def _binding() -> TrackGeometrySourceBinding:
return TrackGeometrySourceBinding(
source_pack_id="e10-lidar-pack-" + "a" * 64,
source_session_id="source-session",
representation_profile_id="representation-profile/v1",
e31_qualification_id="e31-source-qualification-" + "b" * 64,
calibration_sha256="c" * 64,
coordinate_frame="map",
time_basis="source-time",
selected_offset_ms=0,
)
def _frame(frame_index: int = 10) -> TrackGeometryFrame:
geometries = (
TrackGeometry(
owner_key="track:7",
owner_kind=TrackGeometryOwnerKind.CAMERA_TRACK,
evidence_state=TrackGeometryEvidenceState.AGREE,
currentness=TrackGeometryCurrentness.CURRENT,
metric_basis=TrackGeometryMetricBasis.CURRENT_POINTS,
reason_codes=("accepted-hit-backed-support",),
semantic_track_id=7,
semantic_label="car",
bbox_xyxy=(1.0, 1.0, 2.0, 2.0),
range_m=3.0,
),
TrackGeometry(
owner_key="geometry:8",
owner_kind=TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
evidence_state=TrackGeometryEvidenceState.GEOMETRY_ONLY,
currentness=TrackGeometryCurrentness.CURRENT,
metric_basis=TrackGeometryMetricBasis.CURRENT_POINTS,
reason_codes=("accepted-hit-backed-support",),
range_m=4.0,
),
TrackGeometry(
owner_key="track:9",
owner_kind=TrackGeometryOwnerKind.CAMERA_TRACK,
evidence_state=TrackGeometryEvidenceState.CAMERA_ONLY,
currentness=TrackGeometryCurrentness.CURRENT,
metric_basis=TrackGeometryMetricBasis.UNAVAILABLE,
reason_codes=("camera-without-current-points",),
semantic_track_id=9,
semantic_label="person",
bbox_xyxy=(2.0, 2.0, 3.0, 4.0),
),
)
return TrackGeometryFrame(
binding=_binding(),
frame_index=frame_index,
source_frame_index=frame_index,
session_seconds=frame_index / 10.0,
source_available=True,
point_slab=PointSlab(
frame_index=frame_index,
source_frame_index=frame_index,
source_point_count=4,
coordinate_frame="map",
owner_keys=("track:7", "geometry:8"),
source_indices=np.asarray([0, 1, 2, 3], dtype="<i8"),
points_xyz_m=np.asarray(
[
[1.0, 0.0, 0.0],
[1.1, 0.0, 0.0],
[2.0, 0.0, 0.0],
[2.1, 0.0, 0.0],
],
dtype="<f4",
),
owner_indices=np.asarray([0, 0, 1, 1], dtype="<u4"),
),
geometries=geometries,
)
def _scenario(
kind: DegradationKind,
parameters: dict[str, object],
*,
frame_start: int = 10,
frame_end: int = 20,
) -> DegradationScenario:
return DegradationScenario(
scenario_id=kind.value,
kind=kind,
frame_start=frame_start,
frame_end=frame_end,
parameters=parameters,
)
def test_camera_loss_removes_semantics_but_retains_geometry_points() -> None:
transformed = transform_track_geometry_frame(
_frame(),
_scenario(
DegradationKind.CAMERA_LOSS,
{"drop_camera_observations": True},
),
)
assert transformed.frame.point_slab.row_count == 4
assert all(
geometry.owner_kind is TrackGeometryOwnerKind.GEOMETRY_CLUSTER
for geometry in transformed.frame.geometries
)
assert all(
geometry.semantic_label is None
for geometry in transformed.frame.geometries
)
assert transformed.transformation["channels"]["camera"] == "unavailable"
@pytest.mark.parametrize(
("kind", "parameters", "expected_source_available"),
[
(
DegradationKind.LIDAR_LOSS,
{"drop_lidar_points": True},
False,
),
(
DegradationKind.POSE_STALENESS,
{"pose_age_seconds": 0.5},
True,
),
],
)
def test_metric_loss_keeps_camera_nonmetric_and_withholds_map_rows(
kind: DegradationKind,
parameters: dict[str, object],
expected_source_available: bool,
) -> None:
transformed = transform_track_geometry_frame(
_frame(),
_scenario(kind, parameters),
)
assert transformed.frame.point_slab.row_count == 0
assert transformed.frame.source_available is expected_source_available
assert all(
geometry.owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
for geometry in transformed.frame.geometries
)
assert all(
geometry.metric_basis is not TrackGeometryMetricBasis.CURRENT_POINTS
for geometry in transformed.frame.geometries
)
assert all(
geometry.evidence_state is not TrackGeometryEvidenceState.AGREE
for geometry in transformed.frame.geometries
)
def test_delayed_frame_is_discarded_and_cannot_retain_claims() -> None:
transformed = transform_track_geometry_frame(
_frame(),
_scenario(
DegradationKind.DELAYED_FRAMES,
{"delay_seconds": 1.0, "late_result_policy": "discard"},
),
)
assert not transformed.frame.source_available
assert transformed.frame.point_slab.row_count == 0
assert transformed.frame.geometries == ()
assert transformed.transformation["action"] == "late-frame-discarded"
def test_bounded_drop_is_deterministic_and_journals_pass_frames() -> None:
scenario = _scenario(
DegradationKind.BOUNDED_DROP,
{"drop_every_nth_frame": 3},
)
dropped = transform_track_geometry_frame(_frame(10), scenario)
passed_input = _frame(11)
passed = transform_track_geometry_frame(passed_input, scenario)
assert dropped.frame.geometries == ()
assert dropped.transformation["action"] == "input-frame-dropped"
assert passed.frame is passed_input
assert passed.frame.geometries
assert passed.transformation["action"] == "bounded-drop-pass"
def test_timing_offset_splits_camera_and_geometry_without_agree() -> None:
transformed = transform_track_geometry_frame(
_frame(),
_scenario(
DegradationKind.TIMING_OFFSET,
{"camera_lidar_offset_ms": 250},
),
)
camera = [
geometry
for geometry in transformed.frame.geometries
if geometry.owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
]
geometry = [
item
for item in transformed.frame.geometries
if item.owner_kind is TrackGeometryOwnerKind.GEOMETRY_CLUSTER
]
assert transformed.frame.point_slab.row_count == 4
assert len(camera) == 2
assert len(geometry) == 2
assert all(
item.evidence_state is not TrackGeometryEvidenceState.AGREE
for item in transformed.frame.geometries
)
assert transformed.frame.point_slab.owner_keys == (
"e35-timing-offset:track:7",
"geometry:8",
)
def test_outside_window_returns_exact_frame_with_inactive_journal() -> None:
frame = _frame(9)
transformed = transform_track_geometry_frame(
frame,
_scenario(
DegradationKind.CAMERA_LOSS,
{"drop_camera_observations": True},
),
)
assert transformed.frame is frame
assert transformed.transformation["active"] is False
assert transformed.transformation["original_frame_sha256"] == (
transformed.transformation["transformed_frame_sha256"]
)
def test_scenario_requires_exact_parameters_and_unsafe_offset_fails() -> None:
with pytest.raises(DegradationRecoveryError):
_scenario(DegradationKind.CAMERA_LOSS, {})
with pytest.raises(DegradationRecoveryError):
_scenario(
DegradationKind.TIMING_OFFSET,
{"camera_lidar_offset_ms": 100},
)
def test_source_frame_and_input_arrays_are_not_mutated() -> None:
frame = _frame()
before = copy.deepcopy(frame.to_dict())
before_points = frame.point_slab.points_xyz_m.copy()
transform_track_geometry_frame(
frame,
_scenario(
DegradationKind.CAMERA_LOSS,
{"drop_camera_observations": True},
),
)
assert frame.to_dict() == before
np.testing.assert_array_equal(frame.point_slab.points_xyz_m, before_points)
def test_frozen_profile_contains_every_degradation_once() -> None:
profile, scenarios = read_e35_degradation_profile(
Path("experiments/perception/e35_deterministic_degradation_profile.json")
)
assert profile["profile_id"] == "e35-six-channel-degradation-recovery/v1"
assert {scenario.kind for scenario in scenarios} == set(DegradationKind)
assert sum(scenario.frame_count for scenario in scenarios) == 360
def test_profile_rejects_authority_and_policy_relaxation(
tmp_path: Path,
) -> None:
source = Path(
"experiments/perception/e35_deterministic_degradation_profile.json"
)
profile = json.loads(source.read_text(encoding="utf-8"))
profile["policy"]["late_results_may_reenter"] = True
path = tmp_path / "relaxed.json"
path.write_text(json.dumps(profile), encoding="utf-8")
with pytest.raises(E35DegradationReplayError):
read_e35_degradation_profile(path)