simulo-interfaces 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simulo/interfaces/__init__.py +65 -0
- simulo/interfaces/exceptions.py +31 -0
- simulo/interfaces/ids.py +21 -0
- simulo/interfaces/platform/__init__.py +120 -0
- simulo/interfaces/platform/app.py +47 -0
- simulo/interfaces/platform/asset.py +15 -0
- simulo/interfaces/platform/callbacks.py +17 -0
- simulo/interfaces/platform/debug.py +31 -0
- simulo/interfaces/platform/domain.py +118 -0
- simulo/interfaces/platform/enums.py +49 -0
- simulo/interfaces/platform/runs.py +112 -0
- simulo/interfaces/platform/runtime.py +19 -0
- simulo/interfaces/platform/submit.py +538 -0
- simulo/interfaces/platform/volume.py +13 -0
- simulo/interfaces/py.typed +0 -0
- simulo/interfaces/runtime/__init__.py +50 -0
- simulo/interfaces/runtime/anomaly.py +195 -0
- simulo/interfaces/runtime/components.py +35 -0
- simulo/interfaces/runtime/env.py +27 -0
- simulo/interfaces/runtime/player.py +19 -0
- simulo/interfaces/runtime/policy.py +16 -0
- simulo/interfaces/runtime/scenario.py +13 -0
- simulo/interfaces/runtime/task.py +21 -0
- simulo/interfaces/runtime/tensors.py +12 -0
- simulo/interfaces/runtime/trainer.py +37 -0
- simulo_interfaces-0.1.0.dist-info/METADATA +166 -0
- simulo_interfaces-0.1.0.dist-info/RECORD +29 -0
- simulo_interfaces-0.1.0.dist-info/WHEEL +5 -0
- simulo_interfaces-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Anomaly-capture contracts (``DebugOnAnomaly``) — the *local* half of Rule #10.
|
|
2
|
+
|
|
3
|
+
Training/inference anomaly detection + capture is a **runtime** concern: the
|
|
4
|
+
backend trainer/env detect an anomalous training signal (non-finite loss,
|
|
5
|
+
reward collapse, physics/observation explosion) and capture a bounded MCAP
|
|
6
|
+
debug session locally, via the same recording seam — and therefore the same
|
|
7
|
+
storage/retrieval path — as policy rollout recordings.
|
|
8
|
+
|
|
9
|
+
The *remote* debug/replay sessions (attach to a live cloud job, replay a
|
|
10
|
+
recorded run, clone-to-debug) remain the FORWARD Protocols in
|
|
11
|
+
:mod:`simulo.interfaces.platform.debug` (``DebugSessionProtocol`` et al.);
|
|
12
|
+
their semantics depend on the cloud debug-session design, which does not exist
|
|
13
|
+
yet. :class:`AnomalyDebugSessionProtocol` here is deliberately narrower — a
|
|
14
|
+
handle to a completed local capture artifact — and is NOT a replacement for
|
|
15
|
+
the platform ``DebugSessionProtocol``.
|
|
16
|
+
|
|
17
|
+
Torch-free by construction (Rule #11): thresholds and signal values are plain
|
|
18
|
+
floats; the tensor-facing detection lives in the implementer package
|
|
19
|
+
(``simulo.recording.anomaly`` in ``simulo-backend``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import math
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from enum import StrEnum
|
|
27
|
+
from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AnomalyKind(StrEnum):
|
|
31
|
+
"""Kind of a detected training/inference anomaly.
|
|
32
|
+
|
|
33
|
+
Values are a stable wire ABI (they appear in MCAP debug-session payloads
|
|
34
|
+
and file names): append-only, never rename or renumber.
|
|
35
|
+
|
|
36
|
+
The 4-kind set is intentional: a finite-gradient check (``NON_FINITE_GRAD``)
|
|
37
|
+
is a deliberate deferral, to be appended when a detection site exists.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
NON_FINITE_LOSS = "non_finite_loss"
|
|
41
|
+
NON_FINITE_REWARD = "non_finite_reward"
|
|
42
|
+
REWARD_COLLAPSE = "reward_collapse"
|
|
43
|
+
OBSERVATION_EXPLOSION = "observation_explosion"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, kw_only=True)
|
|
47
|
+
class AnomalySignal:
|
|
48
|
+
"""A single detected anomaly, as handed to the capture path.
|
|
49
|
+
|
|
50
|
+
``value``/``baseline`` are plain floats and MAY be non-finite (a NaN loss
|
|
51
|
+
signal carries the NaN) — serializers must JSON-stringify them rather than
|
|
52
|
+
emitting raw NaN/Inf tokens.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
kind: AnomalyKind
|
|
56
|
+
message: str
|
|
57
|
+
step_index: int | None = None
|
|
58
|
+
iteration: int | None = None
|
|
59
|
+
value: float | None = None
|
|
60
|
+
baseline: float | None = None
|
|
61
|
+
env_indices: tuple[int, ...] = ()
|
|
62
|
+
detected_at: str = "" # ISO 8601 UTC, stamped by the detector
|
|
63
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, kw_only=True)
|
|
67
|
+
class DebugOnAnomaly:
|
|
68
|
+
"""Configuration for anomaly detection + local MCAP debug-session capture.
|
|
69
|
+
|
|
70
|
+
The feature is **off by default** at the integration point: a trainer
|
|
71
|
+
constructed without a ``DebugOnAnomaly`` (or with ``enabled=False``) runs
|
|
72
|
+
the exact pre-feature code path — no monitor object exists, no buffering
|
|
73
|
+
happens, and no file is ever written.
|
|
74
|
+
|
|
75
|
+
Signals (deliberately not a bare threshold hack — each maps to a distinct
|
|
76
|
+
failure mode with its own detection site):
|
|
77
|
+
|
|
78
|
+
* ``NON_FINITE_LOSS`` — NaN/Inf policy or value loss mean at a trainer
|
|
79
|
+
chunk boundary (``check_losses``).
|
|
80
|
+
* ``NON_FINITE_REWARD`` — NaN/Inf reward at an env step, or a non-finite
|
|
81
|
+
mean episode reward at a chunk boundary.
|
|
82
|
+
* ``REWARD_COLLAPSE`` — the chunk's mean episode reward drops by at least
|
|
83
|
+
``collapse_drop_fraction`` of the running best baseline (the same
|
|
84
|
+
best-checkpoint signal the trainer already tracks), sustained for
|
|
85
|
+
``collapse_patience`` consecutive chunk boundaries
|
|
86
|
+
(``check_reward_collapse``).
|
|
87
|
+
* ``OBSERVATION_EXPLOSION`` — non-finite observations, or observations
|
|
88
|
+
whose magnitude exceeds ``observation_limit``, at an env step
|
|
89
|
+
(``check_observations``).
|
|
90
|
+
|
|
91
|
+
Video evidence (``video``): naming a camera sensor (e.g.
|
|
92
|
+
``video="side_cam"``) additionally buffers that camera's frames for the
|
|
93
|
+
FIRST watched env (``env_indices[0]``) alongside the numeric window, and
|
|
94
|
+
a capture writes them into the same debug MCAP (``/debug/window/video``
|
|
95
|
+
plus a ``foxglove.CompressedVideo`` sibling) so a viewer shows what the
|
|
96
|
+
sim looked like in the window ending at the anomalous step.
|
|
97
|
+
|
|
98
|
+
Honest costs of ``video`` (both opt-in, like the monitor itself):
|
|
99
|
+
|
|
100
|
+
* attaching a camera to a training env forces the render pipeline ON
|
|
101
|
+
during training (the env must be built with cameras enabled), which
|
|
102
|
+
slows every step whether or not an anomaly ever fires;
|
|
103
|
+
* the frame ring buffer holds up to ``window_steps`` raw RGB frames in
|
|
104
|
+
host memory — ``window_steps x H x W x 3`` bytes (64 frames of
|
|
105
|
+
640x480 is ~56 MiB). Size ``window_steps`` and the camera resolution
|
|
106
|
+
together.
|
|
107
|
+
|
|
108
|
+
``video=None`` (the default) buffers no frames, resolves no camera, and
|
|
109
|
+
changes nothing about the numeric-only behavior.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
enabled: bool = True
|
|
113
|
+
output_dir: str = "runs/debug"
|
|
114
|
+
# Bounded pre-anomaly evidence window (in checked env steps) captured
|
|
115
|
+
# into the debug MCAP.
|
|
116
|
+
window_steps: int = 256
|
|
117
|
+
# Env indices whose full observation/reward rows are buffered (aggregate
|
|
118
|
+
# stats always cover all envs).
|
|
119
|
+
env_indices: Sequence[int] = (0,)
|
|
120
|
+
# --- env-step signals -------------------------------------------------
|
|
121
|
+
check_observations: bool = True
|
|
122
|
+
# Out-of-envelope magnitude threshold; None disables the envelope check
|
|
123
|
+
# (non-finite detection stays on while check_observations is True).
|
|
124
|
+
observation_limit: float | None = 1e6
|
|
125
|
+
# Check (and buffer) every Nth env step; 1 = every step.
|
|
126
|
+
check_every_n_steps: int = 1
|
|
127
|
+
# Name of ONE camera sensor whose frames are buffered as video evidence
|
|
128
|
+
# for env_indices[0] (see the class docstring for the honest costs).
|
|
129
|
+
# None (the default) disables video evidence entirely.
|
|
130
|
+
video: str | None = None
|
|
131
|
+
# --- chunk-boundary signals -------------------------------------------
|
|
132
|
+
check_losses: bool = True
|
|
133
|
+
check_reward_collapse: bool = True
|
|
134
|
+
# Relative drop vs the running best baseline that counts as a collapse:
|
|
135
|
+
# (best - current) / max(|best|, collapse_baseline_floor).
|
|
136
|
+
collapse_drop_fraction: float = 0.5
|
|
137
|
+
# Consecutive collapsed chunk boundaries required before capturing.
|
|
138
|
+
collapse_patience: int = 2
|
|
139
|
+
# Denominator floor so a near-zero best baseline cannot divide away.
|
|
140
|
+
collapse_baseline_floor: float = 1e-6
|
|
141
|
+
# Total capture budget; the monitor disarms after this many captures.
|
|
142
|
+
max_captures: int = 1
|
|
143
|
+
|
|
144
|
+
def __post_init__(self) -> None:
|
|
145
|
+
object.__setattr__(self, "env_indices", tuple(int(i) for i in self.env_indices))
|
|
146
|
+
for i in self.env_indices:
|
|
147
|
+
if i < 0:
|
|
148
|
+
raise ValueError(f"env_indices must be non-negative, got {i}.")
|
|
149
|
+
if self.window_steps < 1:
|
|
150
|
+
raise ValueError(f"window_steps must be >= 1, got {self.window_steps}.")
|
|
151
|
+
if self.check_every_n_steps < 1:
|
|
152
|
+
raise ValueError(f"check_every_n_steps must be >= 1, got {self.check_every_n_steps}.")
|
|
153
|
+
if self.max_captures < 1:
|
|
154
|
+
raise ValueError(f"max_captures must be >= 1, got {self.max_captures}.")
|
|
155
|
+
if self.collapse_patience < 1:
|
|
156
|
+
raise ValueError(f"collapse_patience must be >= 1, got {self.collapse_patience}.")
|
|
157
|
+
if not (0.0 < self.collapse_drop_fraction <= 1.0):
|
|
158
|
+
raise ValueError(f"collapse_drop_fraction must be in (0, 1], got {self.collapse_drop_fraction}.")
|
|
159
|
+
if not (math.isfinite(self.collapse_baseline_floor) and self.collapse_baseline_floor > 0.0):
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"collapse_baseline_floor must be a finite positive float, got {self.collapse_baseline_floor}."
|
|
162
|
+
)
|
|
163
|
+
if self.observation_limit is not None and not (
|
|
164
|
+
math.isfinite(self.observation_limit) and self.observation_limit > 0.0
|
|
165
|
+
):
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"observation_limit must be a finite positive float or None, got {self.observation_limit}."
|
|
168
|
+
)
|
|
169
|
+
if self.video is not None and (not isinstance(self.video, str) or not self.video.strip()):
|
|
170
|
+
raise ValueError(f"video must be a non-empty camera sensor name or None, got {self.video!r}.")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@runtime_checkable
|
|
174
|
+
class AnomalyDebugSessionProtocol(Protocol):
|
|
175
|
+
"""Handle to a completed local anomaly capture (an MCAP debug session).
|
|
176
|
+
|
|
177
|
+
Minimal by design: the artifact is a plain MCAP file a user opens in
|
|
178
|
+
Lichtblick / Foxglove or reads with the ``mcap`` package. Interactive
|
|
179
|
+
remote debug sessions are the FORWARD ``DebugSessionProtocol`` in
|
|
180
|
+
:mod:`simulo.interfaces.platform.debug`.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def signal(self) -> AnomalySignal:
|
|
185
|
+
"""The anomaly that triggered this capture."""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def mcap_path(self) -> str:
|
|
190
|
+
"""Path of the written MCAP debug-session file."""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
def close(self) -> None:
|
|
194
|
+
"""Release any resources held by the session (idempotent)."""
|
|
195
|
+
...
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Observation / reward / termination component Protocols (Part A).
|
|
2
|
+
|
|
3
|
+
Structural mirrors of ``simulo.core.observations.ObservationComponent``,
|
|
4
|
+
``simulo.core.rewards.RewardComponent``, and
|
|
5
|
+
``simulo.core.terminations.TerminationCondition``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from simulo.interfaces.runtime.tensors import TensorLike
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class ObservationComponentProtocol(Protocol):
|
|
15
|
+
"""Mirror of ``simulo.core.observations.ObservationComponent``."""
|
|
16
|
+
|
|
17
|
+
def initialize(self, robot: Any, device: str, num_envs: int) -> None: ...
|
|
18
|
+
def dim(self, robot: Any) -> int: ...
|
|
19
|
+
def compute(self, prev_actions: TensorLike) -> TensorLike: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class RewardComponentProtocol(Protocol):
|
|
24
|
+
"""Mirror of ``simulo.core.rewards.RewardComponent``."""
|
|
25
|
+
|
|
26
|
+
def initialize(self, robot: Any, device: str, num_envs: int) -> None: ...
|
|
27
|
+
def compute(self, prev_actions: TensorLike, terminated: Optional[TensorLike]) -> TensorLike: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class TerminationConditionProtocol(Protocol):
|
|
32
|
+
"""Mirror of ``simulo.core.terminations.TerminationCondition``."""
|
|
33
|
+
|
|
34
|
+
def initialize(self, robot: Any, device: str, num_envs: int) -> None: ...
|
|
35
|
+
def check(self) -> TensorLike: ...
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""``LearningEnvProtocol`` — structural mirror of ``simulo.core.env.LearningEnv`` (Part A)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Protocol, Tuple, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from simulo.interfaces.runtime.tensors import TensorLike
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class LearningEnvProtocol(Protocol):
|
|
10
|
+
"""Gym-like vectorized env. Mirror of ``simulo.core.env.LearningEnv``."""
|
|
11
|
+
|
|
12
|
+
# Plain data member (not a read-only property): the implementation,
|
|
13
|
+
# ``simulo.core.env.LearningEnv``, exposes ``num_envs`` as an instance
|
|
14
|
+
# attribute assigned in ``__init__``. Mirror fidelity matters under nominal
|
|
15
|
+
# conformance — a read-only property here would make the implementer's
|
|
16
|
+
# ``self.num_envs = num_envs`` an illegal assignment.
|
|
17
|
+
num_envs: int
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def device(self) -> str: ...
|
|
21
|
+
@property
|
|
22
|
+
def dt(self) -> float: ...
|
|
23
|
+
def reset(
|
|
24
|
+
self, seed: Optional[int] = ..., options: Optional[Dict[str, Any]] = ...
|
|
25
|
+
) -> Tuple[TensorLike, Dict[str, Any]]: ...
|
|
26
|
+
def step(self, actions: TensorLike) -> Tuple[TensorLike, TensorLike, TensorLike, TensorLike, Dict[str, Any]]: ...
|
|
27
|
+
def close(self) -> None: ...
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""``PlayerProtocol`` — structural mirror of ``simulo.core.player.Player`` (Part A)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class PlayerProtocol(Protocol):
|
|
8
|
+
"""Trained-policy playback. Mirror of ``simulo.core.player.Player``."""
|
|
9
|
+
|
|
10
|
+
def play(
|
|
11
|
+
self,
|
|
12
|
+
num_episodes: Optional[int] = ...,
|
|
13
|
+
num_steps: Optional[int] = ...,
|
|
14
|
+
real_time: bool = ...,
|
|
15
|
+
video_path: Optional[str] = ...,
|
|
16
|
+
video_length: int = ...,
|
|
17
|
+
deterministic: bool = ...,
|
|
18
|
+
) -> Dict[str, Any]: ...
|
|
19
|
+
def load(self, path: str) -> None: ...
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""``PolicyProtocol`` — structural mirror of ``simulo.core.policy.Policy`` (Part A)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from simulo.interfaces.runtime.tensors import TensorLike
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class PolicyProtocol(Protocol):
|
|
10
|
+
"""obs -> action callable. Mirror of ``simulo.core.policy.Policy``."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def input_dim(self) -> Optional[int]: ...
|
|
14
|
+
@property
|
|
15
|
+
def output_dim(self) -> Optional[int]: ...
|
|
16
|
+
def __call__(self, observation: TensorLike) -> TensorLike: ...
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""``ScenarioProtocol`` — structural mirror of ``simulo.scenario.Scenario`` (Part A)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class ScenarioProtocol(Protocol):
|
|
8
|
+
"""Interactive sim lifecycle. Mirror of ``simulo.scenario.Scenario``."""
|
|
9
|
+
|
|
10
|
+
def build(self, scene: Any) -> None: ...
|
|
11
|
+
def on_start(self) -> None: ...
|
|
12
|
+
def on_step(self) -> None: ...
|
|
13
|
+
def on_shutdown(self) -> None: ...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""``TaskProtocol`` — structural mirror of ``simulo.core.task.Task`` (Part A)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol, Tuple, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from simulo.interfaces.runtime.tensors import TensorLike
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class TaskProtocol(Protocol):
|
|
10
|
+
"""RL task contract — structural mirror of ``simulo.core.task.Task``."""
|
|
11
|
+
|
|
12
|
+
observation_dim: int
|
|
13
|
+
action_dim: int
|
|
14
|
+
|
|
15
|
+
def build(self, scene: Any) -> None: ...
|
|
16
|
+
def on_start(self, env: Any) -> None: ...
|
|
17
|
+
def get_observations(self) -> TensorLike: ...
|
|
18
|
+
def get_rewards(self) -> TensorLike: ...
|
|
19
|
+
def get_dones(self) -> Tuple[TensorLike, TensorLike]: ...
|
|
20
|
+
def apply_actions(self, actions: TensorLike) -> None: ...
|
|
21
|
+
def reset_idx(self, env_ids: TensorLike) -> None: ...
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Tensor-library-agnostic alias for the runtime contract (Part A).
|
|
2
|
+
|
|
3
|
+
The runtime Protocols describe tensor in/out method shapes without binding to
|
|
4
|
+
any concrete tensor library. We do NOT import ``torch`` — not even under
|
|
5
|
+
``TYPE_CHECKING`` — keeping the package importable with no torch installed AND
|
|
6
|
+
mypy free of torch stubs. A concrete ``torch.Tensor`` is assignable to/from
|
|
7
|
+
``Any``, so structural conformance of any real implementation still holds.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
TensorLike = Any
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""``TrainerProtocol`` — structural mirror of ``simulo.core.trainer.Trainer`` (Part A).
|
|
2
|
+
|
|
3
|
+
Also home of the narrower ``PolicyExporterProtocol``: policy export is an
|
|
4
|
+
*optional capability*, deliberately NOT a ``TrainerProtocol`` member. A Protocol
|
|
5
|
+
binds ALL implementations, and the Trainer contract must not promise what only
|
|
6
|
+
some trainers deliver — ``export_policy`` requires a deterministic, standalone
|
|
7
|
+
exportable action head, which the base ``simulo.core.trainer.Trainer`` (a
|
|
8
|
+
user-extensible ABC) does not guarantee. Implementations that CAN export (today:
|
|
9
|
+
``simulo.core.trainer.RLTrainer``) declare the capability protocol separately;
|
|
10
|
+
consumers feature-detect with ``isinstance(trainer, PolicyExporterProtocol)``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class TrainerProtocol(Protocol):
|
|
18
|
+
"""Training orchestration. Mirror of ``simulo.core.trainer.Trainer``."""
|
|
19
|
+
|
|
20
|
+
def train(self, max_iterations: int) -> Dict[str, Any]: ...
|
|
21
|
+
def evaluate(self, checkpoint: Optional[str] = ..., num_episodes: int = ...) -> Dict[str, Any]: ...
|
|
22
|
+
def save(self, path: str) -> None: ...
|
|
23
|
+
def load(self, path: str) -> None: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class PolicyExporterProtocol(Protocol):
|
|
28
|
+
"""Optional capability: export the trained policy as a standalone artifact.
|
|
29
|
+
|
|
30
|
+
``export_policy`` writes the policy's deterministic inference head to
|
|
31
|
+
``path`` and returns the path actually written. Mirror of
|
|
32
|
+
``simulo.core.trainer.RLTrainer.export_policy`` — see the module docstring
|
|
33
|
+
for why this is a separate protocol rather than a ``TrainerProtocol``
|
|
34
|
+
member.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def export_policy(self, path: str) -> str: ...
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simulo-interfaces
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Implementation-free contract layer for the Simulo platform — torch-free runtime/sim Protocols and platform/workspace domain contracts. Importable with no torch/numpy/gymnasium/Isaac installed.
|
|
5
|
+
Author-email: Simulo Team <team@simulo.ai>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://simulo.ai
|
|
8
|
+
Project-URL: Source, https://github.com/simulo-org/simulo-platform
|
|
9
|
+
Keywords: robotics,contracts,protocols,interfaces,simulo
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
21
|
+
Requires-Dist: isort>=5.13; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
25
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
26
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
27
|
+
Provides-Extra: release
|
|
28
|
+
Requires-Dist: commitizen>=3.27; extra == "release"
|
|
29
|
+
Requires-Dist: build>=1.2; extra == "release"
|
|
30
|
+
Requires-Dist: twine>=5.1; extra == "release"
|
|
31
|
+
|
|
32
|
+
# simulo-interfaces
|
|
33
|
+
|
|
34
|
+
The **implementation-free contract layer** for the Simulo platform. A single,
|
|
35
|
+
stable, torch-free, **zero-runtime-dependency** package that both Simulo SDK
|
|
36
|
+
distributions depend on:
|
|
37
|
+
|
|
38
|
+
- `simulo-backend` — the cloud GPU SDK (torch + IsaacLab + Isaac Sim).
|
|
39
|
+
- `simulo` — the future thin local client (no heavy deps).
|
|
40
|
+
|
|
41
|
+
Because both distributions share these contracts, `simulo-interfaces` is the one
|
|
42
|
+
place the runtime Protocols and the platform/workspace domain model are defined.
|
|
43
|
+
|
|
44
|
+
## What it is
|
|
45
|
+
|
|
46
|
+
A pure-stdlib set of `typing.Protocol` contracts, frozen dataclasses, enums,
|
|
47
|
+
`NewType` identifiers, and an exception hierarchy. Nothing here does any work —
|
|
48
|
+
it only describes shapes.
|
|
49
|
+
|
|
50
|
+
## What it is NOT
|
|
51
|
+
|
|
52
|
+
It imports **none** of: `torch`, `numpy`, `gymnasium`, IsaacLab / Isaac Sim, AWS
|
|
53
|
+
SDKs, HTTP clients, or database drivers. It performs **no** I/O, persistence, or
|
|
54
|
+
computation. It is **not** a dumping ground (Contract Rule #12) — only contracts
|
|
55
|
+
shared by more than one distribution belong here.
|
|
56
|
+
|
|
57
|
+
The central invariant (Contract Rule #11): this package must import on a laptop
|
|
58
|
+
with no torch/numpy/gymnasium/Isaac installed. Two tests enforce it:
|
|
59
|
+
`tests/test_import_without_torch.py` (no heavy module enters `sys.modules`) and
|
|
60
|
+
`tests/test_no_heavy_deps.py` (zero unconditional `Requires-Dist`).
|
|
61
|
+
|
|
62
|
+
## Two parts
|
|
63
|
+
|
|
64
|
+
### Part A — `simulo.interfaces.runtime`
|
|
65
|
+
|
|
66
|
+
Tensor-library-agnostic structural mirrors of the `simulo.core` runtime surface.
|
|
67
|
+
Tensors are typed as `TensorLike` (an alias for `Any`) so the contracts never
|
|
68
|
+
bind to a concrete tensor library.
|
|
69
|
+
|
|
70
|
+
- `TaskProtocol`, `PolicyProtocol`, `TrainerProtocol`, `PlayerProtocol`
|
|
71
|
+
- `LearningEnvProtocol`, `ScenarioProtocol`
|
|
72
|
+
- `ObservationComponentProtocol`, `RewardComponentProtocol`,
|
|
73
|
+
`TerminationConditionProtocol`
|
|
74
|
+
- `PolicyExporterProtocol` — the narrow *optional* export capability
|
|
75
|
+
(`export_policy(path) -> str`). Deliberately not a `TrainerProtocol` member:
|
|
76
|
+
a Protocol binds all implementations, and only some trainers (today
|
|
77
|
+
`simulo.core.trainer.RLTrainer`) can deliver a deterministic standalone
|
|
78
|
+
policy export. Consumers feature-detect with `isinstance(trainer,
|
|
79
|
+
PolicyExporterProtocol)`.
|
|
80
|
+
|
|
81
|
+
### Part B — `simulo.interfaces.platform`
|
|
82
|
+
|
|
83
|
+
The platform/workspace domain contracts:
|
|
84
|
+
|
|
85
|
+
- **Enums** — `JobStatus`, `ResumePolicy`, `ResourceKind`, `ArtifactKind`.
|
|
86
|
+
- **Domain dataclasses** (frozen, slotted, keyword-only) — `Tag`, `Project`,
|
|
87
|
+
`Package`, `Resource`, `Job`, `Checkpoint`, `TrainedModel`.
|
|
88
|
+
- **Cloud-execution Protocols** — `AppProtocol`, `JobFunctionProtocol`,
|
|
89
|
+
`RuntimeProtocol`, `AssetProtocol`, `VolumeProtocol`, `JobCallbackProtocol`,
|
|
90
|
+
`DebugSessionProtocol`, `ReplaySessionProtocol`, `VisualizationSessionProtocol`.
|
|
91
|
+
|
|
92
|
+
## Public import surface
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import simulo.interfaces as si
|
|
96
|
+
|
|
97
|
+
# exceptions + ids re-exported at the top level
|
|
98
|
+
si.SimuloError, si.ContractViolationError, si.JobFailedError
|
|
99
|
+
si.ProjectId, si.JobId, si.ResourceUri, si.Digest
|
|
100
|
+
|
|
101
|
+
# Part A
|
|
102
|
+
from simulo.interfaces.runtime import TaskProtocol, PolicyProtocol, LearningEnvProtocol
|
|
103
|
+
|
|
104
|
+
# Part B
|
|
105
|
+
from simulo.interfaces.platform import (
|
|
106
|
+
JobStatus, ResumePolicy, ResourceKind, ArtifactKind,
|
|
107
|
+
Project, Package, Job, Resource, Checkpoint, TrainedModel, Tag,
|
|
108
|
+
AppProtocol, RuntimeProtocol, AssetProtocol, VolumeProtocol,
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Conformance model
|
|
113
|
+
|
|
114
|
+
The contracts are `typing.Protocol`s — implementers conform **structurally**, no
|
|
115
|
+
inheritance required.
|
|
116
|
+
|
|
117
|
+
- **Static** — implementers (e.g. `simulo-backend`) are type-checked against
|
|
118
|
+
these Protocols by mypy. That static assertion is the primary conformance
|
|
119
|
+
gate and lives in the **implementer** package, not here.
|
|
120
|
+
- **Runtime** — every Protocol is `@runtime_checkable`, so `isinstance(obj,
|
|
121
|
+
SomeProtocol)` provides a lightweight smoke check (presence of members only —
|
|
122
|
+
not signatures). This package's tests use that smoke form.
|
|
123
|
+
- **Nominal (optional, opt-in per implementer)** — `simulo-backend`'s
|
|
124
|
+
`simulo.core` classes additionally *declare* the Part A Protocols as base
|
|
125
|
+
classes, so mypy drift-checks each implementation against its mirror at the
|
|
126
|
+
class-definition site. Nominal declaration never replaces the structural
|
|
127
|
+
contract — a third-party implementation that merely matches the signatures
|
|
128
|
+
still conforms.
|
|
129
|
+
|
|
130
|
+
## Domain-model invariants
|
|
131
|
+
|
|
132
|
+
- **Package ≠ Job** (Rule #5) — a `Package` is an immutable, content-addressed
|
|
133
|
+
source archive; a `Job` is one execution of a package and references it by id.
|
|
134
|
+
- **Resource ≠ Package** (Rule #6) — a `Resource` is a read-only, versioned,
|
|
135
|
+
registry-resolved mount (robot / environment / dataset / …).
|
|
136
|
+
- **Checkpoint ≠ TrainedModel** (Rule #8) — an intermediate training artifact is
|
|
137
|
+
a distinct type from the final, deployable model. Never collapse them.
|
|
138
|
+
- **Project is the anchor** (Rule #7) — every other domain object hangs off a
|
|
139
|
+
`Project`.
|
|
140
|
+
- **No packaged-skill type** (Rule #11) — there is deliberately no `Skill` /
|
|
141
|
+
`PackagedSkill` type here.
|
|
142
|
+
|
|
143
|
+
## Stability policy
|
|
144
|
+
|
|
145
|
+
- Enum **values** are a stable wire ABI: append-only, never renumber or rename.
|
|
146
|
+
- Dataclass fields and Protocol method signatures follow the same
|
|
147
|
+
breaking-change discipline as the rest of the contract layer.
|
|
148
|
+
- The package follows SemVer; additive changes (new optional field, new
|
|
149
|
+
Protocol, new enum member at the end) are minor/patch, removals/renames major.
|
|
150
|
+
|
|
151
|
+
## Requirements
|
|
152
|
+
|
|
153
|
+
- Python **>= 3.11** (uses `enum.StrEnum`). Tested on 3.11 and 3.12.
|
|
154
|
+
- Standard library only — **zero** runtime dependencies.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pip install -e ".[dev]"
|
|
160
|
+
ruff check src
|
|
161
|
+
black --check src tests
|
|
162
|
+
isort --check-only src tests
|
|
163
|
+
mypy src # strict
|
|
164
|
+
python -m build && twine check dist/*
|
|
165
|
+
pytest
|
|
166
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
simulo/interfaces/__init__.py,sha256=4AcE1wwcD89GMuMjgbCERUjpRLdveJGd869qlZMthy8,1877
|
|
2
|
+
simulo/interfaces/exceptions.py,sha256=UDK0k6SFf2uKqBGNCtOgdwGKqrR0EGX8JlT8k1-s7qM,1006
|
|
3
|
+
simulo/interfaces/ids.py,sha256=du-ODsdz7twbv9q1xL_KPFPOegh2yeZUig1bZFm-tWs,946
|
|
4
|
+
simulo/interfaces/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
simulo/interfaces/platform/__init__.py,sha256=a5JoCCmvetwIo7xLO6XQ-80zdOuTbTScEqY88oHQZtY,3970
|
|
6
|
+
simulo/interfaces/platform/app.py,sha256=1xO3ooZThI3H3mToFazCZyWZ2xMAzMPrxuLxnAz_wBk,1810
|
|
7
|
+
simulo/interfaces/platform/asset.py,sha256=udUdC-p46XhDOmu939YLno95zozLCHPpsTQXI9Z4CMg,467
|
|
8
|
+
simulo/interfaces/platform/callbacks.py,sha256=_RbzIvzEa2ZVn6KjYzOQ4z5EVV1gQorqSFcD-Hx_04Q,660
|
|
9
|
+
simulo/interfaces/platform/debug.py,sha256=b-_4NylxAO4CkgAriv0dt2RBgwkEBxv1MibT8YmlYqY,804
|
|
10
|
+
simulo/interfaces/platform/domain.py,sha256=VJaCf9bsoDAE9e0a0aXa1PHvaeNi3XNMplmNkzbF5GY,3286
|
|
11
|
+
simulo/interfaces/platform/enums.py,sha256=cUPa-nVOHKpYco9QTeuzIYH_KChiUNWlWbtIN4t39PM,1239
|
|
12
|
+
simulo/interfaces/platform/runs.py,sha256=gzzS-_p0WO3Odk2sgg2tbfbYmx4rahUVDBERBJbPJBk,5542
|
|
13
|
+
simulo/interfaces/platform/runtime.py,sha256=eyVQVTVHa0y_l85FMWkPJuPMLfD34_tSjxEbSrHVydg,751
|
|
14
|
+
simulo/interfaces/platform/submit.py,sha256=CFf4QTKa9Mp1JdzDt2wJsAHWsiqD7DMFEgekpEGe5sc,29298
|
|
15
|
+
simulo/interfaces/platform/volume.py,sha256=rCi0C3-VKFiBfxmgNfNtZ9rLQ7CGtYmyC2r6jenHpM4,388
|
|
16
|
+
simulo/interfaces/runtime/__init__.py,sha256=tv79QeafWvJ4EF-ew5QuzHLmYZaDyDjS2QiLqti8Et8,1952
|
|
17
|
+
simulo/interfaces/runtime/anomaly.py,sha256=SFaZs83WVCsr5upIDHo4iGFXC0kaqJrFxGbRrrVFI-Y,9012
|
|
18
|
+
simulo/interfaces/runtime/components.py,sha256=0XCZDk5em3K0TcrgxhqYvODUpVTtNeUOUpdC5uV8uts,1248
|
|
19
|
+
simulo/interfaces/runtime/env.py,sha256=q_LnPwJr6DM7i0ifkzYFnqlYfcyrEfINN_SdXgti58Q,1136
|
|
20
|
+
simulo/interfaces/runtime/player.py,sha256=1SldSTpbIV-l0c7nNg5BxKes5xV2NvLdOVuLy6BASEU,612
|
|
21
|
+
simulo/interfaces/runtime/policy.py,sha256=DY1vNV3FrI0FLqJQwosvVStsGrJIjRPX-0lkIl9vJiI,523
|
|
22
|
+
simulo/interfaces/runtime/scenario.py,sha256=Du0HHlmsg62wlCoVTSbTMbPRpvxeDNWZkPNx3ngKcZw,432
|
|
23
|
+
simulo/interfaces/runtime/task.py,sha256=Jts29KtMJNvcvf4ciNSux-XVkVNP6DiDzJ77LW38giM,747
|
|
24
|
+
simulo/interfaces/runtime/tensors.py,sha256=oA7wUpfIOXwT64rrF6W-TmRdJ800o_Y-auPn_VvL6E8,500
|
|
25
|
+
simulo/interfaces/runtime/trainer.py,sha256=x0D6eKz3NPHTBrdMOmbbUvxvwGs9fRuzfYKQq736v2U,1683
|
|
26
|
+
simulo_interfaces-0.1.0.dist-info/METADATA,sha256=lPwPsp_oBCGCg03i5QbN1DECQ4xy7a-6xLwKtaGe568,7028
|
|
27
|
+
simulo_interfaces-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
28
|
+
simulo_interfaces-0.1.0.dist-info/top_level.txt,sha256=qud9mL24_iIiqb0ZMUSRitgwjDv_3zxgVqlWY45swJI,7
|
|
29
|
+
simulo_interfaces-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simulo
|