feat(simulation): qualify S0 time and resource gates
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user