Добавление канонического графа M4.7

This commit is contained in:
DCCONSTRUCTIONS
2026-08-23 20:03:40 +03:00
parent 51bb1369eb
commit d50d3bb3d3
18 changed files with 2538 additions and 110 deletions
+180
View File
@@ -5,6 +5,7 @@ import threading
from collections.abc import Iterator
from dataclasses import replace
from pathlib import Path
from queue import Queue
from threading import Event
import numpy as np
@@ -36,9 +37,12 @@ from k1link.perception.contracts import (
TimestampBundle,
)
from k1link.perception.graph import (
GraphExecutionError,
GraphRunMode,
GraphRunResult,
GraphState,
ReferencePerceptionGraphV1,
ReferencePerceptionGraphV2,
TerminalOutcomeType,
)
from k1link.perception.providers import (
@@ -47,6 +51,7 @@ from k1link.perception.providers import (
ProviderRole,
QueuePolicy,
ReferencePerceptionGraphConfig,
ReferencePerceptionGraphConfigV2,
SourcePacket,
)
from k1link.perception.recorded_source import (
@@ -230,6 +235,44 @@ class _Motion:
return obstacles
class _Rolling:
provider_id = "test-rolling/v1"
def update(
self,
packet: SourcePacket,
obstacles: tuple[TemporalObstacle, ...],
) -> tuple[TemporalObstacle, ...]:
if packet.envelope.sequence == 0:
return ()
return (
TemporalObstacle(
component_id=f"rolling-{packet.envelope.sequence}",
identity_scope="ephemeral",
state=TemporalState.RETAINED,
ttl_ns=3_000_000_000,
last_hit_ns=packet.envelope.timestamps.source_ns - 100_000_000,
age_ns=100_000_000,
association_basis="registered-map-increment-retention",
history=(
HistorySample(
frame_id=f"frame-{packet.envelope.sequence - 1:06d}",
evidence_time_ns=(
packet.envelope.timestamps.source_ns - 100_000_000
),
centroid_xyz_m=(3.0, 0.5, 0.5),
),
),
cells=(GridCell(99, packet.envelope.sequence, 0),),
coordinate_frame="map",
last_centroid_xyz_m=(3.0, 0.5, 0.5),
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
motion_reason="retained-map-increment-no-current-motion",
),
)
class _Threat:
provider_id = "test-threat/v1"
@@ -312,6 +355,39 @@ def _config(
)
def _config_v2(
capacity: int = 8,
terminal_timeout_ns: int = 500_000_000,
) -> ReferencePerceptionGraphConfigV2:
ids = {
ProviderRole.SOURCE: _Source.provider_id,
ProviderRole.DETECTOR: _Detector.provider_id,
ProviderRole.GEOMETRY: _Geometry.provider_id,
ProviderRole.TEMPORAL: _Temporal.provider_id,
ProviderRole.MOTION: _Motion.provider_id,
ProviderRole.ROLLING: _Rolling.provider_id,
ProviderRole.THREAT: _Threat.provider_id,
}
return ReferencePerceptionGraphConfigV2(
graph_id="reference-perception-graph/v2",
source_profile_id=BASELINE_PROFILE_ID,
providers=tuple(
ProviderPin(role, provider_id, "v1", "test-revision", "b" * 64)
for role, provider_id in ids.items()
),
queues=tuple(
QueuePolicy(
stage,
capacity,
min(80_000_000, terminal_timeout_ns),
terminal_timeout_ns,
)
for stage in ("detector", "geometry", "temporal", "rolling", "threat")
),
authority=GraphAuthority(),
)
class _MemoryTelemetry:
def __init__(self) -> None:
self.records: list[dict[str, object]] = []
@@ -358,6 +434,28 @@ def _graph(
)
def _graph_v2(
source: object,
*,
detector: object | None = None,
capacity: int = 8,
run_mode: GraphRunMode = GraphRunMode.LOSSLESS_REPLAY,
terminal_timeout_ns: int = 500_000_000,
) -> ReferencePerceptionGraphV2:
return ReferencePerceptionGraphV2(
config=_config_v2(capacity, terminal_timeout_ns),
source=source,
detector=detector or _Detector(),
geometry=_Geometry(),
temporal=_Temporal(),
motion=_Motion(),
rolling=_Rolling(),
threat=_Threat(),
run_mode=run_mode,
clock_ns=lambda: 10_000,
)
def test_reference_graph_closes_accounting_telemetry_and_deterministic_digest() -> None:
telemetry = _MemoryTelemetry()
graph = _graph(_Source((_packet(0), _packet(1))), telemetry=telemetry)
@@ -386,6 +484,88 @@ def test_reference_graph_closes_accounting_telemetry_and_deterministic_digest()
assert {"detector", "geometry", "temporal", "threat"}.issubset(stage_ids)
def test_reference_graph_v2_publishes_current_and_retained_occupancy() -> None:
result = _graph_v2(_Source((_packet(0), _packet(1)))).run()
assert result.graph_id == "reference-perception-graph/v2"
assert result.run_mode is GraphRunMode.LOSSLESS_REPLAY
assert [item.outcome for item in result.terminal_outcomes] == [
TerminalOutcomeType.DELIVERED,
TerminalOutcomeType.DELIVERED,
]
second = result.deliveries[1].obstacle_map
assert [item.state for item in second.occupied] == [
TemporalState.CURRENT,
TemporalState.RETAINED,
]
assert second.unknown == ()
assert set(dict(result.queue_high_watermarks)) == {
"detector",
"geometry",
"temporal",
"rolling",
"threat",
}
def test_reference_graph_v2_lossless_mode_applies_bounded_backpressure() -> None:
release = Event()
class BlockingDetector(_Detector):
def detect(self, packet: SourcePacket) -> tuple[ObjectProposal2D, ...]:
if packet.envelope.sequence == 0:
assert release.wait(2)
return super().detect(packet)
BlockingDetector.provider_id = _Detector.provider_id
graph = _graph_v2(
_Source(tuple(_packet(index) for index in range(4))),
detector=BlockingDetector(),
capacity=1,
)
holder: list[GraphRunResult] = []
runner = threading.Thread(target=lambda: holder.append(graph.run()))
runner.start()
release.set()
runner.join(3)
assert not runner.is_alive()
assert len(holder[0].terminal_outcomes) == 4
assert all(
item.outcome is TerminalOutcomeType.DELIVERED
for item in holder[0].terminal_outcomes
)
def test_reference_graph_v2_restart_requires_fresh_stateful_providers() -> None:
graph = _graph_v2(_Source((_packet(0),)))
assert graph.run().state is GraphState.STOPPED
with pytest.raises(
GraphExecutionError,
match="restart requires freshly instantiated providers",
):
graph.run()
def test_reference_graph_v2_terminal_timeout_fails_stranded_packet() -> None:
packet = _packet(0)
graph = _graph_v2(
_Source(()),
capacity=1,
terminal_timeout_ns=1,
)
queue = Queue(maxsize=1)
queue.put_nowait(packet)
graph._put_stop(queue, "detector")
outcome = graph._outcomes[0]
assert outcome.outcome is TerminalOutcomeType.FAILED
assert outcome.reason == "terminal-queue-timeout"
assert queue.qsize() == 1
def test_reference_graph_marks_unavailable_stale_and_provider_failure() -> None:
class FailingDetector(_Detector):
def detect(self, packet: SourcePacket) -> tuple[ObjectProposal2D, ...]: