luckyrobots 0.1.71__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 +90 -458
- luckyrobots/config/robots.yaml +72 -48
- luckyrobots/engine/__init__.py +5 -20
- luckyrobots/grpc/generated/agent_pb2.py +7 -3
- luckyrobots/grpc/generated/agent_pb2_grpc.py +46 -0
- luckyrobots/grpc/generated/scene_pb2.py +13 -3
- luckyrobots/grpc/generated/scene_pb2_grpc.py +88 -0
- luckyrobots/grpc/proto/agent.proto +29 -0
- luckyrobots/grpc/proto/scene.proto +34 -0
- luckyrobots/luckyrobots.py +37 -6
- luckyrobots/models/__init__.py +2 -14
- luckyrobots/models/observation.py +4 -33
- luckyrobots/utils.py +1 -43
- {luckyrobots-0.1.71.dist-info → luckyrobots-0.1.73.dist-info}/METADATA +1 -1
- {luckyrobots-0.1.71.dist-info → luckyrobots-0.1.73.dist-info}/RECORD +18 -22
- 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.71.dist-info → luckyrobots-0.1.73.dist-info}/WHEEL +0 -0
- {luckyrobots-0.1.71.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,163 +338,126 @@ class LuckyEngineClient:
|
|
|
485
338
|
action_names=action_names,
|
|
486
339
|
)
|
|
487
340
|
|
|
488
|
-
def
|
|
341
|
+
def reset_agent(
|
|
489
342
|
self,
|
|
490
343
|
agent_name: str = "",
|
|
491
|
-
|
|
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",
|
|
344
|
+
randomization_cfg: Optional[Any] = None,
|
|
497
345
|
timeout: Optional[float] = None,
|
|
498
|
-
)
|
|
346
|
+
):
|
|
499
347
|
"""
|
|
500
|
-
|
|
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)
|
|
348
|
+
Reset a specific agent.
|
|
508
349
|
|
|
509
350
|
Args:
|
|
510
|
-
agent_name: Agent name
|
|
511
|
-
|
|
512
|
-
|
|
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.
|
|
351
|
+
agent_name: Agent logical name. Empty string means default agent.
|
|
352
|
+
randomization_cfg: Optional domain randomization config for this reset.
|
|
353
|
+
timeout: Timeout in seconds (uses default if None).
|
|
518
354
|
|
|
519
355
|
Returns:
|
|
520
|
-
|
|
356
|
+
ResetAgentResponse with success and message fields.
|
|
521
357
|
"""
|
|
522
358
|
timeout = timeout or self.timeout
|
|
523
359
|
|
|
524
|
-
|
|
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
|
-
)
|
|
360
|
+
request_kwargs = {"agent_name": agent_name}
|
|
530
361
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
cameras.append(
|
|
535
|
-
self.pb.agent.GetCameraFrameRequest(
|
|
536
|
-
name=name,
|
|
537
|
-
width=width,
|
|
538
|
-
height=height,
|
|
539
|
-
format=format,
|
|
540
|
-
)
|
|
541
|
-
)
|
|
362
|
+
if randomization_cfg is not None:
|
|
363
|
+
randomization_proto = self._randomization_to_proto(randomization_cfg)
|
|
364
|
+
request_kwargs["dr_config"] = randomization_proto
|
|
542
365
|
|
|
543
|
-
|
|
544
|
-
self.pb.agent.
|
|
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
|
-
),
|
|
366
|
+
return self.agent.ResetAgent(
|
|
367
|
+
self.pb.agent.ResetAgentRequest(**request_kwargs),
|
|
552
368
|
timeout=timeout,
|
|
553
369
|
)
|
|
554
370
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
)
|
|
371
|
+
def step(
|
|
372
|
+
self,
|
|
373
|
+
actions: list[float],
|
|
374
|
+
agent_name: str = "",
|
|
375
|
+
timeout_ms: int = 0,
|
|
376
|
+
timeout: Optional[float] = None,
|
|
377
|
+
) -> ObservationResponse:
|
|
378
|
+
"""
|
|
379
|
+
Synchronous RL step: apply action, wait for physics, return observation.
|
|
578
380
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
frame_number=getattr(resp, "frame_number", 0),
|
|
585
|
-
)
|
|
381
|
+
Args:
|
|
382
|
+
actions: Action vector to apply for this step.
|
|
383
|
+
agent_name: Agent name (empty = default agent).
|
|
384
|
+
timeout_ms: Server-side timeout for the step in milliseconds.
|
|
385
|
+
timeout: RPC timeout in seconds.
|
|
586
386
|
|
|
587
|
-
|
|
387
|
+
Returns:
|
|
388
|
+
ObservationResponse with observation after physics step.
|
|
588
389
|
"""
|
|
589
|
-
|
|
390
|
+
timeout = timeout or self.timeout
|
|
590
391
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
392
|
+
resp = self.agent.Step(
|
|
393
|
+
self.pb.agent.StepRequest(
|
|
394
|
+
agent_name=agent_name,
|
|
395
|
+
actions=actions,
|
|
396
|
+
timeout_ms=timeout_ms,
|
|
596
397
|
),
|
|
398
|
+
timeout=timeout,
|
|
597
399
|
)
|
|
598
400
|
|
|
599
|
-
|
|
401
|
+
if not resp.success:
|
|
402
|
+
raise RuntimeError(f"Step failed: {resp.message}")
|
|
403
|
+
|
|
404
|
+
agent_frame = resp.observation
|
|
405
|
+
observations = list(agent_frame.observations) if agent_frame.observations else []
|
|
406
|
+
actions_out = list(agent_frame.actions) if agent_frame.actions else []
|
|
407
|
+
timestamp_ms = getattr(agent_frame, "timestamp_ms", 0)
|
|
408
|
+
frame_number = getattr(agent_frame, "frame_number", 0)
|
|
409
|
+
|
|
410
|
+
cache_key = agent_name or "agent_0"
|
|
411
|
+
obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
|
|
412
|
+
|
|
413
|
+
return ObservationResponse(
|
|
414
|
+
observation=observations,
|
|
415
|
+
actions=actions_out,
|
|
416
|
+
timestamp_ms=timestamp_ms,
|
|
417
|
+
frame_number=frame_number,
|
|
418
|
+
agent_name=cache_key,
|
|
419
|
+
observation_names=obs_names,
|
|
420
|
+
action_names=action_names,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def set_simulation_mode(
|
|
600
424
|
self,
|
|
601
|
-
|
|
602
|
-
randomization_cfg: Optional[Any] = None,
|
|
425
|
+
mode: str = "fast",
|
|
603
426
|
timeout: Optional[float] = None,
|
|
604
427
|
):
|
|
605
428
|
"""
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
Useful for multi-env RL where individual agents need to be reset without resetting the entire scene.
|
|
429
|
+
Set simulation timing mode.
|
|
609
430
|
|
|
610
431
|
Args:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
432
|
+
mode: "realtime", "deterministic", or "fast"
|
|
433
|
+
- realtime: Physics runs at 1x wall-clock speed
|
|
434
|
+
- deterministic: Physics runs at fixed rate
|
|
435
|
+
- fast: Physics runs as fast as possible (for RL training)
|
|
436
|
+
timeout: RPC timeout in seconds.
|
|
615
437
|
|
|
616
438
|
Returns:
|
|
617
|
-
|
|
439
|
+
SetSimulationModeResponse with success and current mode.
|
|
618
440
|
"""
|
|
619
441
|
timeout = timeout or self.timeout
|
|
620
442
|
|
|
621
|
-
|
|
622
|
-
|
|
443
|
+
mode_map = {
|
|
444
|
+
"realtime": 0,
|
|
445
|
+
"deterministic": 1,
|
|
446
|
+
"fast": 2,
|
|
447
|
+
}
|
|
448
|
+
mode_value = mode_map.get(mode.lower(), 2)
|
|
623
449
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
request_kwargs["dr_config"] = randomization_proto
|
|
627
|
-
|
|
628
|
-
return self.agent.ResetAgent(
|
|
629
|
-
self.pb.agent.ResetAgentRequest(**request_kwargs),
|
|
450
|
+
return self.scene.SetSimulationMode(
|
|
451
|
+
self.pb.scene.SetSimulationModeRequest(mode=mode_value),
|
|
630
452
|
timeout=timeout,
|
|
631
453
|
)
|
|
632
454
|
|
|
633
455
|
def _randomization_to_proto(self, randomization_cfg: Any):
|
|
634
|
-
"""Convert domain randomization config to proto message.
|
|
635
|
-
|
|
636
|
-
Accepts any object with randomization config fields (DomainRandomizationConfig,
|
|
637
|
-
PhysicsDRCfg from luckylab, or similar).
|
|
638
|
-
"""
|
|
456
|
+
"""Convert domain randomization config to proto message."""
|
|
639
457
|
proto_kwargs = {}
|
|
640
458
|
|
|
641
|
-
# Helper to get attribute value, checking for None
|
|
642
459
|
def get_val(name: str, default=None):
|
|
643
460
|
val = getattr(randomization_cfg, name, default)
|
|
644
|
-
# Handle both None and empty tuples/lists
|
|
645
461
|
if val is None or (isinstance(val, (tuple, list)) and len(val) == 0):
|
|
646
462
|
return None
|
|
647
463
|
return val
|
|
@@ -663,7 +479,7 @@ class LuckyEngineClient:
|
|
|
663
479
|
if joint_vel is not None and joint_vel != 0.0:
|
|
664
480
|
proto_kwargs["joint_velocity_noise"] = joint_vel
|
|
665
481
|
|
|
666
|
-
# Physics parameters
|
|
482
|
+
# Physics parameters
|
|
667
483
|
friction = get_val("friction_range")
|
|
668
484
|
if friction is not None:
|
|
669
485
|
proto_kwargs["friction_range"] = list(friction)
|
|
@@ -708,187 +524,3 @@ class LuckyEngineClient:
|
|
|
708
524
|
proto_kwargs["terrain_difficulty"] = terrain_diff
|
|
709
525
|
|
|
710
526
|
return self.pb.agent.DomainRandomizationConfig(**proto_kwargs)
|
|
711
|
-
|
|
712
|
-
def stream_telemetry(self, target_fps: int = 30):
|
|
713
|
-
"""
|
|
714
|
-
Start streaming telemetry data. [PLACEHOLDER - not confirmed working]
|
|
715
|
-
|
|
716
|
-
Returns an iterator of TelemetryFrame messages.
|
|
717
|
-
"""
|
|
718
|
-
raise NotImplementedError(
|
|
719
|
-
"stream_telemetry() is not yet confirmed working. "
|
|
720
|
-
"Remove this check if you want to test it."
|
|
721
|
-
)
|
|
722
|
-
return self.telemetry.StreamTelemetry(
|
|
723
|
-
self.pb.telemetry.StreamTelemetryRequest(target_fps=target_fps),
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
def list_cameras(self, timeout: Optional[float] = None):
|
|
727
|
-
"""List available cameras. [PLACEHOLDER - not confirmed working]"""
|
|
728
|
-
raise NotImplementedError(
|
|
729
|
-
"list_cameras() is not yet confirmed working. "
|
|
730
|
-
"Remove this check if you want to test it."
|
|
731
|
-
)
|
|
732
|
-
timeout = timeout or self.timeout
|
|
733
|
-
return self.camera.ListCameras(
|
|
734
|
-
self.pb.camera.ListCamerasRequest(),
|
|
735
|
-
timeout=timeout,
|
|
736
|
-
)
|
|
737
|
-
|
|
738
|
-
def stream_camera(
|
|
739
|
-
self,
|
|
740
|
-
camera_id: Optional[int] = None,
|
|
741
|
-
camera_name: Optional[str] = None,
|
|
742
|
-
target_fps: int = 30,
|
|
743
|
-
width: int = 640,
|
|
744
|
-
height: int = 480,
|
|
745
|
-
format: str = "raw",
|
|
746
|
-
):
|
|
747
|
-
"""
|
|
748
|
-
Start streaming camera frames. [PLACEHOLDER - not confirmed working]
|
|
749
|
-
|
|
750
|
-
Args:
|
|
751
|
-
camera_id: Camera entity ID (use either id or name).
|
|
752
|
-
camera_name: Camera name (use either id or name).
|
|
753
|
-
target_fps: Desired frames per second.
|
|
754
|
-
width: Desired width (0 = native).
|
|
755
|
-
height: Desired height (0 = native).
|
|
756
|
-
format: Image format ("raw" or "jpeg").
|
|
757
|
-
|
|
758
|
-
Returns an iterator of ImageFrame messages.
|
|
759
|
-
"""
|
|
760
|
-
raise NotImplementedError(
|
|
761
|
-
"stream_camera() is not yet confirmed working. "
|
|
762
|
-
"Remove this check if you want to test it."
|
|
763
|
-
)
|
|
764
|
-
if camera_id is not None:
|
|
765
|
-
request = self.pb.camera.StreamCameraRequest(
|
|
766
|
-
id=self.pb.common.EntityId(id=camera_id),
|
|
767
|
-
target_fps=target_fps,
|
|
768
|
-
width=width,
|
|
769
|
-
height=height,
|
|
770
|
-
format=format,
|
|
771
|
-
)
|
|
772
|
-
elif camera_name is not None:
|
|
773
|
-
request = self.pb.camera.StreamCameraRequest(
|
|
774
|
-
name=camera_name,
|
|
775
|
-
target_fps=target_fps,
|
|
776
|
-
width=width,
|
|
777
|
-
height=height,
|
|
778
|
-
format=format,
|
|
779
|
-
)
|
|
780
|
-
else:
|
|
781
|
-
raise ValueError("Either camera_id or camera_name must be provided")
|
|
782
|
-
|
|
783
|
-
return self.camera.StreamCamera(request)
|
|
784
|
-
|
|
785
|
-
def benchmark(
|
|
786
|
-
self,
|
|
787
|
-
duration_seconds: float = 5.0,
|
|
788
|
-
method: str = "get_observation",
|
|
789
|
-
print_results: bool = True,
|
|
790
|
-
) -> "BenchmarkResult":
|
|
791
|
-
"""
|
|
792
|
-
Benchmark gRPC performance.
|
|
793
|
-
|
|
794
|
-
Measures actual FPS and latency for observation calls.
|
|
795
|
-
|
|
796
|
-
Args:
|
|
797
|
-
duration_seconds: How long to run the benchmark.
|
|
798
|
-
method: Which method to benchmark ("get_observation" or "stream_agent").
|
|
799
|
-
print_results: Whether to print results to console.
|
|
800
|
-
|
|
801
|
-
Returns:
|
|
802
|
-
BenchmarkResult with FPS and latency statistics.
|
|
803
|
-
"""
|
|
804
|
-
if method not in ("get_observation", "stream_agent"):
|
|
805
|
-
raise ValueError(f"Unknown method: {method}. Use 'get_observation' or 'stream_agent'.")
|
|
806
|
-
|
|
807
|
-
latencies: list[float] = []
|
|
808
|
-
frame_times: list[float] = []
|
|
809
|
-
frame_count = 0
|
|
810
|
-
start_time = time.perf_counter()
|
|
811
|
-
last_frame_time = start_time
|
|
812
|
-
|
|
813
|
-
if method == "get_observation":
|
|
814
|
-
while time.perf_counter() - start_time < duration_seconds:
|
|
815
|
-
call_start = time.perf_counter()
|
|
816
|
-
self.get_observation()
|
|
817
|
-
call_end = time.perf_counter()
|
|
818
|
-
|
|
819
|
-
latencies.append((call_end - call_start) * 1000) # ms
|
|
820
|
-
frame_times.append(call_end - last_frame_time)
|
|
821
|
-
last_frame_time = call_end
|
|
822
|
-
frame_count += 1
|
|
823
|
-
|
|
824
|
-
elif method == "stream_agent":
|
|
825
|
-
stream = self.stream_agent(target_fps=1000) # Request max FPS
|
|
826
|
-
for frame in stream:
|
|
827
|
-
now = time.perf_counter()
|
|
828
|
-
if now - start_time >= duration_seconds:
|
|
829
|
-
break
|
|
830
|
-
|
|
831
|
-
frame_times.append(now - last_frame_time)
|
|
832
|
-
last_frame_time = now
|
|
833
|
-
frame_count += 1
|
|
834
|
-
|
|
835
|
-
total_time = time.perf_counter() - start_time
|
|
836
|
-
|
|
837
|
-
# Calculate statistics
|
|
838
|
-
actual_fps = frame_count / total_time if total_time > 0 else 0
|
|
839
|
-
|
|
840
|
-
if len(frame_times) > 1:
|
|
841
|
-
# Remove first frame (warmup)
|
|
842
|
-
frame_times = frame_times[1:]
|
|
843
|
-
fps_from_frames = 1.0 / statistics.mean(frame_times) if frame_times else 0
|
|
844
|
-
else:
|
|
845
|
-
fps_from_frames = actual_fps
|
|
846
|
-
|
|
847
|
-
result = BenchmarkResult(
|
|
848
|
-
method=method,
|
|
849
|
-
duration_seconds=total_time,
|
|
850
|
-
frame_count=frame_count,
|
|
851
|
-
actual_fps=actual_fps,
|
|
852
|
-
avg_latency_ms=statistics.mean(latencies) if latencies else 0,
|
|
853
|
-
min_latency_ms=min(latencies) if latencies else 0,
|
|
854
|
-
max_latency_ms=max(latencies) if latencies else 0,
|
|
855
|
-
std_latency_ms=statistics.stdev(latencies) if len(latencies) > 1 else 0,
|
|
856
|
-
p50_latency_ms=statistics.median(latencies) if latencies else 0,
|
|
857
|
-
p99_latency_ms=sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0,
|
|
858
|
-
)
|
|
859
|
-
|
|
860
|
-
if print_results:
|
|
861
|
-
print(f"\n{'=' * 50}")
|
|
862
|
-
print(f"Benchmark Results ({method})")
|
|
863
|
-
print(f"{'=' * 50}")
|
|
864
|
-
print(f"Duration: {result.duration_seconds:.2f}s")
|
|
865
|
-
print(f"Frames: {result.frame_count}")
|
|
866
|
-
print(f"Actual FPS: {result.actual_fps:.1f}")
|
|
867
|
-
if latencies:
|
|
868
|
-
print(f"{'─' * 50}")
|
|
869
|
-
print(f"Latency (ms):")
|
|
870
|
-
print(f" avg: {result.avg_latency_ms:.2f}")
|
|
871
|
-
print(f" min: {result.min_latency_ms:.2f}")
|
|
872
|
-
print(f" max: {result.max_latency_ms:.2f}")
|
|
873
|
-
print(f" std: {result.std_latency_ms:.2f}")
|
|
874
|
-
print(f" p50: {result.p50_latency_ms:.2f}")
|
|
875
|
-
print(f" p99: {result.p99_latency_ms:.2f}")
|
|
876
|
-
print(f"{'=' * 50}\n")
|
|
877
|
-
|
|
878
|
-
return result
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
@dataclass
|
|
882
|
-
class BenchmarkResult:
|
|
883
|
-
"""Results from a benchmark run."""
|
|
884
|
-
|
|
885
|
-
method: str
|
|
886
|
-
duration_seconds: float
|
|
887
|
-
frame_count: int
|
|
888
|
-
actual_fps: float
|
|
889
|
-
avg_latency_ms: float
|
|
890
|
-
min_latency_ms: float
|
|
891
|
-
max_latency_ms: float
|
|
892
|
-
std_latency_ms: float
|
|
893
|
-
p50_latency_ms: float
|
|
894
|
-
p99_latency_ms: float
|