roboscope-kit 0.8.0b1__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.
Files changed (71) hide show
  1. roboscope_kit/__init__.py +67 -0
  2. roboscope_kit/benchmark_specs.py +268 -0
  3. roboscope_kit/client.py +133 -0
  4. roboscope_kit/contracts.py +176 -0
  5. roboscope_kit/policy.py +348 -0
  6. roboscope_kit/policy_config.py +141 -0
  7. roboscope_kit/protocol.py +111 -0
  8. roboscope_kit/runtime/__init__.py +4 -0
  9. roboscope_kit/runtime/autotune.py +437 -0
  10. roboscope_kit/runtime/benchmark/__init__.py +60 -0
  11. roboscope_kit/runtime/benchmark/__main__.py +6 -0
  12. roboscope_kit/runtime/benchmark/adapters.py +99 -0
  13. roboscope_kit/runtime/benchmark/autotune.py +698 -0
  14. roboscope_kit/runtime/benchmark/checkpoint.py +131 -0
  15. roboscope_kit/runtime/benchmark/cli.py +590 -0
  16. roboscope_kit/runtime/benchmark/cuda_mps.py +284 -0
  17. roboscope_kit/runtime/benchmark/factories.py +57 -0
  18. roboscope_kit/runtime/benchmark/image_manifest.py +122 -0
  19. roboscope_kit/runtime/benchmark/legacy.py +198 -0
  20. roboscope_kit/runtime/benchmark/metrics.py +152 -0
  21. roboscope_kit/runtime/benchmark/orchestrator.py +844 -0
  22. roboscope_kit/runtime/benchmark/policy_process.py +221 -0
  23. roboscope_kit/runtime/benchmark/processes.py +378 -0
  24. roboscope_kit/runtime/benchmark/queue.py +172 -0
  25. roboscope_kit/runtime/benchmark/registry.py +41 -0
  26. roboscope_kit/runtime/benchmark/resources.py +138 -0
  27. roboscope_kit/runtime/benchmark/schema.py +104 -0
  28. roboscope_kit/runtime/benchmark/shard_merge.py +313 -0
  29. roboscope_kit/runtime/benchmark/shard_reduce.py +200 -0
  30. roboscope_kit/runtime/benchmark/shard_validation.py +94 -0
  31. roboscope_kit/runtime/benchmark/sharding.py +117 -0
  32. roboscope_kit/runtime/benchmark/suite_sizes.py +52 -0
  33. roboscope_kit/runtime/benchmark/timing.py +133 -0
  34. roboscope_kit/runtime/benchmark/worker.py +79 -0
  35. roboscope_kit/runtime/benchmarks/__init__.py +4 -0
  36. roboscope_kit/runtime/benchmarks/calvin/__init__.py +9 -0
  37. roboscope_kit/runtime/benchmarks/calvin/adapter.py +245 -0
  38. roboscope_kit/runtime/benchmarks/calvin/factory.py +114 -0
  39. roboscope_kit/runtime/benchmarks/calvin/worker.py +283 -0
  40. roboscope_kit/runtime/benchmarks/libero/__init__.py +9 -0
  41. roboscope_kit/runtime/benchmarks/libero/adapter.py +285 -0
  42. roboscope_kit/runtime/benchmarks/libero/factory.py +127 -0
  43. roboscope_kit/runtime/benchmarks/libero/worker.py +285 -0
  44. roboscope_kit/runtime/benchmarks/libero_plus/__init__.py +9 -0
  45. roboscope_kit/runtime/benchmarks/libero_plus/adapter.py +189 -0
  46. roboscope_kit/runtime/benchmarks/libero_plus/factory.py +130 -0
  47. roboscope_kit/runtime/benchmarks/metaworld/__init__.py +17 -0
  48. roboscope_kit/runtime/benchmarks/metaworld/adapter.py +351 -0
  49. roboscope_kit/runtime/benchmarks/metaworld/agent.py +177 -0
  50. roboscope_kit/runtime/benchmarks/metaworld/config.py +163 -0
  51. roboscope_kit/runtime/benchmarks/metaworld/factory.py +114 -0
  52. roboscope_kit/runtime/benchmarks/metaworld/instructions.py +268 -0
  53. roboscope_kit/runtime/benchmarks/metaworld/worker.py +337 -0
  54. roboscope_kit/runtime/benchmarks/robocasa/__init__.py +8 -0
  55. roboscope_kit/runtime/benchmarks/robocasa/adapter.py +229 -0
  56. roboscope_kit/runtime/benchmarks/robocasa/factory.py +120 -0
  57. roboscope_kit/runtime/benchmarks/robocasa/worker.py +218 -0
  58. roboscope_kit/runtime/benchmarks/robotwin/__init__.py +14 -0
  59. roboscope_kit/runtime/benchmarks/robotwin/adapter.py +198 -0
  60. roboscope_kit/runtime/benchmarks/robotwin/factory.py +232 -0
  61. roboscope_kit/runtime/benchmarks/robotwin/seed_cache.py +79 -0
  62. roboscope_kit/runtime/benchmarks/robotwin/seed_protocol.py +286 -0
  63. roboscope_kit/runtime/benchmarks/robotwin/worker.py +581 -0
  64. roboscope_kit/runtime/hil/__init__.py +23 -0
  65. roboscope_kit/runtime/hil/checkpoints.py +436 -0
  66. roboscope_kit/runtime/hil/lerobot_policy.py +224 -0
  67. roboscope_kit/server.py +245 -0
  68. roboscope_kit-0.8.0b1.dist-info/METADATA +490 -0
  69. roboscope_kit-0.8.0b1.dist-info/RECORD +71 -0
  70. roboscope_kit-0.8.0b1.dist-info/WHEEL +4 -0
  71. roboscope_kit-0.8.0b1.dist-info/licenses/LICENSE +203 -0
@@ -0,0 +1,67 @@
1
+ # Copyright 2026 RoboScope Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """RoboScope SDK for adapting custom inference code to official simulation."""
5
+
6
+ from roboscope_kit.benchmark_specs import (
7
+ ActionSpec,
8
+ BenchmarkName,
9
+ BenchmarkSpec,
10
+ FeatureKind,
11
+ FeatureSpec,
12
+ benchmark_names,
13
+ get_benchmark_spec,
14
+ )
15
+ from roboscope_kit.client import PolicyClient, PolicyRemoteError
16
+ from roboscope_kit.policy import (
17
+ ActionChunk,
18
+ EpisodeContext,
19
+ Observation,
20
+ Policy,
21
+ PolicyCapabilities,
22
+ PolicyExecutionContract,
23
+ SessionContext,
24
+ negotiate_policy_action_contract,
25
+ validate_action_chunk,
26
+ validate_observation,
27
+ validate_policy_capabilities,
28
+ )
29
+ from roboscope_kit.policy_config import (
30
+ POLICY_CONFIG_PATH,
31
+ POLICY_CONFIG_SCHEMA_VERSION,
32
+ PolicyConfigError,
33
+ PolicyRuntimeConfig,
34
+ load_policy_runtime_config,
35
+ )
36
+ from roboscope_kit.server import PolicyServer, PolicyState, run
37
+
38
+ __all__ = [
39
+ "ActionChunk",
40
+ "ActionSpec",
41
+ "BenchmarkName",
42
+ "BenchmarkSpec",
43
+ "EpisodeContext",
44
+ "FeatureKind",
45
+ "FeatureSpec",
46
+ "Observation",
47
+ "Policy",
48
+ "POLICY_CONFIG_PATH",
49
+ "POLICY_CONFIG_SCHEMA_VERSION",
50
+ "PolicyConfigError",
51
+ "PolicyRuntimeConfig",
52
+ "PolicyCapabilities",
53
+ "PolicyExecutionContract",
54
+ "PolicyClient",
55
+ "PolicyRemoteError",
56
+ "PolicyServer",
57
+ "PolicyState",
58
+ "SessionContext",
59
+ "benchmark_names",
60
+ "get_benchmark_spec",
61
+ "load_policy_runtime_config",
62
+ "negotiate_policy_action_contract",
63
+ "run",
64
+ "validate_action_chunk",
65
+ "validate_observation",
66
+ "validate_policy_capabilities",
67
+ ]
@@ -0,0 +1,268 @@
1
+ # Copyright 2026 RoboScope Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Built-in benchmark contracts exposed to user policy implementations."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from enum import Enum
10
+
11
+ from roboscope_kit.contracts import ActionSpec, BenchmarkSpec, FeatureKind, FeatureSpec
12
+
13
+
14
+ class BenchmarkName(str, Enum): # noqa: UP042 -- benchmark images include Python 3.10
15
+ LIBERO = "libero"
16
+ LIBERO_PLUS = "libero_plus"
17
+ ROBOCASA = "robocasa"
18
+ ROBOTWIN = "robotwin"
19
+ CALVIN = "calvin"
20
+ METAWORLD = "metaworld"
21
+
22
+
23
+ class BenchmarkDefinition(ABC):
24
+ """Internal provider for one platform-owned benchmark specification."""
25
+
26
+ name: BenchmarkName
27
+ protocol_version: str
28
+ supported_action_representations: tuple[str, ...]
29
+ default_action_representation: str
30
+
31
+ @abstractmethod
32
+ def features(self, suite: str) -> tuple[FeatureSpec, ...]: ...
33
+
34
+ @abstractmethod
35
+ def action(self, suite: str) -> ActionSpec: ...
36
+
37
+ def spec(self, suite: str) -> BenchmarkSpec:
38
+ if not suite:
39
+ raise ValueError("benchmark suite cannot be empty")
40
+ return BenchmarkSpec(
41
+ name=self.name.value,
42
+ suite=suite,
43
+ protocol_version=self.protocol_version,
44
+ features=self.features(suite),
45
+ action=self.action(suite),
46
+ supported_action_representations=self.supported_action_representations,
47
+ default_action_representation=self.default_action_representation,
48
+ )
49
+
50
+
51
+ def _feature(
52
+ name: str,
53
+ kind: FeatureKind,
54
+ description: str,
55
+ *,
56
+ required: bool = True,
57
+ shape: tuple[int | None, ...] | None = None,
58
+ ) -> FeatureSpec:
59
+ dtype = {
60
+ FeatureKind.IMAGE: "uint8",
61
+ FeatureKind.STATE: "float32",
62
+ FeatureKind.LANGUAGE: "string",
63
+ }[kind]
64
+ return FeatureSpec(
65
+ name=name,
66
+ kind=kind,
67
+ description=description,
68
+ required=required,
69
+ dtype=dtype,
70
+ shape=shape,
71
+ )
72
+
73
+
74
+ class LiberoBenchmarkDefinition(BenchmarkDefinition):
75
+ name = BenchmarkName.LIBERO
76
+ protocol_version = "libero-v1"
77
+ supported_action_representations = ("libero.delta-eef-v1",)
78
+ default_action_representation = "libero.delta-eef-v1"
79
+
80
+ def features(self, _suite: str) -> tuple[FeatureSpec, ...]:
81
+ return (
82
+ _feature(
83
+ "images.primary",
84
+ FeatureKind.IMAGE,
85
+ "Agent-view RGB image.",
86
+ shape=(256, 256, 3),
87
+ ),
88
+ _feature(
89
+ "images.wrist",
90
+ FeatureKind.IMAGE,
91
+ "Robot wrist RGB image.",
92
+ shape=(256, 256, 3),
93
+ ),
94
+ _feature(
95
+ "state.proprioception",
96
+ FeatureKind.STATE,
97
+ "EEF position/quaternion, gripper position, and seven joint positions.",
98
+ shape=(16,),
99
+ ),
100
+ _feature("language.instruction", FeatureKind.LANGUAGE, "Task instruction."),
101
+ )
102
+
103
+ def action(self, _suite: str) -> ActionSpec:
104
+ return ActionSpec("action.robot", "LIBERO 6-DoF end-effector delta and gripper command.", 7)
105
+
106
+
107
+ class LiberoPlusBenchmarkDefinition(LiberoBenchmarkDefinition):
108
+ name = BenchmarkName.LIBERO_PLUS
109
+ protocol_version = "libero-plus-v1"
110
+
111
+
112
+ class RoboCasaBenchmarkDefinition(BenchmarkDefinition):
113
+ name = BenchmarkName.ROBOCASA
114
+ protocol_version = "robocasa-v1"
115
+ supported_action_representations = ("robocasa.gr1-joint-v1",)
116
+ default_action_representation = "robocasa.gr1-joint-v1"
117
+
118
+ def features(self, _suite: str) -> tuple[FeatureSpec, ...]:
119
+ return (
120
+ _feature("images.primary", FeatureKind.IMAGE, "Primary workspace RGB image."),
121
+ _feature("images.wrist", FeatureKind.IMAGE, "Robot wrist RGB image.", required=False),
122
+ _feature("state.proprioception", FeatureKind.STATE, "RoboCasa embodiment state vector."),
123
+ _feature("language.instruction", FeatureKind.LANGUAGE, "Task instruction."),
124
+ )
125
+
126
+ def action(self, _suite: str) -> ActionSpec:
127
+ return ActionSpec("action.robot", "RoboCasa GR1 arms, hands, and waist action vector.", 29)
128
+
129
+
130
+ class RoboTwinBenchmarkDefinition(BenchmarkDefinition):
131
+ name = BenchmarkName.ROBOTWIN
132
+ protocol_version = "robotwin2-v1"
133
+ supported_action_representations = (
134
+ "robotwin.qpos-v1",
135
+ "robotwin.ee-compact-quat-v1",
136
+ )
137
+ default_action_representation = "robotwin.qpos-v1"
138
+
139
+ def features(self, _suite: str) -> tuple[FeatureSpec, ...]:
140
+ return (
141
+ _feature("images.head", FeatureKind.IMAGE, "Head-camera RGB image."),
142
+ _feature("images.left_wrist", FeatureKind.IMAGE, "Left wrist RGB image."),
143
+ _feature("images.right_wrist", FeatureKind.IMAGE, "Right wrist RGB image."),
144
+ _feature("state.joints", FeatureKind.STATE, "Dual-arm joint and gripper state."),
145
+ _feature("language.instruction", FeatureKind.LANGUAGE, "Task instruction."),
146
+ )
147
+
148
+ def action(self, _suite: str) -> ActionSpec:
149
+ return ActionSpec("action.robot", "Absolute dual-arm joint and gripper target.", 14)
150
+
151
+
152
+ class CalvinBenchmarkDefinition(BenchmarkDefinition):
153
+ name = BenchmarkName.CALVIN
154
+ protocol_version = "calvin-v1"
155
+ supported_action_representations = (
156
+ "calvin.relative-v1",
157
+ "calvin.absolute-compact-quat-v1",
158
+ )
159
+ default_action_representation = "calvin.relative-v1"
160
+
161
+ def features(self, _suite: str) -> tuple[FeatureSpec, ...]:
162
+ return (
163
+ _feature("images.static", FeatureKind.IMAGE, "Static scene RGB image."),
164
+ _feature("images.gripper", FeatureKind.IMAGE, "Gripper RGB image."),
165
+ _feature("state.proprioception", FeatureKind.STATE, "CALVIN robot state vector."),
166
+ _feature("language.instruction", FeatureKind.LANGUAGE, "Task instruction."),
167
+ )
168
+
169
+ def action(self, _suite: str) -> ActionSpec:
170
+ return ActionSpec("action.robot", "CALVIN relative end-effector and gripper action.", 7)
171
+
172
+
173
+ METAWORLD_DEFAULT_CAMERA = {
174
+ "feature": "images.primary",
175
+ "camera_name": "corner2",
176
+ "render_width": 480,
177
+ "render_height": 480,
178
+ "camera_position": None,
179
+ "rotation_deg": 180,
180
+ "center_crop_ratio": 1.0,
181
+ "output_width": 480,
182
+ "output_height": 480,
183
+ }
184
+
185
+
186
+ class MetaworldBenchmarkDefinition(BenchmarkDefinition):
187
+ name = BenchmarkName.METAWORLD
188
+ protocol_version = "metaworld-v1"
189
+ supported_action_representations = ("metaworld.delta-eef-v1",)
190
+ default_action_representation = "metaworld.delta-eef-v1"
191
+
192
+ def features(self, _suite: str, cameras: tuple[dict, ...] | None = None) -> tuple[FeatureSpec, ...]:
193
+ resolved = (METAWORLD_DEFAULT_CAMERA,) if cameras is None else cameras
194
+ features = [str(camera["feature"]) for camera in resolved]
195
+ if not features or len(set(features)) != len(features):
196
+ raise ValueError("metaworld camera features must be unique and non-empty")
197
+ image_features = tuple(
198
+ _feature(
199
+ feature,
200
+ FeatureKind.IMAGE,
201
+ f"MetaWorld RGB image from camera {camera['camera_name']!r}.",
202
+ shape=(int(camera["output_height"]), int(camera["output_width"]), 3),
203
+ )
204
+ for feature, camera in zip(features, resolved, strict=True)
205
+ )
206
+ return (
207
+ *image_features,
208
+ _feature(
209
+ "state.proprioception",
210
+ FeatureKind.STATE,
211
+ "End-effector xyz position and normalized gripper aperture (raw observation[:4]).",
212
+ shape=(4,),
213
+ ),
214
+ _feature("language.instruction", FeatureKind.LANGUAGE, "Official MetaWorld task instruction."),
215
+ )
216
+
217
+ def action(self, _suite: str) -> ActionSpec:
218
+ return ActionSpec(
219
+ "action.robot",
220
+ "MetaWorld delta end-effector position (dx, dy, dz) and gripper command in [-1, 1].",
221
+ 4,
222
+ )
223
+
224
+ def spec(self, suite: str, cameras: tuple[dict, ...] | None = None) -> BenchmarkSpec:
225
+ if not suite:
226
+ raise ValueError("benchmark suite cannot be empty")
227
+ return BenchmarkSpec(
228
+ name=self.name.value,
229
+ suite=suite,
230
+ protocol_version=self.protocol_version,
231
+ features=self.features(suite, cameras),
232
+ action=self.action(suite),
233
+ supported_action_representations=self.supported_action_representations,
234
+ default_action_representation=self.default_action_representation,
235
+ )
236
+
237
+
238
+ _DEFINITIONS: dict[BenchmarkName, BenchmarkDefinition] = {
239
+ definition.name: definition
240
+ for definition in (
241
+ LiberoBenchmarkDefinition(),
242
+ LiberoPlusBenchmarkDefinition(),
243
+ RoboCasaBenchmarkDefinition(),
244
+ RoboTwinBenchmarkDefinition(),
245
+ CalvinBenchmarkDefinition(),
246
+ MetaworldBenchmarkDefinition(),
247
+ )
248
+ }
249
+
250
+
251
+ def get_benchmark_spec(name: str | BenchmarkName, suite: str) -> BenchmarkSpec:
252
+ benchmark = BenchmarkName(name)
253
+ return _DEFINITIONS[benchmark].spec(suite)
254
+
255
+
256
+ def benchmark_names() -> tuple[str, ...]:
257
+ return tuple(benchmark.value for benchmark in _DEFINITIONS)
258
+
259
+
260
+ __all__ = [
261
+ "ActionSpec",
262
+ "BenchmarkName",
263
+ "BenchmarkSpec",
264
+ "FeatureKind",
265
+ "FeatureSpec",
266
+ "benchmark_names",
267
+ "get_benchmark_spec",
268
+ ]
@@ -0,0 +1,133 @@
1
+ # Copyright 2026 RoboScope Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Client used by the simulation runtime to call a custom policy process."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import socket
9
+ import time
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from roboscope_kit.policy import (
15
+ ActionChunk,
16
+ EpisodeContext,
17
+ Observation,
18
+ PolicyCapabilities,
19
+ SessionContext,
20
+ )
21
+ from roboscope_kit.protocol import (
22
+ PROTOCOL_VERSION,
23
+ ProtocolError,
24
+ receive_message,
25
+ send_message,
26
+ validate_version,
27
+ )
28
+
29
+
30
+ class PolicyRemoteError(RuntimeError):
31
+ def __init__(self, error: dict[str, Any]) -> None:
32
+ self.operation = str(error.get("operation") or "unknown")
33
+ self.error_type = str(error.get("type") or "RemoteError")
34
+ self.remote_message = str(error.get("message") or "")
35
+ self.remote_traceback = str(error.get("traceback") or "")
36
+ self.state = str(error.get("state") or "")
37
+ super().__init__(f"policy {self.operation} failed ({self.error_type}): {self.remote_message}")
38
+
39
+
40
+ class PolicyClient:
41
+ def __init__(self, socket_path: str | Path, timeout_s: float = 30.0) -> None:
42
+ self.socket_path = str(socket_path)
43
+ self.timeout_s = timeout_s
44
+ self._connection: socket.socket | None = None
45
+
46
+ def connect(self) -> None:
47
+ if self._connection is not None:
48
+ return
49
+ connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
50
+ connection.settimeout(self.timeout_s)
51
+ connection.connect(self.socket_path)
52
+ self._connection = connection
53
+
54
+ def wait_until_ready(self, timeout_s: float) -> dict[str, Any]:
55
+ deadline = time.monotonic() + timeout_s
56
+ last_error: OSError | None = None
57
+ while time.monotonic() < deadline:
58
+ try:
59
+ self.connect()
60
+ return self.health()
61
+ except OSError as exc:
62
+ last_error = exc
63
+ self.close()
64
+ time.sleep(0.05)
65
+ raise TimeoutError(
66
+ f"policy socket {self.socket_path} was not ready within {timeout_s}s"
67
+ ) from last_error
68
+
69
+ def health(self) -> dict[str, Any]:
70
+ return self._request("health", {})
71
+
72
+ def load(self, context: SessionContext) -> PolicyCapabilities:
73
+ result = self._request("load", {"context": context.to_dict()})
74
+ return PolicyCapabilities.from_dict(result["capabilities"])
75
+
76
+ def reset(self, context: EpisodeContext) -> dict[str, Any]:
77
+ return self._request("reset", {"context": context.to_dict()})
78
+
79
+ def act(self, observation: Observation) -> ActionChunk:
80
+ result = self._request("act", {"observation": observation.to_dict()})
81
+ return ActionChunk.from_dict(result["action_chunk"])
82
+
83
+ def act_batch(self, observations: tuple[Observation, ...]) -> tuple[ActionChunk, ...]:
84
+ result = self._request(
85
+ "act_batch",
86
+ {"observations": [observation.to_dict() for observation in observations]},
87
+ )
88
+ return tuple(ActionChunk.from_dict(item) for item in result["action_chunks"])
89
+
90
+ def end_episode(self, episode_id: str) -> dict[str, Any]:
91
+ if not episode_id:
92
+ raise ValueError("episode_id cannot be empty")
93
+ return self._request("end_episode", {"episode_id": episode_id})
94
+
95
+ def unload(self) -> dict[str, Any]:
96
+ return self._request("unload", {})
97
+
98
+ def close(self) -> None:
99
+ if self._connection is not None:
100
+ self._connection.close()
101
+ self._connection = None
102
+
103
+ def __enter__(self) -> PolicyClient:
104
+ self.connect()
105
+ return self
106
+
107
+ def __exit__(self, *_: object) -> None:
108
+ self.close()
109
+
110
+ def _request(self, operation: str, payload: dict[str, Any]) -> Any:
111
+ if self._connection is None:
112
+ self.connect()
113
+ if self._connection is None:
114
+ raise RuntimeError("policy connection is unavailable")
115
+ request_id = str(uuid.uuid4())
116
+ send_message(
117
+ self._connection,
118
+ {
119
+ "version": PROTOCOL_VERSION,
120
+ "request_id": request_id,
121
+ "operation": operation,
122
+ "payload": payload,
123
+ },
124
+ )
125
+ response = receive_message(self._connection)
126
+ if response is None:
127
+ raise ProtocolError("policy server closed the connection without a response")
128
+ validate_version(response)
129
+ if response.get("request_id") != request_id:
130
+ raise ProtocolError("response request_id does not match request")
131
+ if not response.get("ok"):
132
+ raise PolicyRemoteError(response.get("error") or {})
133
+ return response.get("result")
@@ -0,0 +1,176 @@
1
+ # Copyright 2026 RoboScope Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Benchmark-neutral data contracts shared by Policy clients and servers."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+
13
+ class FeatureKind(str, Enum): # noqa: UP042 -- benchmark images include Python 3.10
14
+ IMAGE = "image"
15
+ STATE = "state"
16
+ LANGUAGE = "language"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class FeatureSpec:
21
+ name: str
22
+ kind: FeatureKind
23
+ required: bool = True
24
+ description: str = ""
25
+ dtype: str | None = None
26
+ shape: tuple[int | None, ...] | None = None
27
+
28
+ def __post_init__(self) -> None:
29
+ if not self.name:
30
+ raise ValueError("feature name cannot be empty")
31
+ if self.dtype == "":
32
+ raise ValueError("feature dtype cannot be empty")
33
+ if self.shape is not None and any(
34
+ dimension is not None and dimension < 1 for dimension in self.shape
35
+ ):
36
+ raise ValueError("feature dimensions must be positive or dynamic")
37
+
38
+ @classmethod
39
+ def from_dict(cls, value: dict[str, Any]) -> FeatureSpec:
40
+ return cls(
41
+ name=str(value["name"]),
42
+ kind=FeatureKind(value["kind"]),
43
+ required=bool(value.get("required", True)),
44
+ description=str(value.get("description", "")),
45
+ dtype=str(value["dtype"]) if value.get("dtype") is not None else None,
46
+ shape=(
47
+ tuple(int(dimension) if dimension is not None else None for dimension in value["shape"])
48
+ if value.get("shape") is not None
49
+ else None
50
+ ),
51
+ )
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ return {
55
+ "name": self.name,
56
+ "kind": self.kind.value,
57
+ "required": self.required,
58
+ "description": self.description,
59
+ "dtype": self.dtype,
60
+ "shape": list(self.shape) if self.shape is not None else None,
61
+ }
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ActionSpec:
66
+ name: str
67
+ description: str
68
+ dimension: int | None = None
69
+ dtype: str = "float32"
70
+
71
+ def __post_init__(self) -> None:
72
+ if not self.name:
73
+ raise ValueError("action name cannot be empty")
74
+ if not self.dtype:
75
+ raise ValueError("action dtype cannot be empty")
76
+ if self.dimension is not None and self.dimension < 1:
77
+ raise ValueError("action dimension must be positive")
78
+
79
+ @classmethod
80
+ def from_dict(cls, value: dict[str, Any]) -> ActionSpec:
81
+ dimension = value.get("dimension")
82
+ return cls(
83
+ name=str(value["name"]),
84
+ description=str(value["description"]),
85
+ dimension=int(dimension) if dimension is not None else None,
86
+ dtype=str(value.get("dtype", "float32")),
87
+ )
88
+
89
+ def to_dict(self) -> dict[str, Any]:
90
+ return {
91
+ "name": self.name,
92
+ "description": self.description,
93
+ "dimension": self.dimension,
94
+ "dtype": self.dtype,
95
+ }
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class BenchmarkSpec:
100
+ name: str
101
+ suite: str
102
+ protocol_version: str
103
+ features: tuple[FeatureSpec, ...]
104
+ action: ActionSpec
105
+ supported_action_representations: tuple[str, ...] = ()
106
+ default_action_representation: str = ""
107
+
108
+ def __post_init__(self) -> None:
109
+ if not self.name:
110
+ raise ValueError("benchmark name cannot be empty")
111
+ if not self.suite:
112
+ raise ValueError("benchmark suite cannot be empty")
113
+ if not self.protocol_version:
114
+ raise ValueError("benchmark protocol version cannot be empty")
115
+ if not self.features:
116
+ raise ValueError("benchmark features cannot be empty")
117
+ names = [feature.name for feature in self.features]
118
+ if len(names) != len(set(names)):
119
+ raise ValueError("benchmark feature names must be unique")
120
+ if not self.supported_action_representations:
121
+ raise ValueError("benchmark action representations cannot be empty")
122
+ if len(self.supported_action_representations) != len(
123
+ set(self.supported_action_representations)
124
+ ):
125
+ raise ValueError("benchmark action representations must be unique")
126
+ if self.default_action_representation not in self.supported_action_representations:
127
+ raise ValueError("benchmark default action representation must be supported")
128
+
129
+ @classmethod
130
+ def from_dict(cls, value: dict[str, Any]) -> BenchmarkSpec:
131
+ representations = tuple(
132
+ str(item) for item in value.get("supported_action_representations", ())
133
+ )
134
+ default_representation = str(value.get("default_action_representation", ""))
135
+ if not representations:
136
+ default_representation = _legacy_action_representation(str(value["name"]))
137
+ representations = (default_representation,)
138
+ return cls(
139
+ name=str(value["name"]),
140
+ suite=str(value["suite"]),
141
+ protocol_version=str(value["protocol_version"]),
142
+ features=tuple(FeatureSpec.from_dict(item) for item in value["features"]),
143
+ action=ActionSpec.from_dict(value["action"]),
144
+ supported_action_representations=representations,
145
+ default_action_representation=default_representation,
146
+ )
147
+
148
+ def to_dict(self) -> dict[str, Any]:
149
+ return {
150
+ "name": self.name,
151
+ "suite": self.suite,
152
+ "protocol_version": self.protocol_version,
153
+ "features": [feature.to_dict() for feature in self.features],
154
+ "action": self.action.to_dict(),
155
+ "supported_action_representations": list(self.supported_action_representations),
156
+ "default_action_representation": self.default_action_representation,
157
+ }
158
+
159
+
160
+ def _legacy_action_representation(benchmark: str) -> str:
161
+ defaults = {
162
+ "libero": "libero.delta-eef-v1",
163
+ "libero_plus": "libero.delta-eef-v1",
164
+ "robocasa": "robocasa.gr1-joint-v1",
165
+ "robotwin": "robotwin.qpos-v1",
166
+ "calvin": "calvin.relative-v1",
167
+ }
168
+ try:
169
+ return defaults[benchmark]
170
+ except KeyError as exc:
171
+ raise ValueError(
172
+ f"legacy benchmark payload {benchmark!r} has no action representation default"
173
+ ) from exc
174
+
175
+
176
+ __all__ = ["ActionSpec", "BenchmarkSpec", "FeatureKind", "FeatureSpec"]