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.
- roboscope_kit/__init__.py +67 -0
- roboscope_kit/benchmark_specs.py +268 -0
- roboscope_kit/client.py +133 -0
- roboscope_kit/contracts.py +176 -0
- roboscope_kit/policy.py +348 -0
- roboscope_kit/policy_config.py +141 -0
- roboscope_kit/protocol.py +111 -0
- roboscope_kit/runtime/__init__.py +4 -0
- roboscope_kit/runtime/autotune.py +437 -0
- roboscope_kit/runtime/benchmark/__init__.py +60 -0
- roboscope_kit/runtime/benchmark/__main__.py +6 -0
- roboscope_kit/runtime/benchmark/adapters.py +99 -0
- roboscope_kit/runtime/benchmark/autotune.py +698 -0
- roboscope_kit/runtime/benchmark/checkpoint.py +131 -0
- roboscope_kit/runtime/benchmark/cli.py +590 -0
- roboscope_kit/runtime/benchmark/cuda_mps.py +284 -0
- roboscope_kit/runtime/benchmark/factories.py +57 -0
- roboscope_kit/runtime/benchmark/image_manifest.py +122 -0
- roboscope_kit/runtime/benchmark/legacy.py +198 -0
- roboscope_kit/runtime/benchmark/metrics.py +152 -0
- roboscope_kit/runtime/benchmark/orchestrator.py +844 -0
- roboscope_kit/runtime/benchmark/policy_process.py +221 -0
- roboscope_kit/runtime/benchmark/processes.py +378 -0
- roboscope_kit/runtime/benchmark/queue.py +172 -0
- roboscope_kit/runtime/benchmark/registry.py +41 -0
- roboscope_kit/runtime/benchmark/resources.py +138 -0
- roboscope_kit/runtime/benchmark/schema.py +104 -0
- roboscope_kit/runtime/benchmark/shard_merge.py +313 -0
- roboscope_kit/runtime/benchmark/shard_reduce.py +200 -0
- roboscope_kit/runtime/benchmark/shard_validation.py +94 -0
- roboscope_kit/runtime/benchmark/sharding.py +117 -0
- roboscope_kit/runtime/benchmark/suite_sizes.py +52 -0
- roboscope_kit/runtime/benchmark/timing.py +133 -0
- roboscope_kit/runtime/benchmark/worker.py +79 -0
- roboscope_kit/runtime/benchmarks/__init__.py +4 -0
- roboscope_kit/runtime/benchmarks/calvin/__init__.py +9 -0
- roboscope_kit/runtime/benchmarks/calvin/adapter.py +245 -0
- roboscope_kit/runtime/benchmarks/calvin/factory.py +114 -0
- roboscope_kit/runtime/benchmarks/calvin/worker.py +283 -0
- roboscope_kit/runtime/benchmarks/libero/__init__.py +9 -0
- roboscope_kit/runtime/benchmarks/libero/adapter.py +285 -0
- roboscope_kit/runtime/benchmarks/libero/factory.py +127 -0
- roboscope_kit/runtime/benchmarks/libero/worker.py +285 -0
- roboscope_kit/runtime/benchmarks/libero_plus/__init__.py +9 -0
- roboscope_kit/runtime/benchmarks/libero_plus/adapter.py +189 -0
- roboscope_kit/runtime/benchmarks/libero_plus/factory.py +130 -0
- roboscope_kit/runtime/benchmarks/metaworld/__init__.py +17 -0
- roboscope_kit/runtime/benchmarks/metaworld/adapter.py +351 -0
- roboscope_kit/runtime/benchmarks/metaworld/agent.py +177 -0
- roboscope_kit/runtime/benchmarks/metaworld/config.py +163 -0
- roboscope_kit/runtime/benchmarks/metaworld/factory.py +114 -0
- roboscope_kit/runtime/benchmarks/metaworld/instructions.py +268 -0
- roboscope_kit/runtime/benchmarks/metaworld/worker.py +337 -0
- roboscope_kit/runtime/benchmarks/robocasa/__init__.py +8 -0
- roboscope_kit/runtime/benchmarks/robocasa/adapter.py +229 -0
- roboscope_kit/runtime/benchmarks/robocasa/factory.py +120 -0
- roboscope_kit/runtime/benchmarks/robocasa/worker.py +218 -0
- roboscope_kit/runtime/benchmarks/robotwin/__init__.py +14 -0
- roboscope_kit/runtime/benchmarks/robotwin/adapter.py +198 -0
- roboscope_kit/runtime/benchmarks/robotwin/factory.py +232 -0
- roboscope_kit/runtime/benchmarks/robotwin/seed_cache.py +79 -0
- roboscope_kit/runtime/benchmarks/robotwin/seed_protocol.py +286 -0
- roboscope_kit/runtime/benchmarks/robotwin/worker.py +581 -0
- roboscope_kit/runtime/hil/__init__.py +23 -0
- roboscope_kit/runtime/hil/checkpoints.py +436 -0
- roboscope_kit/runtime/hil/lerobot_policy.py +224 -0
- roboscope_kit/server.py +245 -0
- roboscope_kit-0.8.0b1.dist-info/METADATA +490 -0
- roboscope_kit-0.8.0b1.dist-info/RECORD +71 -0
- roboscope_kit-0.8.0b1.dist-info/WHEEL +4 -0
- 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
|
+
]
|
roboscope_kit/client.py
ADDED
|
@@ -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"]
|