feat(simulation): qualify S0 time and resource gates

This commit is contained in:
DCCONSTRUCTIONS
2026-07-24 17:19:28 +03:00
parent 6290fcb2ce
commit c9b1009259
10 changed files with 1205 additions and 23 deletions
+2
View File
@@ -3,6 +3,7 @@
from k1link.simulation.s0 import (
CheckStatus,
DoctorVerdict,
RuntimeAcceptance,
S0DoctorReport,
S0Profile,
S0ProfileError,
@@ -13,6 +14,7 @@ from k1link.simulation.s0 import (
__all__ = [
"CheckStatus",
"DoctorVerdict",
"RuntimeAcceptance",
"S0DoctorReport",
"S0Profile",
"S0ProfileError",
+107
View File
@@ -90,6 +90,21 @@ class RosProfile:
namespace: str
@dataclass(frozen=True, slots=True)
class RuntimeAcceptance:
world: str
model: str
physics_step_ns: int
pause_max_advance_ns: int
speed_factors: tuple[int, ...]
rtf_tolerance_percent: int
measurement_window_seconds: int
resource_sample_interval_milliseconds: int
max_owned_rss_mib: int
max_owned_cpu_percent: int
minimum_available_memory_gib: int
@dataclass(frozen=True, slots=True)
class ComponentPin:
identifier: str
@@ -126,6 +141,7 @@ class S0Profile:
clock: ClockPolicy
authority: AuthorityPolicy
ros: RosProfile
runtime_acceptance: RuntimeAcceptance
components: tuple[ComponentPin, ...]
ports: tuple[PortBinding, ...]
processes: tuple[ProcessOwnership, ...]
@@ -191,6 +207,7 @@ def load_s0_profile(path: Path) -> S0Profile:
"clock",
"authority",
"ros",
"runtime_acceptance",
"components",
"ports",
"processes",
@@ -208,6 +225,7 @@ def load_s0_profile(path: Path) -> S0Profile:
clock = _parse_clock(document["clock"])
authority = _parse_authority(document["authority"])
ros = _parse_ros(document["ros"])
runtime_acceptance = _parse_runtime_acceptance(document["runtime_acceptance"])
components = _parse_components(document["components"])
ports = _parse_ports(document["ports"])
processes = _parse_processes(document["processes"])
@@ -229,6 +247,7 @@ def load_s0_profile(path: Path) -> S0Profile:
clock=clock,
authority=authority,
ros=ros,
runtime_acceptance=runtime_acceptance,
components=components,
ports=ports,
processes=processes,
@@ -523,6 +542,94 @@ def _parse_ros(value: object) -> RosProfile:
return RosProfile(domain_id, namespace)
def _parse_runtime_acceptance(value: object) -> RuntimeAcceptance:
data = _mapping(value, "runtime_acceptance")
_expect_keys(
data,
required={
"world",
"model",
"physics_step_ns",
"pause_max_advance_ns",
"speed_factors",
"rtf_tolerance_percent",
"measurement_window_seconds",
"resource_sample_interval_milliseconds",
"max_owned_rss_mib",
"max_owned_cpu_percent",
"minimum_available_memory_gib",
},
context="runtime_acceptance",
)
speed_factors = tuple(
_integer(item, f"runtime_acceptance.speed_factors[{index}]")
for index, item in enumerate(
_sequence(
data["speed_factors"],
"runtime_acceptance.speed_factors",
)
)
)
acceptance = RuntimeAcceptance(
world=_safe_id(data["world"], "runtime_acceptance.world"),
model=_string(data["model"], "runtime_acceptance.model"),
physics_step_ns=_integer(
data["physics_step_ns"],
"runtime_acceptance.physics_step_ns",
),
pause_max_advance_ns=_integer(
data["pause_max_advance_ns"],
"runtime_acceptance.pause_max_advance_ns",
),
speed_factors=speed_factors,
rtf_tolerance_percent=_integer(
data["rtf_tolerance_percent"],
"runtime_acceptance.rtf_tolerance_percent",
),
measurement_window_seconds=_integer(
data["measurement_window_seconds"],
"runtime_acceptance.measurement_window_seconds",
),
resource_sample_interval_milliseconds=_integer(
data["resource_sample_interval_milliseconds"],
"runtime_acceptance.resource_sample_interval_milliseconds",
),
max_owned_rss_mib=_integer(
data["max_owned_rss_mib"],
"runtime_acceptance.max_owned_rss_mib",
),
max_owned_cpu_percent=_integer(
data["max_owned_cpu_percent"],
"runtime_acceptance.max_owned_cpu_percent",
),
minimum_available_memory_gib=_integer(
data["minimum_available_memory_gib"],
"runtime_acceptance.minimum_available_memory_gib",
),
)
if acceptance.world != "rover" or acceptance.model != "rover_ackermann_0":
raise S0ProfileError("S0 runtime acceptance must remain on the stock Ackermann rover")
if acceptance.physics_step_ns != 2_000_000:
raise S0ProfileError("S0 physics step must match the pinned rover world")
if acceptance.pause_max_advance_ns != 0:
raise S0ProfileError("S0 pause acceptance must require a frozen simulation clock")
if acceptance.speed_factors != (1, 2):
raise S0ProfileError("S0 must qualify exactly the 1x and 2x speed factors")
if not 1 <= acceptance.rtf_tolerance_percent <= 30:
raise S0ProfileError("S0 RTF tolerance must be within 1..30 percent")
if not 3 <= acceptance.measurement_window_seconds <= 30:
raise S0ProfileError("S0 RTF measurement window must be within 3..30 seconds")
if not 100 <= acceptance.resource_sample_interval_milliseconds <= 2_000:
raise S0ProfileError("S0 resource sample interval must be within 100..2000 ms")
if not 256 <= acceptance.max_owned_rss_mib <= 8_192:
raise S0ProfileError("S0 owned RSS budget must be within 256..8192 MiB")
if not 100 <= acceptance.max_owned_cpu_percent <= 3_200:
raise S0ProfileError("S0 owned CPU budget must be within 100..3200 percent")
if not 1 <= acceptance.minimum_available_memory_gib <= 64:
raise S0ProfileError("S0 available-memory floor must be within 1..64 GiB")
return acceptance
def _parse_components(value: object) -> tuple[ComponentPin, ...]:
items: list[ComponentPin] = []
seen: set[str] = set()