luckyrobots 0.1.72__tar.gz → 0.1.74__tar.gz

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