luckyrobots 0.1.72__tar.gz → 0.1.74__tar.gz
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.
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/PKG-INFO +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/pyproject.toml +1 -1
- luckyrobots-0.1.74/src/luckyrobots/__init__.py +10 -0
- luckyrobots-0.1.74/src/luckyrobots/client.py +677 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/config/robots.yaml +96 -48
- luckyrobots-0.1.74/src/luckyrobots/engine/__init__.py +8 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/agent_pb2.py +4 -4
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/agent_pb2_grpc.py +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/camera_pb2.py +2 -2
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/camera_pb2_grpc.py +2 -2
- luckyrobots-0.1.74/src/luckyrobots/grpc/generated/debug_pb2.py +51 -0
- luckyrobots-0.1.74/src/luckyrobots/grpc/generated/debug_pb2_grpc.py +100 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/hazel_rpc_pb2.py +10 -9
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/mujoco_pb2_grpc.py +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/scene_pb2.py +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/scene_pb2_grpc.py +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/telemetry_pb2_grpc.py +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/viewport_pb2.py +1 -1
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/viewport_pb2_grpc.py +2 -2
- luckyrobots-0.1.74/src/luckyrobots/grpc/proto/debug.proto +63 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/hazel_rpc.proto +1 -0
- luckyrobots-0.1.74/src/luckyrobots/models/__init__.py +3 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/models/observation.py +4 -33
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/utils.py +1 -43
- luckyrobots-0.1.72/src/luckyrobots/__init__.py +0 -37
- luckyrobots-0.1.72/src/luckyrobots/client.py +0 -997
- luckyrobots-0.1.72/src/luckyrobots/engine/__init__.py +0 -23
- luckyrobots-0.1.72/src/luckyrobots/engine/check_updates.py +0 -264
- luckyrobots-0.1.72/src/luckyrobots/engine/download.py +0 -125
- luckyrobots-0.1.72/src/luckyrobots/models/__init__.py +0 -15
- luckyrobots-0.1.72/src/luckyrobots/models/camera.py +0 -97
- luckyrobots-0.1.72/src/luckyrobots/models/randomization.py +0 -77
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/.gitignore +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/LICENSE +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/README.md +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/config/__init__.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/engine/manager.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/__init__.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/__init__.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/common_pb2.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/common_pb2_grpc.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/media_pb2.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/media_pb2_grpc.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/mujoco_pb2.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/generated/telemetry_pb2.py +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/agent.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/camera.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/common.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/media.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/mujoco.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/scene.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/telemetry.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/grpc/proto/viewport.proto +0 -0
- {luckyrobots-0.1.72 → luckyrobots-0.1.74}/src/luckyrobots/luckyrobots.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luckyrobots
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.74
|
|
4
4
|
Summary: Robotics-AI Training in Hyperrealistic Game Environments
|
|
5
5
|
Project-URL: Homepage, https://github.com/luckyrobots/luckyrobots
|
|
6
6
|
Project-URL: Documentation, https://luckyrobots.readthedocs.io
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""LuckyRobots - Robotics simulation framework with gRPC communication.
|
|
2
|
+
|
|
3
|
+
This package provides a Python API for controlling robots in the LuckyEngine
|
|
4
|
+
simulation environment via gRPC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from luckyrobots.client import GrpcConnectionError as GrpcConnectionError
|
|
8
|
+
from luckyrobots.client import LuckyEngineClient as LuckyEngineClient
|
|
9
|
+
from luckyrobots.luckyrobots import LuckyRobots as LuckyRobots
|
|
10
|
+
from luckyrobots.models import ObservationResponse as ObservationResponse
|
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LuckyEngine gRPC client.
|
|
3
|
+
|
|
4
|
+
Uses checked-in Python stubs generated from the `.proto` files under
|
|
5
|
+
`src/luckyrobots/grpc/proto/`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("luckyrobots.client")
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from .grpc.generated import agent_pb2 # type: ignore
|
|
19
|
+
from .grpc.generated import agent_pb2_grpc # type: ignore
|
|
20
|
+
from .grpc.generated import common_pb2 # type: ignore
|
|
21
|
+
from .grpc.generated import debug_pb2 # type: ignore
|
|
22
|
+
from .grpc.generated import debug_pb2_grpc # type: ignore
|
|
23
|
+
from .grpc.generated import mujoco_pb2 # type: ignore
|
|
24
|
+
from .grpc.generated import mujoco_pb2_grpc # type: ignore
|
|
25
|
+
from .grpc.generated import scene_pb2 # type: ignore
|
|
26
|
+
from .grpc.generated import scene_pb2_grpc # type: ignore
|
|
27
|
+
except Exception as e: # pragma: no cover
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"Missing generated gRPC stubs. Regenerate them from the protos in "
|
|
30
|
+
"src/luckyrobots/grpc/proto into src/luckyrobots/grpc/generated."
|
|
31
|
+
) from e
|
|
32
|
+
|
|
33
|
+
from .models import ObservationResponse
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GrpcConnectionError(Exception):
|
|
37
|
+
"""Raised when gRPC connection fails."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str):
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
logger.warning("gRPC connection error: %s", message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LuckyEngineClient:
|
|
45
|
+
"""
|
|
46
|
+
Client for connecting to the LuckyEngine gRPC server.
|
|
47
|
+
|
|
48
|
+
Provides access to gRPC services for RL training:
|
|
49
|
+
- AgentService: observations, stepping, resets
|
|
50
|
+
- SceneService: simulation mode control
|
|
51
|
+
- MujocoService: health checks
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
client = LuckyEngineClient(host="127.0.0.1", port=50051)
|
|
55
|
+
client.connect()
|
|
56
|
+
client.wait_for_server()
|
|
57
|
+
|
|
58
|
+
schema = client.get_agent_schema()
|
|
59
|
+
obs = client.step(actions=[0.0] * 12)
|
|
60
|
+
|
|
61
|
+
client.close()
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
host: str = "127.0.0.1",
|
|
67
|
+
port: int = 50051,
|
|
68
|
+
timeout: float = 5.0,
|
|
69
|
+
*,
|
|
70
|
+
robot_name: Optional[str] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Initialize the LuckyEngine gRPC client.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
host: gRPC server host address.
|
|
77
|
+
port: gRPC server port.
|
|
78
|
+
timeout: Default timeout for RPC calls in seconds.
|
|
79
|
+
robot_name: Default robot name for calls that require it.
|
|
80
|
+
"""
|
|
81
|
+
self.host = host
|
|
82
|
+
self.port = port
|
|
83
|
+
self.timeout = timeout
|
|
84
|
+
self._robot_name = robot_name
|
|
85
|
+
|
|
86
|
+
self._channel = None
|
|
87
|
+
|
|
88
|
+
# Service stubs (populated after connect)
|
|
89
|
+
self._scene = None
|
|
90
|
+
self._mujoco = None
|
|
91
|
+
self._agent = None
|
|
92
|
+
self._debug = None
|
|
93
|
+
|
|
94
|
+
# Cached agent schemas: agent_name -> (observation_names, action_names)
|
|
95
|
+
self._schema_cache: dict[str, tuple[list[str], list[str]]] = {}
|
|
96
|
+
|
|
97
|
+
# Protobuf modules (for discoverability + explicit imports).
|
|
98
|
+
self._pb = SimpleNamespace(
|
|
99
|
+
common=common_pb2,
|
|
100
|
+
scene=scene_pb2,
|
|
101
|
+
mujoco=mujoco_pb2,
|
|
102
|
+
agent=agent_pb2,
|
|
103
|
+
debug=debug_pb2,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def connect(self) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Connect to the LuckyEngine gRPC server.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
GrpcConnectionError: If connection fails.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
import grpc # type: ignore
|
|
115
|
+
except ImportError as e:
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
"Missing grpcio. Install with: pip install grpcio protobuf"
|
|
118
|
+
) from e
|
|
119
|
+
|
|
120
|
+
target = f"{self.host}:{self.port}"
|
|
121
|
+
logger.info(f"Connecting to LuckyEngine gRPC server at {target}")
|
|
122
|
+
|
|
123
|
+
self._channel = grpc.insecure_channel(target)
|
|
124
|
+
|
|
125
|
+
# Create service stubs
|
|
126
|
+
self._scene = scene_pb2_grpc.SceneServiceStub(self._channel)
|
|
127
|
+
self._mujoco = mujoco_pb2_grpc.MujocoServiceStub(self._channel)
|
|
128
|
+
self._agent = agent_pb2_grpc.AgentServiceStub(self._channel)
|
|
129
|
+
self._debug = debug_pb2_grpc.DebugServiceStub(self._channel)
|
|
130
|
+
|
|
131
|
+
logger.info(f"Channel opened to {target} (server not verified yet)")
|
|
132
|
+
|
|
133
|
+
def close(self) -> None:
|
|
134
|
+
"""Close the gRPC channel."""
|
|
135
|
+
if self._channel is not None:
|
|
136
|
+
try:
|
|
137
|
+
self._channel.close()
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.debug(f"Error closing gRPC channel: {e}")
|
|
140
|
+
self._channel = None
|
|
141
|
+
self._scene = None
|
|
142
|
+
self._mujoco = None
|
|
143
|
+
self._agent = None
|
|
144
|
+
self._debug = None
|
|
145
|
+
logger.info("gRPC channel closed")
|
|
146
|
+
|
|
147
|
+
def is_connected(self) -> bool:
|
|
148
|
+
"""Check if the client is connected."""
|
|
149
|
+
return self._channel is not None
|
|
150
|
+
|
|
151
|
+
def health_check(self, timeout: Optional[float] = None) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Perform a health check by calling GetMujocoInfo.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
timeout: Timeout in seconds (uses default if None).
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
True if server responds, False otherwise.
|
|
160
|
+
"""
|
|
161
|
+
if not self.is_connected():
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
timeout = timeout or self.timeout
|
|
165
|
+
try:
|
|
166
|
+
self._mujoco.GetMujocoInfo(
|
|
167
|
+
self.pb.mujoco.GetMujocoInfoRequest(robot_name=self._robot_name or ""),
|
|
168
|
+
timeout=timeout,
|
|
169
|
+
)
|
|
170
|
+
return True
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.debug(f"Health check failed: {e}")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def wait_for_server(
|
|
176
|
+
self, timeout: float = 30.0, poll_interval: float = 0.5
|
|
177
|
+
) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Wait for the gRPC server to become available.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
timeout: Maximum time to wait in seconds.
|
|
183
|
+
poll_interval: Time between connection attempts.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if server became available, False if timeout.
|
|
187
|
+
"""
|
|
188
|
+
start = time.perf_counter()
|
|
189
|
+
|
|
190
|
+
while time.perf_counter() - start < timeout:
|
|
191
|
+
if not self.is_connected():
|
|
192
|
+
try:
|
|
193
|
+
self.connect()
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
if self.health_check(timeout=10.0):
|
|
198
|
+
logger.info(f"Connected to LuckyEngine gRPC server at {self.host}:{self.port}")
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
time.sleep(poll_interval)
|
|
202
|
+
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def pb(self) -> Any:
|
|
207
|
+
"""Access protobuf modules grouped by domain (e.g., `client.pb.scene`)."""
|
|
208
|
+
return self._pb
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def robot_name(self) -> Optional[str]:
|
|
212
|
+
"""Default robot name used by calls that accept an optional robot_name."""
|
|
213
|
+
return self._robot_name
|
|
214
|
+
|
|
215
|
+
def set_robot_name(self, robot_name: str) -> None:
|
|
216
|
+
"""Set the default robot name used by calls that accept an optional robot_name."""
|
|
217
|
+
self._robot_name = robot_name
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def scene(self) -> Any:
|
|
221
|
+
"""SceneService stub."""
|
|
222
|
+
if self._scene is None:
|
|
223
|
+
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
224
|
+
return self._scene
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def mujoco(self) -> Any:
|
|
228
|
+
"""MujocoService stub."""
|
|
229
|
+
if self._mujoco is None:
|
|
230
|
+
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
231
|
+
return self._mujoco
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def agent(self) -> Any:
|
|
235
|
+
"""AgentService stub."""
|
|
236
|
+
if self._agent is None:
|
|
237
|
+
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
238
|
+
return self._agent
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def debug(self) -> Any:
|
|
242
|
+
"""DebugService stub."""
|
|
243
|
+
if self._debug is None:
|
|
244
|
+
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
245
|
+
return self._debug
|
|
246
|
+
|
|
247
|
+
def get_mujoco_info(self, robot_name: str = "", timeout: Optional[float] = None):
|
|
248
|
+
"""Get MuJoCo model information (joint names, limits, etc.)."""
|
|
249
|
+
timeout = timeout or self.timeout
|
|
250
|
+
robot_name = robot_name or self._robot_name
|
|
251
|
+
if not robot_name:
|
|
252
|
+
raise ValueError("robot_name is required")
|
|
253
|
+
return self.mujoco.GetMujocoInfo(
|
|
254
|
+
self.pb.mujoco.GetMujocoInfoRequest(robot_name=robot_name),
|
|
255
|
+
timeout=timeout,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def get_agent_schema(self, agent_name: str = "", timeout: Optional[float] = None):
|
|
259
|
+
"""Get agent schema (observation/action sizes and names).
|
|
260
|
+
|
|
261
|
+
The schema is cached for subsequent get_observation() calls to enable
|
|
262
|
+
named access to observation values.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
agent_name: Agent name (empty = default agent).
|
|
266
|
+
timeout: RPC timeout.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
GetAgentSchemaResponse with schema containing observation_names,
|
|
270
|
+
action_names, observation_size, and action_size.
|
|
271
|
+
"""
|
|
272
|
+
timeout = timeout or self.timeout
|
|
273
|
+
resp = self.agent.GetAgentSchema(
|
|
274
|
+
self.pb.agent.GetAgentSchemaRequest(agent_name=agent_name),
|
|
275
|
+
timeout=timeout,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Cache the schema for named observation access
|
|
279
|
+
schema = getattr(resp, "schema", None)
|
|
280
|
+
if schema is not None:
|
|
281
|
+
cache_key = agent_name or "agent_0"
|
|
282
|
+
obs_names = list(schema.observation_names) if schema.observation_names else []
|
|
283
|
+
action_names = list(schema.action_names) if schema.action_names else []
|
|
284
|
+
self._schema_cache[cache_key] = (obs_names, action_names)
|
|
285
|
+
logger.debug(
|
|
286
|
+
"Cached schema for %s: %d obs names, %d action names",
|
|
287
|
+
cache_key,
|
|
288
|
+
len(obs_names),
|
|
289
|
+
len(action_names),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return resp
|
|
293
|
+
|
|
294
|
+
def get_observation(
|
|
295
|
+
self,
|
|
296
|
+
agent_name: str = "",
|
|
297
|
+
timeout: Optional[float] = None,
|
|
298
|
+
) -> ObservationResponse:
|
|
299
|
+
"""
|
|
300
|
+
Get the RL observation vector for an agent.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
agent_name: Agent name (empty = default agent).
|
|
304
|
+
timeout: RPC timeout.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
ObservationResponse with observation vector, actions, timestamp.
|
|
308
|
+
"""
|
|
309
|
+
timeout = timeout or self.timeout
|
|
310
|
+
|
|
311
|
+
resolved_robot_name = self._robot_name
|
|
312
|
+
if not resolved_robot_name:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
"robot_name is required (set it once via "
|
|
315
|
+
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
resp = self.agent.GetObservation(
|
|
319
|
+
self.pb.agent.GetObservationRequest(
|
|
320
|
+
robot_name=resolved_robot_name,
|
|
321
|
+
agent_name=agent_name,
|
|
322
|
+
include_joint_state=False,
|
|
323
|
+
include_agent_frame=True,
|
|
324
|
+
include_telemetry=False,
|
|
325
|
+
),
|
|
326
|
+
timeout=timeout,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
agent_frame = getattr(resp, "agent_frame", None)
|
|
330
|
+
observations = []
|
|
331
|
+
actions = []
|
|
332
|
+
timestamp_ms = getattr(resp, "timestamp_ms", 0)
|
|
333
|
+
frame_number = getattr(resp, "frame_number", 0)
|
|
334
|
+
|
|
335
|
+
if agent_frame is not None:
|
|
336
|
+
observations = list(agent_frame.observations) if agent_frame.observations else []
|
|
337
|
+
actions = list(agent_frame.actions) if agent_frame.actions else []
|
|
338
|
+
timestamp_ms = getattr(agent_frame, "timestamp_ms", timestamp_ms)
|
|
339
|
+
frame_number = getattr(agent_frame, "frame_number", frame_number)
|
|
340
|
+
|
|
341
|
+
cache_key = agent_name or "agent_0"
|
|
342
|
+
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
343
|
+
|
|
344
|
+
return ObservationResponse(
|
|
345
|
+
observation=observations,
|
|
346
|
+
actions=actions,
|
|
347
|
+
timestamp_ms=timestamp_ms,
|
|
348
|
+
frame_number=frame_number,
|
|
349
|
+
agent_name=cache_key,
|
|
350
|
+
observation_names=obs_names,
|
|
351
|
+
action_names=action_names,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def reset_agent(
|
|
355
|
+
self,
|
|
356
|
+
agent_name: str = "",
|
|
357
|
+
randomization_cfg: Optional[Any] = None,
|
|
358
|
+
timeout: Optional[float] = None,
|
|
359
|
+
):
|
|
360
|
+
"""
|
|
361
|
+
Reset a specific agent.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
agent_name: Agent logical name. Empty string means default agent.
|
|
365
|
+
randomization_cfg: Optional domain randomization config for this reset.
|
|
366
|
+
timeout: Timeout in seconds (uses default if None).
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
ResetAgentResponse with success and message fields.
|
|
370
|
+
"""
|
|
371
|
+
timeout = timeout or self.timeout
|
|
372
|
+
|
|
373
|
+
request_kwargs = {"agent_name": agent_name}
|
|
374
|
+
|
|
375
|
+
if randomization_cfg is not None:
|
|
376
|
+
randomization_proto = self._randomization_to_proto(randomization_cfg)
|
|
377
|
+
request_kwargs["dr_config"] = randomization_proto
|
|
378
|
+
|
|
379
|
+
return self.agent.ResetAgent(
|
|
380
|
+
self.pb.agent.ResetAgentRequest(**request_kwargs),
|
|
381
|
+
timeout=timeout,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def step(
|
|
385
|
+
self,
|
|
386
|
+
actions: list[float],
|
|
387
|
+
agent_name: str = "",
|
|
388
|
+
timeout_ms: int = 0,
|
|
389
|
+
timeout: Optional[float] = None,
|
|
390
|
+
) -> ObservationResponse:
|
|
391
|
+
"""
|
|
392
|
+
Synchronous RL step: apply action, wait for physics, return observation.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
actions: Action vector to apply for this step.
|
|
396
|
+
agent_name: Agent name (empty = default agent).
|
|
397
|
+
timeout_ms: Server-side timeout for the step in milliseconds.
|
|
398
|
+
timeout: RPC timeout in seconds.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
ObservationResponse with observation after physics step.
|
|
402
|
+
"""
|
|
403
|
+
timeout = timeout or self.timeout
|
|
404
|
+
|
|
405
|
+
resp = self.agent.Step(
|
|
406
|
+
self.pb.agent.StepRequest(
|
|
407
|
+
agent_name=agent_name,
|
|
408
|
+
actions=actions,
|
|
409
|
+
timeout_ms=timeout_ms,
|
|
410
|
+
),
|
|
411
|
+
timeout=timeout,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if not resp.success:
|
|
415
|
+
raise RuntimeError(f"Step failed: {resp.message}")
|
|
416
|
+
|
|
417
|
+
agent_frame = resp.observation
|
|
418
|
+
observations = list(agent_frame.observations) if agent_frame.observations else []
|
|
419
|
+
actions_out = list(agent_frame.actions) if agent_frame.actions else []
|
|
420
|
+
timestamp_ms = getattr(agent_frame, "timestamp_ms", 0)
|
|
421
|
+
frame_number = getattr(agent_frame, "frame_number", 0)
|
|
422
|
+
|
|
423
|
+
cache_key = agent_name or "agent_0"
|
|
424
|
+
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
425
|
+
|
|
426
|
+
return ObservationResponse(
|
|
427
|
+
observation=observations,
|
|
428
|
+
actions=actions_out,
|
|
429
|
+
timestamp_ms=timestamp_ms,
|
|
430
|
+
frame_number=frame_number,
|
|
431
|
+
agent_name=cache_key,
|
|
432
|
+
observation_names=obs_names,
|
|
433
|
+
action_names=action_names,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def set_simulation_mode(
|
|
437
|
+
self,
|
|
438
|
+
mode: str = "fast",
|
|
439
|
+
timeout: Optional[float] = None,
|
|
440
|
+
):
|
|
441
|
+
"""
|
|
442
|
+
Set simulation timing mode.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
mode: "realtime", "deterministic", or "fast"
|
|
446
|
+
- realtime: Physics runs at 1x wall-clock speed
|
|
447
|
+
- deterministic: Physics runs at fixed rate
|
|
448
|
+
- fast: Physics runs as fast as possible (for RL training)
|
|
449
|
+
timeout: RPC timeout in seconds.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
SetSimulationModeResponse with success and current mode.
|
|
453
|
+
"""
|
|
454
|
+
timeout = timeout or self.timeout
|
|
455
|
+
|
|
456
|
+
mode_map = {
|
|
457
|
+
"realtime": 0,
|
|
458
|
+
"deterministic": 1,
|
|
459
|
+
"fast": 2,
|
|
460
|
+
}
|
|
461
|
+
mode_value = mode_map.get(mode.lower(), 2)
|
|
462
|
+
|
|
463
|
+
return self.scene.SetSimulationMode(
|
|
464
|
+
self.pb.scene.SetSimulationModeRequest(mode=mode_value),
|
|
465
|
+
timeout=timeout,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def _randomization_to_proto(self, randomization_cfg: Any):
|
|
469
|
+
"""Convert domain randomization config to proto message."""
|
|
470
|
+
proto_kwargs = {}
|
|
471
|
+
|
|
472
|
+
def get_val(name: str, default=None):
|
|
473
|
+
val = getattr(randomization_cfg, name, default)
|
|
474
|
+
if val is None or (isinstance(val, (tuple, list)) and len(val) == 0):
|
|
475
|
+
return None
|
|
476
|
+
return val
|
|
477
|
+
|
|
478
|
+
# Initial state randomization
|
|
479
|
+
pose_pos = get_val("pose_position_noise")
|
|
480
|
+
if pose_pos is not None:
|
|
481
|
+
proto_kwargs["pose_position_noise"] = list(pose_pos)
|
|
482
|
+
|
|
483
|
+
pose_ori = get_val("pose_orientation_noise")
|
|
484
|
+
if pose_ori is not None and pose_ori != 0.0:
|
|
485
|
+
proto_kwargs["pose_orientation_noise"] = pose_ori
|
|
486
|
+
|
|
487
|
+
joint_pos = get_val("joint_position_noise")
|
|
488
|
+
if joint_pos is not None and joint_pos != 0.0:
|
|
489
|
+
proto_kwargs["joint_position_noise"] = joint_pos
|
|
490
|
+
|
|
491
|
+
joint_vel = get_val("joint_velocity_noise")
|
|
492
|
+
if joint_vel is not None and joint_vel != 0.0:
|
|
493
|
+
proto_kwargs["joint_velocity_noise"] = joint_vel
|
|
494
|
+
|
|
495
|
+
# Physics parameters
|
|
496
|
+
friction = get_val("friction_range")
|
|
497
|
+
if friction is not None:
|
|
498
|
+
proto_kwargs["friction_range"] = list(friction)
|
|
499
|
+
|
|
500
|
+
restitution = get_val("restitution_range")
|
|
501
|
+
if restitution is not None:
|
|
502
|
+
proto_kwargs["restitution_range"] = list(restitution)
|
|
503
|
+
|
|
504
|
+
mass_scale = get_val("mass_scale_range")
|
|
505
|
+
if mass_scale is not None:
|
|
506
|
+
proto_kwargs["mass_scale_range"] = list(mass_scale)
|
|
507
|
+
|
|
508
|
+
com_offset = get_val("com_offset_range")
|
|
509
|
+
if com_offset is not None:
|
|
510
|
+
proto_kwargs["com_offset_range"] = list(com_offset)
|
|
511
|
+
|
|
512
|
+
# Motor/actuator
|
|
513
|
+
motor_strength = get_val("motor_strength_range")
|
|
514
|
+
if motor_strength is not None:
|
|
515
|
+
proto_kwargs["motor_strength_range"] = list(motor_strength)
|
|
516
|
+
|
|
517
|
+
motor_offset = get_val("motor_offset_range")
|
|
518
|
+
if motor_offset is not None:
|
|
519
|
+
proto_kwargs["motor_offset_range"] = list(motor_offset)
|
|
520
|
+
|
|
521
|
+
# External disturbances
|
|
522
|
+
push_interval = get_val("push_interval_range")
|
|
523
|
+
if push_interval is not None:
|
|
524
|
+
proto_kwargs["push_interval_range"] = list(push_interval)
|
|
525
|
+
|
|
526
|
+
push_velocity = get_val("push_velocity_range")
|
|
527
|
+
if push_velocity is not None:
|
|
528
|
+
proto_kwargs["push_velocity_range"] = list(push_velocity)
|
|
529
|
+
|
|
530
|
+
# Terrain
|
|
531
|
+
terrain_type = get_val("terrain_type")
|
|
532
|
+
if terrain_type is not None and terrain_type != "":
|
|
533
|
+
proto_kwargs["terrain_type"] = terrain_type
|
|
534
|
+
|
|
535
|
+
terrain_diff = get_val("terrain_difficulty")
|
|
536
|
+
if terrain_diff is not None and terrain_diff != 0.0:
|
|
537
|
+
proto_kwargs["terrain_difficulty"] = terrain_diff
|
|
538
|
+
|
|
539
|
+
return self.pb.agent.DomainRandomizationConfig(**proto_kwargs)
|
|
540
|
+
|
|
541
|
+
def draw_velocity_command(
|
|
542
|
+
self,
|
|
543
|
+
origin: tuple[float, float, float],
|
|
544
|
+
lin_vel_x: float,
|
|
545
|
+
lin_vel_y: float,
|
|
546
|
+
ang_vel_z: float,
|
|
547
|
+
scale: float = 1.0,
|
|
548
|
+
clear_previous: bool = True,
|
|
549
|
+
timeout: Optional[float] = None,
|
|
550
|
+
) -> bool:
|
|
551
|
+
"""
|
|
552
|
+
Draw velocity command visualization in LuckyEngine.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
origin: (x, y, z) position of the robot.
|
|
556
|
+
lin_vel_x: Forward velocity command.
|
|
557
|
+
lin_vel_y: Lateral velocity command.
|
|
558
|
+
ang_vel_z: Angular velocity command (yaw rate).
|
|
559
|
+
scale: Scale factor for visualization.
|
|
560
|
+
clear_previous: Clear previous debug draws before drawing.
|
|
561
|
+
timeout: RPC timeout in seconds.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
True if draw succeeded, False otherwise.
|
|
565
|
+
"""
|
|
566
|
+
timeout = timeout or self.timeout
|
|
567
|
+
|
|
568
|
+
velocity_cmd = self.pb.debug.DebugVelocityCommand(
|
|
569
|
+
origin=self.pb.debug.DebugVector3(x=origin[0], y=origin[1], z=origin[2]),
|
|
570
|
+
lin_vel_x=lin_vel_x,
|
|
571
|
+
lin_vel_y=lin_vel_y,
|
|
572
|
+
ang_vel_z=ang_vel_z,
|
|
573
|
+
scale=scale,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
request = self.pb.debug.DebugDrawRequest(
|
|
577
|
+
velocity_command=velocity_cmd,
|
|
578
|
+
clear_previous=clear_previous,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
resp = self.debug.Draw(request, timeout=timeout)
|
|
583
|
+
return resp.success
|
|
584
|
+
except Exception as e:
|
|
585
|
+
logger.debug(f"Debug draw failed: {e}")
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
def draw_arrow(
|
|
589
|
+
self,
|
|
590
|
+
origin: tuple[float, float, float],
|
|
591
|
+
direction: tuple[float, float, float],
|
|
592
|
+
color: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 1.0),
|
|
593
|
+
scale: float = 1.0,
|
|
594
|
+
clear_previous: bool = False,
|
|
595
|
+
timeout: Optional[float] = None,
|
|
596
|
+
) -> bool:
|
|
597
|
+
"""
|
|
598
|
+
Draw a debug arrow in LuckyEngine.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
origin: (x, y, z) start position.
|
|
602
|
+
direction: (x, y, z) direction and magnitude.
|
|
603
|
+
color: (r, g, b, a) color values (0-1 range).
|
|
604
|
+
scale: Scale factor for visualization.
|
|
605
|
+
clear_previous: Clear previous debug draws before drawing.
|
|
606
|
+
timeout: RPC timeout in seconds.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
True if draw succeeded, False otherwise.
|
|
610
|
+
"""
|
|
611
|
+
timeout = timeout or self.timeout
|
|
612
|
+
|
|
613
|
+
arrow = self.pb.debug.DebugArrow(
|
|
614
|
+
origin=self.pb.debug.DebugVector3(x=origin[0], y=origin[1], z=origin[2]),
|
|
615
|
+
direction=self.pb.debug.DebugVector3(
|
|
616
|
+
x=direction[0], y=direction[1], z=direction[2]
|
|
617
|
+
),
|
|
618
|
+
color=self.pb.debug.DebugColor(
|
|
619
|
+
r=color[0], g=color[1], b=color[2], a=color[3]
|
|
620
|
+
),
|
|
621
|
+
scale=scale,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
request = self.pb.debug.DebugDrawRequest(
|
|
625
|
+
arrows=[arrow],
|
|
626
|
+
clear_previous=clear_previous,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
resp = self.debug.Draw(request, timeout=timeout)
|
|
631
|
+
return resp.success
|
|
632
|
+
except Exception as e:
|
|
633
|
+
logger.debug(f"Debug draw failed: {e}")
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
def draw_line(
|
|
637
|
+
self,
|
|
638
|
+
start: tuple[float, float, float],
|
|
639
|
+
end: tuple[float, float, float],
|
|
640
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
641
|
+
clear_previous: bool = False,
|
|
642
|
+
timeout: Optional[float] = None,
|
|
643
|
+
) -> bool:
|
|
644
|
+
"""
|
|
645
|
+
Draw a debug line in LuckyEngine.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
start: (x, y, z) start position.
|
|
649
|
+
end: (x, y, z) end position.
|
|
650
|
+
color: (r, g, b, a) color values (0-1 range).
|
|
651
|
+
clear_previous: Clear previous debug draws before drawing.
|
|
652
|
+
timeout: RPC timeout in seconds.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
True if draw succeeded, False otherwise.
|
|
656
|
+
"""
|
|
657
|
+
timeout = timeout or self.timeout
|
|
658
|
+
|
|
659
|
+
line = self.pb.debug.DebugLine(
|
|
660
|
+
start=self.pb.debug.DebugVector3(x=start[0], y=start[1], z=start[2]),
|
|
661
|
+
end=self.pb.debug.DebugVector3(x=end[0], y=end[1], z=end[2]),
|
|
662
|
+
color=self.pb.debug.DebugColor(
|
|
663
|
+
r=color[0], g=color[1], b=color[2], a=color[3]
|
|
664
|
+
),
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
request = self.pb.debug.DebugDrawRequest(
|
|
668
|
+
lines=[line],
|
|
669
|
+
clear_previous=clear_previous,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
try:
|
|
673
|
+
resp = self.debug.Draw(request, timeout=timeout)
|
|
674
|
+
return resp.success
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.debug(f"Debug draw failed: {e}")
|
|
677
|
+
return False
|