Добавление канонического графа M4.7
This commit is contained in:
@@ -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, ...]:
|
||||
|
||||
Reference in New Issue
Block a user