luckyrobots 0.1.72__py3-none-any.whl → 0.1.74__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.
- luckyrobots/__init__.py +5 -32
- luckyrobots/client.py +143 -463
- luckyrobots/config/robots.yaml +96 -48
- luckyrobots/engine/__init__.py +5 -20
- luckyrobots/grpc/generated/agent_pb2.py +4 -4
- luckyrobots/grpc/generated/agent_pb2_grpc.py +1 -1
- luckyrobots/grpc/generated/camera_pb2.py +2 -2
- luckyrobots/grpc/generated/camera_pb2_grpc.py +2 -2
- luckyrobots/grpc/generated/debug_pb2.py +51 -0
- luckyrobots/grpc/generated/debug_pb2_grpc.py +100 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2.py +10 -9
- luckyrobots/grpc/generated/mujoco_pb2_grpc.py +1 -1
- luckyrobots/grpc/generated/scene_pb2.py +1 -1
- luckyrobots/grpc/generated/scene_pb2_grpc.py +1 -1
- luckyrobots/grpc/generated/telemetry_pb2_grpc.py +1 -1
- luckyrobots/grpc/generated/viewport_pb2.py +1 -1
- luckyrobots/grpc/generated/viewport_pb2_grpc.py +2 -2
- luckyrobots/grpc/proto/debug.proto +63 -0
- luckyrobots/grpc/proto/hazel_rpc.proto +1 -0
- luckyrobots/models/__init__.py +2 -14
- luckyrobots/models/observation.py +4 -33
- luckyrobots/utils.py +1 -43
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.74.dist-info}/METADATA +1 -1
- luckyrobots-0.1.74.dist-info/RECORD +46 -0
- luckyrobots/engine/check_updates.py +0 -264
- luckyrobots/engine/download.py +0 -125
- luckyrobots/models/camera.py +0 -97
- luckyrobots/models/randomization.py +0 -77
- luckyrobots-0.1.72.dist-info/RECORD +0 -47
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.74.dist-info}/WHEEL +0 -0
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.74.dist-info}/licenses/LICENSE +0 -0
luckyrobots/client.py
CHANGED
|
@@ -9,8 +9,6 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
11
|
import time
|
|
12
|
-
import statistics
|
|
13
|
-
from dataclasses import dataclass
|
|
14
12
|
from types import SimpleNamespace
|
|
15
13
|
from typing import Any, Optional
|
|
16
14
|
|
|
@@ -19,26 +17,20 @@ logger = logging.getLogger("luckyrobots.client")
|
|
|
19
17
|
try:
|
|
20
18
|
from .grpc.generated import agent_pb2 # type: ignore
|
|
21
19
|
from .grpc.generated import agent_pb2_grpc # type: ignore
|
|
22
|
-
from .grpc.generated import camera_pb2 # type: ignore
|
|
23
|
-
from .grpc.generated import camera_pb2_grpc # type: ignore
|
|
24
20
|
from .grpc.generated import common_pb2 # type: ignore
|
|
25
|
-
from .grpc.generated import
|
|
21
|
+
from .grpc.generated import debug_pb2 # type: ignore
|
|
22
|
+
from .grpc.generated import debug_pb2_grpc # type: ignore
|
|
26
23
|
from .grpc.generated import mujoco_pb2 # type: ignore
|
|
27
24
|
from .grpc.generated import mujoco_pb2_grpc # type: ignore
|
|
28
25
|
from .grpc.generated import scene_pb2 # type: ignore
|
|
29
26
|
from .grpc.generated import scene_pb2_grpc # type: ignore
|
|
30
|
-
from .grpc.generated import telemetry_pb2 # type: ignore
|
|
31
|
-
from .grpc.generated import telemetry_pb2_grpc # type: ignore
|
|
32
|
-
from .grpc.generated import viewport_pb2 # type: ignore
|
|
33
|
-
from .grpc.generated import viewport_pb2_grpc # type: ignore
|
|
34
27
|
except Exception as e: # pragma: no cover
|
|
35
28
|
raise ImportError(
|
|
36
29
|
"Missing generated gRPC stubs. Regenerate them from the protos in "
|
|
37
30
|
"src/luckyrobots/grpc/proto into src/luckyrobots/grpc/generated."
|
|
38
31
|
) from e
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
from .models import ObservationResponse, StateSnapshot, DomainRandomizationConfig
|
|
33
|
+
from .models import ObservationResponse
|
|
42
34
|
|
|
43
35
|
|
|
44
36
|
class GrpcConnectionError(Exception):
|
|
@@ -53,24 +45,18 @@ class LuckyEngineClient:
|
|
|
53
45
|
"""
|
|
54
46
|
Client for connecting to the LuckyEngine gRPC server.
|
|
55
47
|
|
|
56
|
-
Provides access to
|
|
57
|
-
|
|
58
|
-
- SceneService
|
|
59
|
-
- MujocoService
|
|
60
|
-
- TelemetryService
|
|
61
|
-
- AgentService
|
|
62
|
-
- ViewportService
|
|
63
|
-
- CameraService
|
|
48
|
+
Provides access to gRPC services for RL training:
|
|
49
|
+
- AgentService: observations, stepping, resets
|
|
50
|
+
- SceneService: simulation mode control
|
|
51
|
+
- MujocoService: health checks
|
|
64
52
|
|
|
65
53
|
Usage:
|
|
66
54
|
client = LuckyEngineClient(host="127.0.0.1", port=50051)
|
|
67
55
|
client.connect()
|
|
56
|
+
client.wait_for_server()
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
joint_state = client.mujoco.GetJointState(
|
|
72
|
-
client.pb.mujoco.GetJointStateRequest()
|
|
73
|
-
)
|
|
58
|
+
schema = client.get_agent_schema()
|
|
59
|
+
obs = client.step(actions=[0.0] * 12)
|
|
74
60
|
|
|
75
61
|
client.close()
|
|
76
62
|
"""
|
|
@@ -102,25 +88,19 @@ class LuckyEngineClient:
|
|
|
102
88
|
# Service stubs (populated after connect)
|
|
103
89
|
self._scene = None
|
|
104
90
|
self._mujoco = None
|
|
105
|
-
self._telemetry = None
|
|
106
91
|
self._agent = None
|
|
107
|
-
self.
|
|
108
|
-
self._camera = None
|
|
92
|
+
self._debug = None
|
|
109
93
|
|
|
110
94
|
# Cached agent schemas: agent_name -> (observation_names, action_names)
|
|
111
|
-
# Populated lazily by get_agent_schema() or fetch_schema()
|
|
112
95
|
self._schema_cache: dict[str, tuple[list[str], list[str]]] = {}
|
|
113
96
|
|
|
114
97
|
# Protobuf modules (for discoverability + explicit imports).
|
|
115
98
|
self._pb = SimpleNamespace(
|
|
116
99
|
common=common_pb2,
|
|
117
|
-
media=media_pb2,
|
|
118
100
|
scene=scene_pb2,
|
|
119
101
|
mujoco=mujoco_pb2,
|
|
120
|
-
telemetry=telemetry_pb2,
|
|
121
102
|
agent=agent_pb2,
|
|
122
|
-
|
|
123
|
-
camera=camera_pb2,
|
|
103
|
+
debug=debug_pb2,
|
|
124
104
|
)
|
|
125
105
|
|
|
126
106
|
def connect(self) -> None:
|
|
@@ -145,10 +125,8 @@ class LuckyEngineClient:
|
|
|
145
125
|
# Create service stubs
|
|
146
126
|
self._scene = scene_pb2_grpc.SceneServiceStub(self._channel)
|
|
147
127
|
self._mujoco = mujoco_pb2_grpc.MujocoServiceStub(self._channel)
|
|
148
|
-
self._telemetry = telemetry_pb2_grpc.TelemetryServiceStub(self._channel)
|
|
149
128
|
self._agent = agent_pb2_grpc.AgentServiceStub(self._channel)
|
|
150
|
-
self.
|
|
151
|
-
self._camera = camera_pb2_grpc.CameraServiceStub(self._channel)
|
|
129
|
+
self._debug = debug_pb2_grpc.DebugServiceStub(self._channel)
|
|
152
130
|
|
|
153
131
|
logger.info(f"Channel opened to {target} (server not verified yet)")
|
|
154
132
|
|
|
@@ -162,10 +140,8 @@ class LuckyEngineClient:
|
|
|
162
140
|
self._channel = None
|
|
163
141
|
self._scene = None
|
|
164
142
|
self._mujoco = None
|
|
165
|
-
self._telemetry = None
|
|
166
143
|
self._agent = None
|
|
167
|
-
self.
|
|
168
|
-
self._camera = None
|
|
144
|
+
self._debug = None
|
|
169
145
|
logger.info("gRPC channel closed")
|
|
170
146
|
|
|
171
147
|
def is_connected(self) -> bool:
|
|
@@ -209,8 +185,6 @@ class LuckyEngineClient:
|
|
|
209
185
|
Returns:
|
|
210
186
|
True if server became available, False if timeout.
|
|
211
187
|
"""
|
|
212
|
-
import time
|
|
213
|
-
|
|
214
188
|
start = time.perf_counter()
|
|
215
189
|
|
|
216
190
|
while time.perf_counter() - start < timeout:
|
|
@@ -228,8 +202,6 @@ class LuckyEngineClient:
|
|
|
228
202
|
|
|
229
203
|
return False
|
|
230
204
|
|
|
231
|
-
# --- Protobuf modules (discoverable + explicit) ---
|
|
232
|
-
|
|
233
205
|
@property
|
|
234
206
|
def pb(self) -> Any:
|
|
235
207
|
"""Access protobuf modules grouped by domain (e.g., `client.pb.scene`)."""
|
|
@@ -244,21 +216,9 @@ class LuckyEngineClient:
|
|
|
244
216
|
"""Set the default robot name used by calls that accept an optional robot_name."""
|
|
245
217
|
self._robot_name = robot_name
|
|
246
218
|
|
|
247
|
-
# --- Service stubs ---
|
|
248
|
-
#
|
|
249
|
-
# Confirmed working:
|
|
250
|
-
# - mujoco: GetMujocoInfo, GetJointState, SendControl
|
|
251
|
-
# - agent: GetObservation, ResetAgent, GetAgentSchema, StreamAgent
|
|
252
|
-
#
|
|
253
|
-
# Placeholders (not yet confirmed working - use at your own risk):
|
|
254
|
-
# - scene: GetSceneInfo
|
|
255
|
-
# - telemetry: StreamTelemetry
|
|
256
|
-
# - viewport: (no methods implemented)
|
|
257
|
-
# - camera: ListCameras, StreamCamera
|
|
258
|
-
|
|
259
219
|
@property
|
|
260
220
|
def scene(self) -> Any:
|
|
261
|
-
"""SceneService stub.
|
|
221
|
+
"""SceneService stub."""
|
|
262
222
|
if self._scene is None:
|
|
263
223
|
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
264
224
|
return self._scene
|
|
@@ -270,13 +230,6 @@ class LuckyEngineClient:
|
|
|
270
230
|
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
271
231
|
return self._mujoco
|
|
272
232
|
|
|
273
|
-
@property
|
|
274
|
-
def telemetry(self) -> Any:
|
|
275
|
-
"""TelemetryService stub. [PLACEHOLDER - not confirmed working]"""
|
|
276
|
-
if self._telemetry is None:
|
|
277
|
-
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
278
|
-
return self._telemetry
|
|
279
|
-
|
|
280
233
|
@property
|
|
281
234
|
def agent(self) -> Any:
|
|
282
235
|
"""AgentService stub."""
|
|
@@ -285,80 +238,23 @@ class LuckyEngineClient:
|
|
|
285
238
|
return self._agent
|
|
286
239
|
|
|
287
240
|
@property
|
|
288
|
-
def
|
|
289
|
-
"""
|
|
290
|
-
if self.
|
|
291
|
-
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
292
|
-
return self._viewport
|
|
293
|
-
|
|
294
|
-
@property
|
|
295
|
-
def camera(self) -> Any:
|
|
296
|
-
"""CameraService stub. [PLACEHOLDER - not confirmed working]"""
|
|
297
|
-
if self._camera is None:
|
|
241
|
+
def debug(self) -> Any:
|
|
242
|
+
"""DebugService stub."""
|
|
243
|
+
if self._debug is None:
|
|
298
244
|
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
299
|
-
return self.
|
|
300
|
-
|
|
301
|
-
# --- Convenience methods ---
|
|
302
|
-
|
|
303
|
-
def get_scene_info(self, timeout: Optional[float] = None):
|
|
304
|
-
"""Get scene information. [PLACEHOLDER - not confirmed working]"""
|
|
305
|
-
raise NotImplementedError(
|
|
306
|
-
"get_scene_info() is not yet confirmed working. "
|
|
307
|
-
"Remove this check if you want to test it."
|
|
308
|
-
)
|
|
309
|
-
timeout = timeout or self.timeout
|
|
310
|
-
return self.scene.GetSceneInfo(
|
|
311
|
-
self.pb.scene.GetSceneInfoRequest(),
|
|
312
|
-
timeout=timeout,
|
|
313
|
-
)
|
|
245
|
+
return self._debug
|
|
314
246
|
|
|
315
247
|
def get_mujoco_info(self, robot_name: str = "", timeout: Optional[float] = None):
|
|
316
|
-
"""Get MuJoCo model information."""
|
|
248
|
+
"""Get MuJoCo model information (joint names, limits, etc.)."""
|
|
317
249
|
timeout = timeout or self.timeout
|
|
318
250
|
robot_name = robot_name or self._robot_name
|
|
319
251
|
if not robot_name:
|
|
320
|
-
raise ValueError(
|
|
321
|
-
"robot_name is required (pass `robot_name=` or set it once via "
|
|
322
|
-
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
323
|
-
)
|
|
252
|
+
raise ValueError("robot_name is required")
|
|
324
253
|
return self.mujoco.GetMujocoInfo(
|
|
325
254
|
self.pb.mujoco.GetMujocoInfoRequest(robot_name=robot_name),
|
|
326
255
|
timeout=timeout,
|
|
327
256
|
)
|
|
328
257
|
|
|
329
|
-
def get_joint_state(self, robot_name: str = "", timeout: Optional[float] = None):
|
|
330
|
-
"""Get current joint state."""
|
|
331
|
-
timeout = timeout or self.timeout
|
|
332
|
-
robot_name = robot_name or self._robot_name
|
|
333
|
-
if not robot_name:
|
|
334
|
-
raise ValueError(
|
|
335
|
-
"robot_name is required (pass `robot_name=` or set it once via "
|
|
336
|
-
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
337
|
-
)
|
|
338
|
-
return self.mujoco.GetJointState(
|
|
339
|
-
self.pb.mujoco.GetJointStateRequest(robot_name=robot_name),
|
|
340
|
-
timeout=timeout,
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
def send_control(
|
|
344
|
-
self,
|
|
345
|
-
controls: list[float],
|
|
346
|
-
robot_name: str = "",
|
|
347
|
-
timeout: Optional[float] = None,
|
|
348
|
-
):
|
|
349
|
-
"""Send control commands to the robot."""
|
|
350
|
-
timeout = timeout or self.timeout
|
|
351
|
-
robot_name = robot_name or self._robot_name
|
|
352
|
-
if not robot_name:
|
|
353
|
-
raise ValueError(
|
|
354
|
-
"robot_name is required (pass `robot_name=` or set it once via "
|
|
355
|
-
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
356
|
-
)
|
|
357
|
-
return self.mujoco.SendControl(
|
|
358
|
-
self.pb.mujoco.SendControlRequest(robot_name=robot_name, controls=controls),
|
|
359
|
-
timeout=timeout,
|
|
360
|
-
)
|
|
361
|
-
|
|
362
258
|
def get_agent_schema(self, agent_name: str = "", timeout: Optional[float] = None):
|
|
363
259
|
"""Get agent schema (observation/action sizes and names).
|
|
364
260
|
|
|
@@ -395,18 +291,6 @@ class LuckyEngineClient:
|
|
|
395
291
|
|
|
396
292
|
return resp
|
|
397
293
|
|
|
398
|
-
def fetch_schema(self, agent_name: str = "", timeout: Optional[float] = None) -> None:
|
|
399
|
-
"""Fetch and cache agent schema for named observation access.
|
|
400
|
-
|
|
401
|
-
Call this once before get_observation() to enable accessing observations
|
|
402
|
-
by name (e.g., obs["proj_grav_x"]).
|
|
403
|
-
|
|
404
|
-
Args:
|
|
405
|
-
agent_name: Agent name (empty = default agent).
|
|
406
|
-
timeout: RPC timeout.
|
|
407
|
-
"""
|
|
408
|
-
self.get_agent_schema(agent_name=agent_name, timeout=timeout)
|
|
409
|
-
|
|
410
294
|
def get_observation(
|
|
411
295
|
self,
|
|
412
296
|
agent_name: str = "",
|
|
@@ -415,10 +299,6 @@ class LuckyEngineClient:
|
|
|
415
299
|
"""
|
|
416
300
|
Get the RL observation vector for an agent.
|
|
417
301
|
|
|
418
|
-
This returns only the flat observation vector defined by the agent's
|
|
419
|
-
observation spec in LuckyEngine. For sensor data (joints, telemetry,
|
|
420
|
-
cameras), use the dedicated methods.
|
|
421
|
-
|
|
422
302
|
Args:
|
|
423
303
|
agent_name: Agent name (empty = default agent).
|
|
424
304
|
timeout: RPC timeout.
|
|
@@ -435,30 +315,17 @@ class LuckyEngineClient:
|
|
|
435
315
|
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
436
316
|
)
|
|
437
317
|
|
|
438
|
-
# Request only the agent's RL observation (agent_frame in gRPC terms).
|
|
439
|
-
# Joint state and telemetry have their own dedicated methods.
|
|
440
318
|
resp = self.agent.GetObservation(
|
|
441
319
|
self.pb.agent.GetObservationRequest(
|
|
442
320
|
robot_name=resolved_robot_name,
|
|
443
321
|
agent_name=agent_name,
|
|
444
322
|
include_joint_state=False,
|
|
445
|
-
include_agent_frame=True,
|
|
323
|
+
include_agent_frame=True,
|
|
446
324
|
include_telemetry=False,
|
|
447
325
|
),
|
|
448
326
|
timeout=timeout,
|
|
449
327
|
)
|
|
450
328
|
|
|
451
|
-
# Extract observation from agent_frame.
|
|
452
|
-
#
|
|
453
|
-
# In gRPC, the "agent_frame" is the message containing the RL observation
|
|
454
|
-
# data from LuckyEngine's agent system. It includes:
|
|
455
|
-
# - observations: flat float vector matching the agent's observation spec
|
|
456
|
-
# - actions: the last action vector sent to the agent (echoed back)
|
|
457
|
-
# - timestamp_ms: wall-clock time when the observation was captured
|
|
458
|
-
# - frame_number: monotonic counter for ordering observations
|
|
459
|
-
#
|
|
460
|
-
# This is distinct from joint_state (raw MuJoCo qpos/qvel) and telemetry
|
|
461
|
-
# (debugging data like contact forces, energy, etc).
|
|
462
329
|
agent_frame = getattr(resp, "agent_frame", None)
|
|
463
330
|
observations = []
|
|
464
331
|
actions = []
|
|
@@ -471,7 +338,6 @@ class LuckyEngineClient:
|
|
|
471
338
|
timestamp_ms = getattr(agent_frame, "timestamp_ms", timestamp_ms)
|
|
472
339
|
frame_number = getattr(agent_frame, "frame_number", frame_number)
|
|
473
340
|
|
|
474
|
-
# Look up cached schema for named access
|
|
475
341
|
cache_key = agent_name or "agent_0"
|
|
476
342
|
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
477
343
|
|
|
@@ -485,117 +351,6 @@ class LuckyEngineClient:
|
|
|
485
351
|
action_names=action_names,
|
|
486
352
|
)
|
|
487
353
|
|
|
488
|
-
def get_state(
|
|
489
|
-
self,
|
|
490
|
-
agent_name: str = "",
|
|
491
|
-
include_observation: bool = True,
|
|
492
|
-
include_joint_state: bool = True,
|
|
493
|
-
camera_names: Optional[list[str]] = None,
|
|
494
|
-
width: int = 0,
|
|
495
|
-
height: int = 0,
|
|
496
|
-
format: str = "raw",
|
|
497
|
-
timeout: Optional[float] = None,
|
|
498
|
-
) -> StateSnapshot:
|
|
499
|
-
"""
|
|
500
|
-
Get a bundled snapshot of multiple data sources.
|
|
501
|
-
|
|
502
|
-
Use this for efficiency when you need multiple data types in one call.
|
|
503
|
-
For single data types, prefer the dedicated methods:
|
|
504
|
-
- get_observation() for RL observation vector
|
|
505
|
-
- get_joint_state() for joint positions/velocities
|
|
506
|
-
- stream_telemetry() for telemetry (streaming only)
|
|
507
|
-
- stream_camera() for camera frames (streaming)
|
|
508
|
-
|
|
509
|
-
Args:
|
|
510
|
-
agent_name: Agent name (empty = default agent).
|
|
511
|
-
include_observation: Include RL observation vector.
|
|
512
|
-
include_joint_state: Include joint positions/velocities.
|
|
513
|
-
camera_names: List of camera names to include frames from.
|
|
514
|
-
width: Desired width for camera frames (0 = native).
|
|
515
|
-
height: Desired height for camera frames (0 = native).
|
|
516
|
-
format: Image format ("raw" or "jpeg").
|
|
517
|
-
timeout: RPC timeout.
|
|
518
|
-
|
|
519
|
-
Returns:
|
|
520
|
-
StateSnapshot with requested data.
|
|
521
|
-
"""
|
|
522
|
-
timeout = timeout or self.timeout
|
|
523
|
-
|
|
524
|
-
resolved_robot_name = self._robot_name
|
|
525
|
-
if not resolved_robot_name:
|
|
526
|
-
raise ValueError(
|
|
527
|
-
"robot_name is required (set it once via "
|
|
528
|
-
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
cameras = []
|
|
532
|
-
if camera_names:
|
|
533
|
-
for name in camera_names:
|
|
534
|
-
cameras.append(
|
|
535
|
-
self.pb.agent.GetCameraFrameRequest(
|
|
536
|
-
name=name,
|
|
537
|
-
width=width,
|
|
538
|
-
height=height,
|
|
539
|
-
format=format,
|
|
540
|
-
)
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
resp = self.agent.GetObservation(
|
|
544
|
-
self.pb.agent.GetObservationRequest(
|
|
545
|
-
robot_name=resolved_robot_name,
|
|
546
|
-
agent_name=agent_name,
|
|
547
|
-
include_joint_state=include_joint_state,
|
|
548
|
-
include_agent_frame=include_observation,
|
|
549
|
-
include_telemetry=False, # Telemetry is streaming-only
|
|
550
|
-
cameras=cameras,
|
|
551
|
-
),
|
|
552
|
-
timeout=timeout,
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
# Build ObservationResponse if requested
|
|
556
|
-
obs_response = None
|
|
557
|
-
if include_observation:
|
|
558
|
-
agent_frame = getattr(resp, "agent_frame", None)
|
|
559
|
-
observations = []
|
|
560
|
-
actions = []
|
|
561
|
-
if agent_frame is not None:
|
|
562
|
-
observations = list(agent_frame.observations) if agent_frame.observations else []
|
|
563
|
-
actions = list(agent_frame.actions) if agent_frame.actions else []
|
|
564
|
-
|
|
565
|
-
# Look up cached schema for named access
|
|
566
|
-
cache_key = agent_name or "agent_0"
|
|
567
|
-
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
568
|
-
|
|
569
|
-
obs_response = ObservationResponse(
|
|
570
|
-
observation=observations,
|
|
571
|
-
actions=actions,
|
|
572
|
-
timestamp_ms=getattr(resp, "timestamp_ms", 0),
|
|
573
|
-
frame_number=getattr(resp, "frame_number", 0),
|
|
574
|
-
agent_name=cache_key,
|
|
575
|
-
observation_names=obs_names,
|
|
576
|
-
action_names=action_names,
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
return StateSnapshot(
|
|
580
|
-
observation=obs_response,
|
|
581
|
-
joint_state=getattr(resp, "joint_state", None) if include_joint_state else None,
|
|
582
|
-
camera_frames=list(getattr(resp, "camera_frames", [])) if camera_names else None,
|
|
583
|
-
timestamp_ms=getattr(resp, "timestamp_ms", 0),
|
|
584
|
-
frame_number=getattr(resp, "frame_number", 0),
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
def stream_agent(self, agent_name: str = "", target_fps: int = 30):
|
|
588
|
-
"""
|
|
589
|
-
Start streaming agent observations.
|
|
590
|
-
|
|
591
|
-
Returns an iterator of AgentFrame messages.
|
|
592
|
-
"""
|
|
593
|
-
return self.agent.StreamAgent(
|
|
594
|
-
self.pb.agent.StreamAgentRequest(
|
|
595
|
-
agent_name=agent_name, target_fps=target_fps
|
|
596
|
-
),
|
|
597
|
-
)
|
|
598
|
-
|
|
599
354
|
def reset_agent(
|
|
600
355
|
self,
|
|
601
356
|
agent_name: str = "",
|
|
@@ -603,13 +358,10 @@ class LuckyEngineClient:
|
|
|
603
358
|
timeout: Optional[float] = None,
|
|
604
359
|
):
|
|
605
360
|
"""
|
|
606
|
-
Reset a specific agent
|
|
607
|
-
|
|
608
|
-
Useful for multi-env RL where individual agents need to be reset without resetting the entire scene.
|
|
361
|
+
Reset a specific agent.
|
|
609
362
|
|
|
610
363
|
Args:
|
|
611
|
-
agent_name: Agent logical name.
|
|
612
|
-
Empty string means "default agent" (agent_0).
|
|
364
|
+
agent_name: Agent logical name. Empty string means default agent.
|
|
613
365
|
randomization_cfg: Optional domain randomization config for this reset.
|
|
614
366
|
timeout: Timeout in seconds (uses default if None).
|
|
615
367
|
|
|
@@ -618,7 +370,6 @@ class LuckyEngineClient:
|
|
|
618
370
|
"""
|
|
619
371
|
timeout = timeout or self.timeout
|
|
620
372
|
|
|
621
|
-
# Build request with optional randomization config
|
|
622
373
|
request_kwargs = {"agent_name": agent_name}
|
|
623
374
|
|
|
624
375
|
if randomization_cfg is not None:
|
|
@@ -640,13 +391,10 @@ class LuckyEngineClient:
|
|
|
640
391
|
"""
|
|
641
392
|
Synchronous RL step: apply action, wait for physics, return observation.
|
|
642
393
|
|
|
643
|
-
This is the recommended interface for RL training as it eliminates
|
|
644
|
-
one network round-trip compared to separate SendControl + GetObservation.
|
|
645
|
-
|
|
646
394
|
Args:
|
|
647
395
|
actions: Action vector to apply for this step.
|
|
648
396
|
agent_name: Agent name (empty = default agent).
|
|
649
|
-
timeout_ms: Server-side timeout for the step in milliseconds
|
|
397
|
+
timeout_ms: Server-side timeout for the step in milliseconds.
|
|
650
398
|
timeout: RPC timeout in seconds.
|
|
651
399
|
|
|
652
400
|
Returns:
|
|
@@ -666,14 +414,12 @@ class LuckyEngineClient:
|
|
|
666
414
|
if not resp.success:
|
|
667
415
|
raise RuntimeError(f"Step failed: {resp.message}")
|
|
668
416
|
|
|
669
|
-
# Extract observation from AgentFrame
|
|
670
417
|
agent_frame = resp.observation
|
|
671
418
|
observations = list(agent_frame.observations) if agent_frame.observations else []
|
|
672
419
|
actions_out = list(agent_frame.actions) if agent_frame.actions else []
|
|
673
420
|
timestamp_ms = getattr(agent_frame, "timestamp_ms", 0)
|
|
674
421
|
frame_number = getattr(agent_frame, "frame_number", 0)
|
|
675
422
|
|
|
676
|
-
# Look up cached schema for named access
|
|
677
423
|
cache_key = agent_name or "agent_0"
|
|
678
424
|
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
679
425
|
|
|
@@ -697,8 +443,8 @@ class LuckyEngineClient:
|
|
|
697
443
|
|
|
698
444
|
Args:
|
|
699
445
|
mode: "realtime", "deterministic", or "fast"
|
|
700
|
-
- realtime: Physics runs at 1x wall-clock speed
|
|
701
|
-
- deterministic: Physics runs at fixed rate
|
|
446
|
+
- realtime: Physics runs at 1x wall-clock speed
|
|
447
|
+
- deterministic: Physics runs at fixed rate
|
|
702
448
|
- fast: Physics runs as fast as possible (for RL training)
|
|
703
449
|
timeout: RPC timeout in seconds.
|
|
704
450
|
|
|
@@ -712,39 +458,19 @@ class LuckyEngineClient:
|
|
|
712
458
|
"deterministic": 1,
|
|
713
459
|
"fast": 2,
|
|
714
460
|
}
|
|
715
|
-
mode_value = mode_map.get(mode.lower(), 2)
|
|
461
|
+
mode_value = mode_map.get(mode.lower(), 2)
|
|
716
462
|
|
|
717
463
|
return self.scene.SetSimulationMode(
|
|
718
464
|
self.pb.scene.SetSimulationModeRequest(mode=mode_value),
|
|
719
465
|
timeout=timeout,
|
|
720
466
|
)
|
|
721
467
|
|
|
722
|
-
def get_simulation_mode(self, timeout: Optional[float] = None):
|
|
723
|
-
"""
|
|
724
|
-
Get current simulation timing mode.
|
|
725
|
-
|
|
726
|
-
Returns:
|
|
727
|
-
GetSimulationModeResponse with mode field (0=realtime, 1=deterministic, 2=fast).
|
|
728
|
-
"""
|
|
729
|
-
timeout = timeout or self.timeout
|
|
730
|
-
|
|
731
|
-
return self.scene.GetSimulationMode(
|
|
732
|
-
self.pb.scene.GetSimulationModeRequest(),
|
|
733
|
-
timeout=timeout,
|
|
734
|
-
)
|
|
735
|
-
|
|
736
468
|
def _randomization_to_proto(self, randomization_cfg: Any):
|
|
737
|
-
"""Convert domain randomization config to proto message.
|
|
738
|
-
|
|
739
|
-
Accepts any object with randomization config fields (DomainRandomizationConfig,
|
|
740
|
-
PhysicsDRCfg from luckylab, or similar).
|
|
741
|
-
"""
|
|
469
|
+
"""Convert domain randomization config to proto message."""
|
|
742
470
|
proto_kwargs = {}
|
|
743
471
|
|
|
744
|
-
# Helper to get attribute value, checking for None
|
|
745
472
|
def get_val(name: str, default=None):
|
|
746
473
|
val = getattr(randomization_cfg, name, default)
|
|
747
|
-
# Handle both None and empty tuples/lists
|
|
748
474
|
if val is None or (isinstance(val, (tuple, list)) and len(val) == 0):
|
|
749
475
|
return None
|
|
750
476
|
return val
|
|
@@ -766,7 +492,7 @@ class LuckyEngineClient:
|
|
|
766
492
|
if joint_vel is not None and joint_vel != 0.0:
|
|
767
493
|
proto_kwargs["joint_velocity_noise"] = joint_vel
|
|
768
494
|
|
|
769
|
-
# Physics parameters
|
|
495
|
+
# Physics parameters
|
|
770
496
|
friction = get_val("friction_range")
|
|
771
497
|
if friction is not None:
|
|
772
498
|
proto_kwargs["friction_range"] = list(friction)
|
|
@@ -812,186 +538,140 @@ class LuckyEngineClient:
|
|
|
812
538
|
|
|
813
539
|
return self.pb.agent.DomainRandomizationConfig(**proto_kwargs)
|
|
814
540
|
|
|
815
|
-
def
|
|
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:
|
|
816
551
|
"""
|
|
817
|
-
|
|
552
|
+
Draw velocity command visualization in LuckyEngine.
|
|
818
553
|
|
|
819
|
-
|
|
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.
|
|
820
565
|
"""
|
|
821
|
-
|
|
822
|
-
"stream_telemetry() is not yet confirmed working. "
|
|
823
|
-
"Remove this check if you want to test it."
|
|
824
|
-
)
|
|
825
|
-
return self.telemetry.StreamTelemetry(
|
|
826
|
-
self.pb.telemetry.StreamTelemetryRequest(target_fps=target_fps),
|
|
827
|
-
)
|
|
566
|
+
timeout = timeout or self.timeout
|
|
828
567
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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,
|
|
834
574
|
)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
575
|
+
|
|
576
|
+
request = self.pb.debug.DebugDrawRequest(
|
|
577
|
+
velocity_command=velocity_cmd,
|
|
578
|
+
clear_previous=clear_previous,
|
|
839
579
|
)
|
|
840
580
|
|
|
841
|
-
|
|
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(
|
|
842
589
|
self,
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
):
|
|
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:
|
|
850
597
|
"""
|
|
851
|
-
|
|
598
|
+
Draw a debug arrow in LuckyEngine.
|
|
852
599
|
|
|
853
600
|
Args:
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
Returns
|
|
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.
|
|
862
610
|
"""
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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,
|
|
866
622
|
)
|
|
867
|
-
if camera_id is not None:
|
|
868
|
-
request = self.pb.camera.StreamCameraRequest(
|
|
869
|
-
id=self.pb.common.EntityId(id=camera_id),
|
|
870
|
-
target_fps=target_fps,
|
|
871
|
-
width=width,
|
|
872
|
-
height=height,
|
|
873
|
-
format=format,
|
|
874
|
-
)
|
|
875
|
-
elif camera_name is not None:
|
|
876
|
-
request = self.pb.camera.StreamCameraRequest(
|
|
877
|
-
name=camera_name,
|
|
878
|
-
target_fps=target_fps,
|
|
879
|
-
width=width,
|
|
880
|
-
height=height,
|
|
881
|
-
format=format,
|
|
882
|
-
)
|
|
883
|
-
else:
|
|
884
|
-
raise ValueError("Either camera_id or camera_name must be provided")
|
|
885
623
|
|
|
886
|
-
|
|
624
|
+
request = self.pb.debug.DebugDrawRequest(
|
|
625
|
+
arrows=[arrow],
|
|
626
|
+
clear_previous=clear_previous,
|
|
627
|
+
)
|
|
887
628
|
|
|
888
|
-
|
|
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(
|
|
889
637
|
self,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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:
|
|
894
644
|
"""
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
Measures actual FPS and latency for observation calls.
|
|
645
|
+
Draw a debug line in LuckyEngine.
|
|
898
646
|
|
|
899
647
|
Args:
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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.
|
|
903
653
|
|
|
904
654
|
Returns:
|
|
905
|
-
|
|
655
|
+
True if draw succeeded, False otherwise.
|
|
906
656
|
"""
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if method == "get_observation":
|
|
917
|
-
while time.perf_counter() - start_time < duration_seconds:
|
|
918
|
-
call_start = time.perf_counter()
|
|
919
|
-
self.get_observation()
|
|
920
|
-
call_end = time.perf_counter()
|
|
921
|
-
|
|
922
|
-
latencies.append((call_end - call_start) * 1000) # ms
|
|
923
|
-
frame_times.append(call_end - last_frame_time)
|
|
924
|
-
last_frame_time = call_end
|
|
925
|
-
frame_count += 1
|
|
926
|
-
|
|
927
|
-
elif method == "stream_agent":
|
|
928
|
-
stream = self.stream_agent(target_fps=1000) # Request max FPS
|
|
929
|
-
for frame in stream:
|
|
930
|
-
now = time.perf_counter()
|
|
931
|
-
if now - start_time >= duration_seconds:
|
|
932
|
-
break
|
|
933
|
-
|
|
934
|
-
frame_times.append(now - last_frame_time)
|
|
935
|
-
last_frame_time = now
|
|
936
|
-
frame_count += 1
|
|
937
|
-
|
|
938
|
-
total_time = time.perf_counter() - start_time
|
|
939
|
-
|
|
940
|
-
# Calculate statistics
|
|
941
|
-
actual_fps = frame_count / total_time if total_time > 0 else 0
|
|
942
|
-
|
|
943
|
-
if len(frame_times) > 1:
|
|
944
|
-
# Remove first frame (warmup)
|
|
945
|
-
frame_times = frame_times[1:]
|
|
946
|
-
fps_from_frames = 1.0 / statistics.mean(frame_times) if frame_times else 0
|
|
947
|
-
else:
|
|
948
|
-
fps_from_frames = actual_fps
|
|
949
|
-
|
|
950
|
-
result = BenchmarkResult(
|
|
951
|
-
method=method,
|
|
952
|
-
duration_seconds=total_time,
|
|
953
|
-
frame_count=frame_count,
|
|
954
|
-
actual_fps=actual_fps,
|
|
955
|
-
avg_latency_ms=statistics.mean(latencies) if latencies else 0,
|
|
956
|
-
min_latency_ms=min(latencies) if latencies else 0,
|
|
957
|
-
max_latency_ms=max(latencies) if latencies else 0,
|
|
958
|
-
std_latency_ms=statistics.stdev(latencies) if len(latencies) > 1 else 0,
|
|
959
|
-
p50_latency_ms=statistics.median(latencies) if latencies else 0,
|
|
960
|
-
p99_latency_ms=sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0,
|
|
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
|
+
),
|
|
961
665
|
)
|
|
962
666
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
print(f" min: {result.min_latency_ms:.2f}")
|
|
975
|
-
print(f" max: {result.max_latency_ms:.2f}")
|
|
976
|
-
print(f" std: {result.std_latency_ms:.2f}")
|
|
977
|
-
print(f" p50: {result.p50_latency_ms:.2f}")
|
|
978
|
-
print(f" p99: {result.p99_latency_ms:.2f}")
|
|
979
|
-
print(f"{'=' * 50}\n")
|
|
980
|
-
|
|
981
|
-
return result
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
@dataclass
|
|
985
|
-
class BenchmarkResult:
|
|
986
|
-
"""Results from a benchmark run."""
|
|
987
|
-
|
|
988
|
-
method: str
|
|
989
|
-
duration_seconds: float
|
|
990
|
-
frame_count: int
|
|
991
|
-
actual_fps: float
|
|
992
|
-
avg_latency_ms: float
|
|
993
|
-
min_latency_ms: float
|
|
994
|
-
max_latency_ms: float
|
|
995
|
-
std_latency_ms: float
|
|
996
|
-
p50_latency_ms: float
|
|
997
|
-
p99_latency_ms: float
|
|
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
|