feat(perception): add bounded reference graph
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""Immutable lifecycle and result documents for the reference perception graph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Final
|
||||
|
||||
from .contracts import LocalObstacleMap, ThreatAssessment
|
||||
|
||||
GRAPH_RESULT_SCHEMA: Final = "missioncore.reference-perception-graph-result/v1"
|
||||
TERMINAL_OUTCOME_SCHEMA: Final = "missioncore.perception-terminal-outcome/v1"
|
||||
REFERENCE_GRAPH_ID: Final = "reference-perception-graph/v1"
|
||||
_SAFE_IDENTIFIER: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$")
|
||||
|
||||
|
||||
class GraphExecutionError(RuntimeError):
|
||||
"""The graph cannot preserve its bounded lifecycle or product accounting."""
|
||||
|
||||
|
||||
class GraphState(StrEnum):
|
||||
CREATED = "created"
|
||||
STARTING = "starting"
|
||||
RUNNING = "running"
|
||||
STOPPING = "stopping"
|
||||
STOPPED = "stopped"
|
||||
CANCELLED = "cancelled"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class TerminalOutcomeType(StrEnum):
|
||||
DELIVERED = "delivered"
|
||||
SUPERSEDED = "superseded"
|
||||
STALE = "stale"
|
||||
REJECTED = "rejected"
|
||||
FAILED = "failed"
|
||||
UNAVAILABLE = "unavailable"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TerminalOutcome:
|
||||
source_id: str
|
||||
session_id: str
|
||||
frame_id: str
|
||||
sequence: int
|
||||
outcome: TerminalOutcomeType
|
||||
stage_id: str
|
||||
reason: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for value in (self.source_id, self.session_id, self.frame_id, self.stage_id, self.reason):
|
||||
if _SAFE_IDENTIFIER.fullmatch(value) is None:
|
||||
raise GraphExecutionError("terminal outcome contains an unsafe identifier")
|
||||
if self.sequence < 0:
|
||||
raise GraphExecutionError("terminal sequence must be nonnegative")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": TERMINAL_OUTCOME_SCHEMA,
|
||||
"source_id": self.source_id,
|
||||
"session_id": self.session_id,
|
||||
"frame_id": self.frame_id,
|
||||
"sequence": self.sequence,
|
||||
"outcome": self.outcome.value,
|
||||
"stage_id": self.stage_id,
|
||||
"reason": self.reason,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DeliveredFrame:
|
||||
sequence: int
|
||||
obstacle_map: LocalObstacleMap
|
||||
threats: tuple[ThreatAssessment, ...]
|
||||
|
||||
def canonical_dict(self) -> dict[str, object]:
|
||||
obstacle_map = self.obstacle_map.to_dict()
|
||||
obstacle_map.pop("generated_monotonic_ns")
|
||||
obstacle_map.pop("output_age_ns")
|
||||
return {
|
||||
"sequence": self.sequence,
|
||||
"obstacle_map": obstacle_map,
|
||||
"threats": [item.to_dict() for item in self.threats],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GraphRunResult:
|
||||
graph_id: str
|
||||
source_profile_id: str
|
||||
state: GraphState
|
||||
admitted_count: int
|
||||
terminal_outcomes: tuple[TerminalOutcome, ...]
|
||||
deliveries: tuple[DeliveredFrame, ...]
|
||||
canonical_payload_sha256: str
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": GRAPH_RESULT_SCHEMA,
|
||||
"graph_id": self.graph_id,
|
||||
"source_profile_id": self.source_profile_id,
|
||||
"state": self.state.value,
|
||||
"admitted_count": self.admitted_count,
|
||||
"terminal_outcomes": [item.to_dict() for item in self.terminal_outcomes],
|
||||
"deliveries": [item.canonical_dict() for item in self.deliveries],
|
||||
"canonical_payload_sha256": self.canonical_payload_sha256,
|
||||
}
|
||||
|
||||
|
||||
def build_graph_run_result(
|
||||
*,
|
||||
graph_id: str,
|
||||
source_profile_id: str,
|
||||
state: GraphState,
|
||||
admitted_count: int,
|
||||
outcomes: tuple[TerminalOutcome, ...],
|
||||
deliveries: tuple[DeliveredFrame, ...],
|
||||
) -> GraphRunResult:
|
||||
canonical = {
|
||||
"graph_id": graph_id,
|
||||
"source_profile_id": source_profile_id,
|
||||
"terminal_outcomes": [item.to_dict() for item in outcomes],
|
||||
"deliveries": [item.canonical_dict() for item in deliveries],
|
||||
}
|
||||
payload = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode()
|
||||
return GraphRunResult(
|
||||
graph_id=graph_id,
|
||||
source_profile_id=source_profile_id,
|
||||
state=state,
|
||||
admitted_count=admitted_count,
|
||||
terminal_outcomes=outcomes,
|
||||
deliveries=deliveries,
|
||||
canonical_payload_sha256=hashlib.sha256(payload).hexdigest(),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"GRAPH_RESULT_SCHEMA",
|
||||
"REFERENCE_GRAPH_ID",
|
||||
"TERMINAL_OUTCOME_SCHEMA",
|
||||
"DeliveredFrame",
|
||||
"GraphExecutionError",
|
||||
"GraphRunResult",
|
||||
"GraphState",
|
||||
"TerminalOutcome",
|
||||
"TerminalOutcomeType",
|
||||
"build_graph_run_result",
|
||||
]
|
||||
Reference in New Issue
Block a user