luckyrobots 0.1.72__py3-none-any.whl → 0.1.73__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 +20 -491
- luckyrobots/config/robots.yaml +72 -48
- luckyrobots/engine/__init__.py +5 -20
- 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.73.dist-info}/METADATA +1 -1
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.73.dist-info}/RECORD +11 -15
- 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 → luckyrobots-0.1.73.dist-info}/WHEEL +0 -0
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.73.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,18 @@ 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 media_pb2 # type: ignore
|
|
26
21
|
from .grpc.generated import mujoco_pb2 # type: ignore
|
|
27
22
|
from .grpc.generated import mujoco_pb2_grpc # type: ignore
|
|
28
23
|
from .grpc.generated import scene_pb2 # type: ignore
|
|
29
24
|
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
25
|
except Exception as e: # pragma: no cover
|
|
35
26
|
raise ImportError(
|
|
36
27
|
"Missing generated gRPC stubs. Regenerate them from the protos in "
|
|
37
28
|
"src/luckyrobots/grpc/proto into src/luckyrobots/grpc/generated."
|
|
38
29
|
) from e
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
from .models import ObservationResponse, StateSnapshot, DomainRandomizationConfig
|
|
31
|
+
from .models import ObservationResponse
|
|
42
32
|
|
|
43
33
|
|
|
44
34
|
class GrpcConnectionError(Exception):
|
|
@@ -53,24 +43,18 @@ class LuckyEngineClient:
|
|
|
53
43
|
"""
|
|
54
44
|
Client for connecting to the LuckyEngine gRPC server.
|
|
55
45
|
|
|
56
|
-
Provides access to
|
|
57
|
-
|
|
58
|
-
- SceneService
|
|
59
|
-
- MujocoService
|
|
60
|
-
- TelemetryService
|
|
61
|
-
- AgentService
|
|
62
|
-
- ViewportService
|
|
63
|
-
- CameraService
|
|
46
|
+
Provides access to gRPC services for RL training:
|
|
47
|
+
- AgentService: observations, stepping, resets
|
|
48
|
+
- SceneService: simulation mode control
|
|
49
|
+
- MujocoService: health checks
|
|
64
50
|
|
|
65
51
|
Usage:
|
|
66
52
|
client = LuckyEngineClient(host="127.0.0.1", port=50051)
|
|
67
53
|
client.connect()
|
|
54
|
+
client.wait_for_server()
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
joint_state = client.mujoco.GetJointState(
|
|
72
|
-
client.pb.mujoco.GetJointStateRequest()
|
|
73
|
-
)
|
|
56
|
+
schema = client.get_agent_schema()
|
|
57
|
+
obs = client.step(actions=[0.0] * 12)
|
|
74
58
|
|
|
75
59
|
client.close()
|
|
76
60
|
"""
|
|
@@ -102,25 +86,17 @@ class LuckyEngineClient:
|
|
|
102
86
|
# Service stubs (populated after connect)
|
|
103
87
|
self._scene = None
|
|
104
88
|
self._mujoco = None
|
|
105
|
-
self._telemetry = None
|
|
106
89
|
self._agent = None
|
|
107
|
-
self._viewport = None
|
|
108
|
-
self._camera = None
|
|
109
90
|
|
|
110
91
|
# Cached agent schemas: agent_name -> (observation_names, action_names)
|
|
111
|
-
# Populated lazily by get_agent_schema() or fetch_schema()
|
|
112
92
|
self._schema_cache: dict[str, tuple[list[str], list[str]]] = {}
|
|
113
93
|
|
|
114
94
|
# Protobuf modules (for discoverability + explicit imports).
|
|
115
95
|
self._pb = SimpleNamespace(
|
|
116
96
|
common=common_pb2,
|
|
117
|
-
media=media_pb2,
|
|
118
97
|
scene=scene_pb2,
|
|
119
98
|
mujoco=mujoco_pb2,
|
|
120
|
-
telemetry=telemetry_pb2,
|
|
121
99
|
agent=agent_pb2,
|
|
122
|
-
viewport=viewport_pb2,
|
|
123
|
-
camera=camera_pb2,
|
|
124
100
|
)
|
|
125
101
|
|
|
126
102
|
def connect(self) -> None:
|
|
@@ -145,10 +121,7 @@ class LuckyEngineClient:
|
|
|
145
121
|
# Create service stubs
|
|
146
122
|
self._scene = scene_pb2_grpc.SceneServiceStub(self._channel)
|
|
147
123
|
self._mujoco = mujoco_pb2_grpc.MujocoServiceStub(self._channel)
|
|
148
|
-
self._telemetry = telemetry_pb2_grpc.TelemetryServiceStub(self._channel)
|
|
149
124
|
self._agent = agent_pb2_grpc.AgentServiceStub(self._channel)
|
|
150
|
-
self._viewport = viewport_pb2_grpc.ViewportServiceStub(self._channel)
|
|
151
|
-
self._camera = camera_pb2_grpc.CameraServiceStub(self._channel)
|
|
152
125
|
|
|
153
126
|
logger.info(f"Channel opened to {target} (server not verified yet)")
|
|
154
127
|
|
|
@@ -162,10 +135,7 @@ class LuckyEngineClient:
|
|
|
162
135
|
self._channel = None
|
|
163
136
|
self._scene = None
|
|
164
137
|
self._mujoco = None
|
|
165
|
-
self._telemetry = None
|
|
166
138
|
self._agent = None
|
|
167
|
-
self._viewport = None
|
|
168
|
-
self._camera = None
|
|
169
139
|
logger.info("gRPC channel closed")
|
|
170
140
|
|
|
171
141
|
def is_connected(self) -> bool:
|
|
@@ -209,8 +179,6 @@ class LuckyEngineClient:
|
|
|
209
179
|
Returns:
|
|
210
180
|
True if server became available, False if timeout.
|
|
211
181
|
"""
|
|
212
|
-
import time
|
|
213
|
-
|
|
214
182
|
start = time.perf_counter()
|
|
215
183
|
|
|
216
184
|
while time.perf_counter() - start < timeout:
|
|
@@ -228,8 +196,6 @@ class LuckyEngineClient:
|
|
|
228
196
|
|
|
229
197
|
return False
|
|
230
198
|
|
|
231
|
-
# --- Protobuf modules (discoverable + explicit) ---
|
|
232
|
-
|
|
233
199
|
@property
|
|
234
200
|
def pb(self) -> Any:
|
|
235
201
|
"""Access protobuf modules grouped by domain (e.g., `client.pb.scene`)."""
|
|
@@ -244,21 +210,9 @@ class LuckyEngineClient:
|
|
|
244
210
|
"""Set the default robot name used by calls that accept an optional robot_name."""
|
|
245
211
|
self._robot_name = robot_name
|
|
246
212
|
|
|
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
213
|
@property
|
|
260
214
|
def scene(self) -> Any:
|
|
261
|
-
"""SceneService stub.
|
|
215
|
+
"""SceneService stub."""
|
|
262
216
|
if self._scene is None:
|
|
263
217
|
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
264
218
|
return self._scene
|
|
@@ -270,13 +224,6 @@ class LuckyEngineClient:
|
|
|
270
224
|
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
271
225
|
return self._mujoco
|
|
272
226
|
|
|
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
227
|
@property
|
|
281
228
|
def agent(self) -> Any:
|
|
282
229
|
"""AgentService stub."""
|
|
@@ -284,81 +231,17 @@ class LuckyEngineClient:
|
|
|
284
231
|
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
285
232
|
return self._agent
|
|
286
233
|
|
|
287
|
-
@property
|
|
288
|
-
def viewport(self) -> Any:
|
|
289
|
-
"""ViewportService stub. [PLACEHOLDER - not confirmed working]"""
|
|
290
|
-
if self._viewport is None:
|
|
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:
|
|
298
|
-
raise GrpcConnectionError("Not connected. Call connect() first.")
|
|
299
|
-
return self._camera
|
|
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
|
-
)
|
|
314
|
-
|
|
315
234
|
def get_mujoco_info(self, robot_name: str = "", timeout: Optional[float] = None):
|
|
316
|
-
"""Get MuJoCo model information."""
|
|
235
|
+
"""Get MuJoCo model information (joint names, limits, etc.)."""
|
|
317
236
|
timeout = timeout or self.timeout
|
|
318
237
|
robot_name = robot_name or self._robot_name
|
|
319
238
|
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
|
-
)
|
|
239
|
+
raise ValueError("robot_name is required")
|
|
324
240
|
return self.mujoco.GetMujocoInfo(
|
|
325
241
|
self.pb.mujoco.GetMujocoInfoRequest(robot_name=robot_name),
|
|
326
242
|
timeout=timeout,
|
|
327
243
|
)
|
|
328
244
|
|
|
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
245
|
def get_agent_schema(self, agent_name: str = "", timeout: Optional[float] = None):
|
|
363
246
|
"""Get agent schema (observation/action sizes and names).
|
|
364
247
|
|
|
@@ -395,18 +278,6 @@ class LuckyEngineClient:
|
|
|
395
278
|
|
|
396
279
|
return resp
|
|
397
280
|
|
|
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
281
|
def get_observation(
|
|
411
282
|
self,
|
|
412
283
|
agent_name: str = "",
|
|
@@ -415,10 +286,6 @@ class LuckyEngineClient:
|
|
|
415
286
|
"""
|
|
416
287
|
Get the RL observation vector for an agent.
|
|
417
288
|
|
|
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
289
|
Args:
|
|
423
290
|
agent_name: Agent name (empty = default agent).
|
|
424
291
|
timeout: RPC timeout.
|
|
@@ -435,30 +302,17 @@ class LuckyEngineClient:
|
|
|
435
302
|
"LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
|
|
436
303
|
)
|
|
437
304
|
|
|
438
|
-
# Request only the agent's RL observation (agent_frame in gRPC terms).
|
|
439
|
-
# Joint state and telemetry have their own dedicated methods.
|
|
440
305
|
resp = self.agent.GetObservation(
|
|
441
306
|
self.pb.agent.GetObservationRequest(
|
|
442
307
|
robot_name=resolved_robot_name,
|
|
443
308
|
agent_name=agent_name,
|
|
444
309
|
include_joint_state=False,
|
|
445
|
-
include_agent_frame=True,
|
|
310
|
+
include_agent_frame=True,
|
|
446
311
|
include_telemetry=False,
|
|
447
312
|
),
|
|
448
313
|
timeout=timeout,
|
|
449
314
|
)
|
|
450
315
|
|
|
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
316
|
agent_frame = getattr(resp, "agent_frame", None)
|
|
463
317
|
observations = []
|
|
464
318
|
actions = []
|
|
@@ -471,7 +325,6 @@ class LuckyEngineClient:
|
|
|
471
325
|
timestamp_ms = getattr(agent_frame, "timestamp_ms", timestamp_ms)
|
|
472
326
|
frame_number = getattr(agent_frame, "frame_number", frame_number)
|
|
473
327
|
|
|
474
|
-
# Look up cached schema for named access
|
|
475
328
|
cache_key = agent_name or "agent_0"
|
|
476
329
|
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
477
330
|
|
|
@@ -485,117 +338,6 @@ class LuckyEngineClient:
|
|
|
485
338
|
action_names=action_names,
|
|
486
339
|
)
|
|
487
340
|
|
|
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
341
|
def reset_agent(
|
|
600
342
|
self,
|
|
601
343
|
agent_name: str = "",
|
|
@@ -603,13 +345,10 @@ class LuckyEngineClient:
|
|
|
603
345
|
timeout: Optional[float] = None,
|
|
604
346
|
):
|
|
605
347
|
"""
|
|
606
|
-
Reset a specific agent
|
|
607
|
-
|
|
608
|
-
Useful for multi-env RL where individual agents need to be reset without resetting the entire scene.
|
|
348
|
+
Reset a specific agent.
|
|
609
349
|
|
|
610
350
|
Args:
|
|
611
|
-
agent_name: Agent logical name.
|
|
612
|
-
Empty string means "default agent" (agent_0).
|
|
351
|
+
agent_name: Agent logical name. Empty string means default agent.
|
|
613
352
|
randomization_cfg: Optional domain randomization config for this reset.
|
|
614
353
|
timeout: Timeout in seconds (uses default if None).
|
|
615
354
|
|
|
@@ -618,7 +357,6 @@ class LuckyEngineClient:
|
|
|
618
357
|
"""
|
|
619
358
|
timeout = timeout or self.timeout
|
|
620
359
|
|
|
621
|
-
# Build request with optional randomization config
|
|
622
360
|
request_kwargs = {"agent_name": agent_name}
|
|
623
361
|
|
|
624
362
|
if randomization_cfg is not None:
|
|
@@ -640,13 +378,10 @@ class LuckyEngineClient:
|
|
|
640
378
|
"""
|
|
641
379
|
Synchronous RL step: apply action, wait for physics, return observation.
|
|
642
380
|
|
|
643
|
-
This is the recommended interface for RL training as it eliminates
|
|
644
|
-
one network round-trip compared to separate SendControl + GetObservation.
|
|
645
|
-
|
|
646
381
|
Args:
|
|
647
382
|
actions: Action vector to apply for this step.
|
|
648
383
|
agent_name: Agent name (empty = default agent).
|
|
649
|
-
timeout_ms: Server-side timeout for the step in milliseconds
|
|
384
|
+
timeout_ms: Server-side timeout for the step in milliseconds.
|
|
650
385
|
timeout: RPC timeout in seconds.
|
|
651
386
|
|
|
652
387
|
Returns:
|
|
@@ -666,14 +401,12 @@ class LuckyEngineClient:
|
|
|
666
401
|
if not resp.success:
|
|
667
402
|
raise RuntimeError(f"Step failed: {resp.message}")
|
|
668
403
|
|
|
669
|
-
# Extract observation from AgentFrame
|
|
670
404
|
agent_frame = resp.observation
|
|
671
405
|
observations = list(agent_frame.observations) if agent_frame.observations else []
|
|
672
406
|
actions_out = list(agent_frame.actions) if agent_frame.actions else []
|
|
673
407
|
timestamp_ms = getattr(agent_frame, "timestamp_ms", 0)
|
|
674
408
|
frame_number = getattr(agent_frame, "frame_number", 0)
|
|
675
409
|
|
|
676
|
-
# Look up cached schema for named access
|
|
677
410
|
cache_key = agent_name or "agent_0"
|
|
678
411
|
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
679
412
|
|
|
@@ -697,8 +430,8 @@ class LuckyEngineClient:
|
|
|
697
430
|
|
|
698
431
|
Args:
|
|
699
432
|
mode: "realtime", "deterministic", or "fast"
|
|
700
|
-
- realtime: Physics runs at 1x wall-clock speed
|
|
701
|
-
- deterministic: Physics runs at fixed rate
|
|
433
|
+
- realtime: Physics runs at 1x wall-clock speed
|
|
434
|
+
- deterministic: Physics runs at fixed rate
|
|
702
435
|
- fast: Physics runs as fast as possible (for RL training)
|
|
703
436
|
timeout: RPC timeout in seconds.
|
|
704
437
|
|
|
@@ -712,39 +445,19 @@ class LuckyEngineClient:
|
|
|
712
445
|
"deterministic": 1,
|
|
713
446
|
"fast": 2,
|
|
714
447
|
}
|
|
715
|
-
mode_value = mode_map.get(mode.lower(), 2)
|
|
448
|
+
mode_value = mode_map.get(mode.lower(), 2)
|
|
716
449
|
|
|
717
450
|
return self.scene.SetSimulationMode(
|
|
718
451
|
self.pb.scene.SetSimulationModeRequest(mode=mode_value),
|
|
719
452
|
timeout=timeout,
|
|
720
453
|
)
|
|
721
454
|
|
|
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
455
|
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
|
-
"""
|
|
456
|
+
"""Convert domain randomization config to proto message."""
|
|
742
457
|
proto_kwargs = {}
|
|
743
458
|
|
|
744
|
-
# Helper to get attribute value, checking for None
|
|
745
459
|
def get_val(name: str, default=None):
|
|
746
460
|
val = getattr(randomization_cfg, name, default)
|
|
747
|
-
# Handle both None and empty tuples/lists
|
|
748
461
|
if val is None or (isinstance(val, (tuple, list)) and len(val) == 0):
|
|
749
462
|
return None
|
|
750
463
|
return val
|
|
@@ -766,7 +479,7 @@ class LuckyEngineClient:
|
|
|
766
479
|
if joint_vel is not None and joint_vel != 0.0:
|
|
767
480
|
proto_kwargs["joint_velocity_noise"] = joint_vel
|
|
768
481
|
|
|
769
|
-
# Physics parameters
|
|
482
|
+
# Physics parameters
|
|
770
483
|
friction = get_val("friction_range")
|
|
771
484
|
if friction is not None:
|
|
772
485
|
proto_kwargs["friction_range"] = list(friction)
|
|
@@ -811,187 +524,3 @@ class LuckyEngineClient:
|
|
|
811
524
|
proto_kwargs["terrain_difficulty"] = terrain_diff
|
|
812
525
|
|
|
813
526
|
return self.pb.agent.DomainRandomizationConfig(**proto_kwargs)
|
|
814
|
-
|
|
815
|
-
def stream_telemetry(self, target_fps: int = 30):
|
|
816
|
-
"""
|
|
817
|
-
Start streaming telemetry data. [PLACEHOLDER - not confirmed working]
|
|
818
|
-
|
|
819
|
-
Returns an iterator of TelemetryFrame messages.
|
|
820
|
-
"""
|
|
821
|
-
raise NotImplementedError(
|
|
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
|
-
)
|
|
828
|
-
|
|
829
|
-
def list_cameras(self, timeout: Optional[float] = None):
|
|
830
|
-
"""List available cameras. [PLACEHOLDER - not confirmed working]"""
|
|
831
|
-
raise NotImplementedError(
|
|
832
|
-
"list_cameras() is not yet confirmed working. "
|
|
833
|
-
"Remove this check if you want to test it."
|
|
834
|
-
)
|
|
835
|
-
timeout = timeout or self.timeout
|
|
836
|
-
return self.camera.ListCameras(
|
|
837
|
-
self.pb.camera.ListCamerasRequest(),
|
|
838
|
-
timeout=timeout,
|
|
839
|
-
)
|
|
840
|
-
|
|
841
|
-
def stream_camera(
|
|
842
|
-
self,
|
|
843
|
-
camera_id: Optional[int] = None,
|
|
844
|
-
camera_name: Optional[str] = None,
|
|
845
|
-
target_fps: int = 30,
|
|
846
|
-
width: int = 640,
|
|
847
|
-
height: int = 480,
|
|
848
|
-
format: str = "raw",
|
|
849
|
-
):
|
|
850
|
-
"""
|
|
851
|
-
Start streaming camera frames. [PLACEHOLDER - not confirmed working]
|
|
852
|
-
|
|
853
|
-
Args:
|
|
854
|
-
camera_id: Camera entity ID (use either id or name).
|
|
855
|
-
camera_name: Camera name (use either id or name).
|
|
856
|
-
target_fps: Desired frames per second.
|
|
857
|
-
width: Desired width (0 = native).
|
|
858
|
-
height: Desired height (0 = native).
|
|
859
|
-
format: Image format ("raw" or "jpeg").
|
|
860
|
-
|
|
861
|
-
Returns an iterator of ImageFrame messages.
|
|
862
|
-
"""
|
|
863
|
-
raise NotImplementedError(
|
|
864
|
-
"stream_camera() is not yet confirmed working. "
|
|
865
|
-
"Remove this check if you want to test it."
|
|
866
|
-
)
|
|
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
|
-
|
|
886
|
-
return self.camera.StreamCamera(request)
|
|
887
|
-
|
|
888
|
-
def benchmark(
|
|
889
|
-
self,
|
|
890
|
-
duration_seconds: float = 5.0,
|
|
891
|
-
method: str = "get_observation",
|
|
892
|
-
print_results: bool = True,
|
|
893
|
-
) -> "BenchmarkResult":
|
|
894
|
-
"""
|
|
895
|
-
Benchmark gRPC performance.
|
|
896
|
-
|
|
897
|
-
Measures actual FPS and latency for observation calls.
|
|
898
|
-
|
|
899
|
-
Args:
|
|
900
|
-
duration_seconds: How long to run the benchmark.
|
|
901
|
-
method: Which method to benchmark ("get_observation" or "stream_agent").
|
|
902
|
-
print_results: Whether to print results to console.
|
|
903
|
-
|
|
904
|
-
Returns:
|
|
905
|
-
BenchmarkResult with FPS and latency statistics.
|
|
906
|
-
"""
|
|
907
|
-
if method not in ("get_observation", "stream_agent"):
|
|
908
|
-
raise ValueError(f"Unknown method: {method}. Use 'get_observation' or 'stream_agent'.")
|
|
909
|
-
|
|
910
|
-
latencies: list[float] = []
|
|
911
|
-
frame_times: list[float] = []
|
|
912
|
-
frame_count = 0
|
|
913
|
-
start_time = time.perf_counter()
|
|
914
|
-
last_frame_time = start_time
|
|
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,
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
if print_results:
|
|
964
|
-
print(f"\n{'=' * 50}")
|
|
965
|
-
print(f"Benchmark Results ({method})")
|
|
966
|
-
print(f"{'=' * 50}")
|
|
967
|
-
print(f"Duration: {result.duration_seconds:.2f}s")
|
|
968
|
-
print(f"Frames: {result.frame_count}")
|
|
969
|
-
print(f"Actual FPS: {result.actual_fps:.1f}")
|
|
970
|
-
if latencies:
|
|
971
|
-
print(f"{'─' * 50}")
|
|
972
|
-
print(f"Latency (ms):")
|
|
973
|
-
print(f" avg: {result.avg_latency_ms:.2f}")
|
|
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
|