luckyrobots 0.1.66__py3-none-any.whl → 0.1.72__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 +30 -12
- luckyrobots/client.py +997 -0
- luckyrobots/config/robots.yaml +231 -71
- luckyrobots/engine/__init__.py +23 -0
- luckyrobots/{utils → engine}/check_updates.py +108 -48
- luckyrobots/{utils → engine}/download.py +61 -39
- luckyrobots/engine/manager.py +427 -0
- luckyrobots/grpc/__init__.py +6 -0
- luckyrobots/grpc/generated/__init__.py +18 -0
- luckyrobots/grpc/generated/agent_pb2.py +69 -0
- luckyrobots/grpc/generated/agent_pb2_grpc.py +283 -0
- luckyrobots/grpc/generated/camera_pb2.py +47 -0
- luckyrobots/grpc/generated/camera_pb2_grpc.py +144 -0
- luckyrobots/grpc/generated/common_pb2.py +43 -0
- luckyrobots/grpc/generated/common_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2.py +43 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/media_pb2.py +39 -0
- luckyrobots/grpc/generated/media_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/mujoco_pb2.py +51 -0
- luckyrobots/grpc/generated/mujoco_pb2_grpc.py +230 -0
- luckyrobots/grpc/generated/scene_pb2.py +66 -0
- luckyrobots/grpc/generated/scene_pb2_grpc.py +317 -0
- luckyrobots/grpc/generated/telemetry_pb2.py +47 -0
- luckyrobots/grpc/generated/telemetry_pb2_grpc.py +143 -0
- luckyrobots/grpc/generated/viewport_pb2.py +50 -0
- luckyrobots/grpc/generated/viewport_pb2_grpc.py +144 -0
- luckyrobots/grpc/proto/agent.proto +213 -0
- luckyrobots/grpc/proto/camera.proto +41 -0
- luckyrobots/grpc/proto/common.proto +36 -0
- luckyrobots/grpc/proto/hazel_rpc.proto +32 -0
- luckyrobots/grpc/proto/media.proto +26 -0
- luckyrobots/grpc/proto/mujoco.proto +64 -0
- luckyrobots/grpc/proto/scene.proto +104 -0
- luckyrobots/grpc/proto/telemetry.proto +43 -0
- luckyrobots/grpc/proto/viewport.proto +45 -0
- luckyrobots/luckyrobots.py +252 -0
- luckyrobots/models/__init__.py +15 -0
- luckyrobots/models/camera.py +97 -0
- luckyrobots/models/observation.py +135 -0
- luckyrobots/models/randomization.py +77 -0
- luckyrobots/{utils/helpers.py → utils.py} +75 -40
- luckyrobots-0.1.72.dist-info/METADATA +262 -0
- luckyrobots-0.1.72.dist-info/RECORD +47 -0
- {luckyrobots-0.1.66.dist-info → luckyrobots-0.1.72.dist-info}/WHEEL +1 -1
- luckyrobots/core/luckyrobots.py +0 -624
- luckyrobots/core/manager.py +0 -236
- luckyrobots/core/models.py +0 -68
- luckyrobots/core/node.py +0 -273
- luckyrobots/message/__init__.py +0 -18
- luckyrobots/message/pubsub.py +0 -145
- luckyrobots/message/srv/client.py +0 -81
- luckyrobots/message/srv/service.py +0 -135
- luckyrobots/message/srv/types.py +0 -83
- luckyrobots/message/transporter.py +0 -427
- luckyrobots/utils/event_loop.py +0 -94
- luckyrobots/utils/sim_manager.py +0 -406
- luckyrobots-0.1.66.dist-info/METADATA +0 -253
- luckyrobots-0.1.66.dist-info/RECORD +0 -24
- {luckyrobots-0.1.66.dist-info → luckyrobots-0.1.72.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RL observation models for LuckyRobots.
|
|
3
|
+
|
|
4
|
+
These are the primary return types for the simplified API:
|
|
5
|
+
- ObservationResponse: returned by get_observation()
|
|
6
|
+
- StateSnapshot: returned by get_state()
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ObservationResponse(BaseModel):
|
|
14
|
+
"""RL observation data from an agent.
|
|
15
|
+
|
|
16
|
+
This is the return type for LuckyEngineClient.get_observation() and
|
|
17
|
+
LuckyRobots.get_observation(). It contains the RL observation vector
|
|
18
|
+
with optional named access for debugging.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
obs = client.get_observation()
|
|
22
|
+
|
|
23
|
+
# Flat vector for RL training
|
|
24
|
+
obs.observation # [0.1, 0.2, 0.3, ...]
|
|
25
|
+
|
|
26
|
+
# Named access (if schema was fetched)
|
|
27
|
+
obs["proj_grav_x"] # 0.1
|
|
28
|
+
obs.to_dict() # {"proj_grav_x": 0.1, "proj_grav_y": 0.2, ...}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(frozen=True)
|
|
32
|
+
|
|
33
|
+
observation: List[float] = Field(
|
|
34
|
+
description="Flat observation vector from the agent's observation spec"
|
|
35
|
+
)
|
|
36
|
+
actions: List[float] = Field(description="Last applied actions")
|
|
37
|
+
timestamp_ms: int = Field(description="Wall-clock timestamp in milliseconds")
|
|
38
|
+
frame_number: int = Field(description="Monotonic frame counter")
|
|
39
|
+
agent_name: str = Field(description="Agent identifier")
|
|
40
|
+
|
|
41
|
+
# Optional named access (populated if schema is available)
|
|
42
|
+
observation_names: Optional[List[str]] = Field(
|
|
43
|
+
default=None,
|
|
44
|
+
description="Observation names from agent schema (enables named access)",
|
|
45
|
+
)
|
|
46
|
+
action_names: Optional[List[str]] = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
description="Action names from agent schema",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __getitem__(self, key: str) -> float:
|
|
52
|
+
"""Access observation value by name.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key: Observation name (e.g., "proj_grav_x", "joint_pos_0").
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The observation value.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
KeyError: If names not available or key not found.
|
|
62
|
+
"""
|
|
63
|
+
if self.observation_names is None:
|
|
64
|
+
raise KeyError(
|
|
65
|
+
f"No observation names available. "
|
|
66
|
+
f"Ensure client has fetched schema via get_agent_schema()."
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
idx = self.observation_names.index(key)
|
|
70
|
+
return self.observation[idx]
|
|
71
|
+
except ValueError:
|
|
72
|
+
raise KeyError(
|
|
73
|
+
f"Unknown observation name: '{key}'. "
|
|
74
|
+
f"Available: {self.observation_names}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def get(self, key: str, default: Optional[float] = None) -> Optional[float]:
|
|
78
|
+
"""Get observation value by name with optional default.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
key: Observation name.
|
|
82
|
+
default: Value to return if key not found.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The observation value or default.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
return self[key]
|
|
89
|
+
except KeyError:
|
|
90
|
+
return default
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> Dict[str, float]:
|
|
93
|
+
"""Convert observations to a name->value dictionary.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict mapping observation names to values. If names not available,
|
|
97
|
+
uses "obs_0", "obs_1", etc.
|
|
98
|
+
"""
|
|
99
|
+
if self.observation_names is not None:
|
|
100
|
+
return dict(zip(self.observation_names, self.observation))
|
|
101
|
+
return {f"obs_{i}": v for i, v in enumerate(self.observation)}
|
|
102
|
+
|
|
103
|
+
def actions_to_dict(self) -> Dict[str, float]:
|
|
104
|
+
"""Convert actions to a name->value dictionary.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dict mapping action names to values. If names not available,
|
|
108
|
+
uses "action_0", "action_1", etc.
|
|
109
|
+
"""
|
|
110
|
+
if self.action_names is not None:
|
|
111
|
+
return dict(zip(self.action_names, self.actions))
|
|
112
|
+
return {f"action_{i}": v for i, v in enumerate(self.actions)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class StateSnapshot(BaseModel):
|
|
116
|
+
"""Bundled snapshot of multiple data sources.
|
|
117
|
+
|
|
118
|
+
Use LuckyEngineClient.get_state() to get a bundled snapshot when you need
|
|
119
|
+
multiple data types in one efficient call. For streaming data like telemetry,
|
|
120
|
+
use the dedicated streaming methods instead.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
|
124
|
+
|
|
125
|
+
observation: Optional[ObservationResponse] = Field(
|
|
126
|
+
default=None, description="RL observation data (if include_observation=True)"
|
|
127
|
+
)
|
|
128
|
+
joint_state: Optional[Any] = Field(
|
|
129
|
+
default=None, description="Joint positions/velocities (if include_joint_state=True)"
|
|
130
|
+
)
|
|
131
|
+
camera_frames: Optional[List[Any]] = Field(
|
|
132
|
+
default=None, description="Camera frames (if camera_names provided)"
|
|
133
|
+
)
|
|
134
|
+
timestamp_ms: int = Field(default=0, description="Wall-clock timestamp in milliseconds")
|
|
135
|
+
frame_number: int = Field(default=0, description="Monotonic frame counter")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Domain randomization configuration for physics parameters.
|
|
3
|
+
|
|
4
|
+
This model maps to the DomainRandomizationConfig proto message in agent.proto.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DomainRandomizationConfig(BaseModel):
|
|
12
|
+
"""Domain randomization configuration for physics parameters.
|
|
13
|
+
|
|
14
|
+
All range fields are [min, max] tuples. None/empty means "use defaults".
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
randomization_cfg = DomainRandomizationConfig(
|
|
18
|
+
friction_range=(0.5, 1.5),
|
|
19
|
+
mass_scale_range=(0.8, 1.2),
|
|
20
|
+
joint_position_noise=0.05,
|
|
21
|
+
)
|
|
22
|
+
client.reset_agent(randomization_cfg=randomization_cfg)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(frozen=True)
|
|
26
|
+
|
|
27
|
+
# Initial state randomization
|
|
28
|
+
pose_position_noise: Optional[tuple[float, float, float]] = Field(
|
|
29
|
+
default=None, description="[x, y, z] position noise std"
|
|
30
|
+
)
|
|
31
|
+
pose_orientation_noise: Optional[float] = Field(
|
|
32
|
+
default=None, description="Orientation noise std (radians)"
|
|
33
|
+
)
|
|
34
|
+
joint_position_noise: Optional[float] = Field(
|
|
35
|
+
default=None, description="Joint position noise std"
|
|
36
|
+
)
|
|
37
|
+
joint_velocity_noise: Optional[float] = Field(
|
|
38
|
+
default=None, description="Joint velocity noise std"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Physics parameters (all [min, max] ranges)
|
|
42
|
+
friction_range: Optional[tuple[float, float]] = Field(
|
|
43
|
+
default=None, description="Surface friction coefficient [min, max]"
|
|
44
|
+
)
|
|
45
|
+
restitution_range: Optional[tuple[float, float]] = Field(
|
|
46
|
+
default=None, description="Bounce/restitution coefficient [min, max]"
|
|
47
|
+
)
|
|
48
|
+
mass_scale_range: Optional[tuple[float, float]] = Field(
|
|
49
|
+
default=None, description="Body mass multiplier [min, max]"
|
|
50
|
+
)
|
|
51
|
+
com_offset_range: Optional[tuple[float, float]] = Field(
|
|
52
|
+
default=None, description="Center of mass offset [min, max]"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Motor/actuator randomization
|
|
56
|
+
motor_strength_range: Optional[tuple[float, float]] = Field(
|
|
57
|
+
default=None, description="Motor strength multiplier [min, max]"
|
|
58
|
+
)
|
|
59
|
+
motor_offset_range: Optional[tuple[float, float]] = Field(
|
|
60
|
+
default=None, description="Motor position offset [min, max]"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# External disturbances
|
|
64
|
+
push_interval_range: Optional[tuple[float, float]] = Field(
|
|
65
|
+
default=None, description="Time between pushes [min, max]"
|
|
66
|
+
)
|
|
67
|
+
push_velocity_range: Optional[tuple[float, float]] = Field(
|
|
68
|
+
default=None, description="Push velocity magnitude [min, max]"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Terrain configuration
|
|
72
|
+
terrain_type: Optional[str] = Field(
|
|
73
|
+
default=None, description="Terrain type identifier"
|
|
74
|
+
)
|
|
75
|
+
terrain_difficulty: Optional[float] = Field(
|
|
76
|
+
default=None, description="Terrain difficulty level"
|
|
77
|
+
)
|
|
@@ -1,18 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Utility functions and classes for LuckyRobots.
|
|
3
|
+
"""
|
|
4
|
+
|
|
2
5
|
import time
|
|
6
|
+
import yaml
|
|
3
7
|
import importlib.resources
|
|
4
8
|
from collections import deque
|
|
5
9
|
|
|
6
10
|
|
|
11
|
+
class FPS:
|
|
12
|
+
"""Utility for measuring frames per second with a rolling window.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
fps = FPS(frame_window=30)
|
|
16
|
+
while running:
|
|
17
|
+
# ... do work ...
|
|
18
|
+
current_fps = fps.measure()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, frame_window: int = 30):
|
|
22
|
+
"""Initialize FPS counter.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
frame_window: Number of frames to average over.
|
|
26
|
+
"""
|
|
27
|
+
self.frame_window = frame_window
|
|
28
|
+
self.frame_times: deque[float] = deque(maxlen=frame_window)
|
|
29
|
+
self.last_frame_time = time.perf_counter()
|
|
30
|
+
|
|
31
|
+
def measure(self) -> float:
|
|
32
|
+
"""Record a frame and return current FPS.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Current frames per second (averaged over window).
|
|
36
|
+
"""
|
|
37
|
+
current_time = time.perf_counter()
|
|
38
|
+
frame_delta = current_time - self.last_frame_time
|
|
39
|
+
self.last_frame_time = current_time
|
|
40
|
+
|
|
41
|
+
self.frame_times.append(frame_delta)
|
|
42
|
+
|
|
43
|
+
if len(self.frame_times) >= 2:
|
|
44
|
+
avg_frame_time = sum(self.frame_times) / len(self.frame_times)
|
|
45
|
+
return 1.0 / avg_frame_time if avg_frame_time > 0 else 0.0
|
|
46
|
+
return 0.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_robot_config(robot: str = None) -> dict:
|
|
50
|
+
"""Get the configuration for a robot from robots.yaml.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
robot: Robot name. If None, returns entire config.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Robot configuration dict, or full config if robot is None.
|
|
57
|
+
"""
|
|
58
|
+
with importlib.resources.files("luckyrobots").joinpath("config/robots.yaml").open(
|
|
59
|
+
"r"
|
|
60
|
+
) as f:
|
|
61
|
+
config = yaml.safe_load(f)
|
|
62
|
+
if robot is not None:
|
|
63
|
+
return config[robot]
|
|
64
|
+
else:
|
|
65
|
+
return config
|
|
66
|
+
|
|
67
|
+
|
|
7
68
|
def validate_params(
|
|
8
69
|
scene: str = None,
|
|
9
70
|
robot: str = None,
|
|
10
71
|
task: str = None,
|
|
11
72
|
observation_type: str = None,
|
|
12
|
-
) ->
|
|
13
|
-
"""Validate
|
|
14
|
-
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Validate parameters for launching LuckyEngine.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
scene: Scene name.
|
|
78
|
+
robot: Robot name.
|
|
79
|
+
task: Task name.
|
|
80
|
+
observation_type: Observation type.
|
|
15
81
|
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If any parameter is invalid.
|
|
84
|
+
"""
|
|
16
85
|
if scene is None:
|
|
17
86
|
raise ValueError("Scene is required")
|
|
18
87
|
if robot is None:
|
|
@@ -22,6 +91,8 @@ def validate_params(
|
|
|
22
91
|
if observation_type is None:
|
|
23
92
|
raise ValueError("Observation type is required")
|
|
24
93
|
|
|
94
|
+
robot_config = get_robot_config(robot)
|
|
95
|
+
|
|
25
96
|
if scene not in robot_config["available_scenes"]:
|
|
26
97
|
raise ValueError(f"Scene {scene} not available in {robot} config")
|
|
27
98
|
if task not in robot_config["available_tasks"]:
|
|
@@ -30,39 +101,3 @@ def validate_params(
|
|
|
30
101
|
raise ValueError(
|
|
31
102
|
f"Observation type {observation_type} not available in {robot} config"
|
|
32
103
|
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_robot_config(robot: str = None) -> dict:
|
|
36
|
-
"""Get the configuration for the robot"""
|
|
37
|
-
with importlib.resources.files("luckyrobots").joinpath("config/robots.yaml").open(
|
|
38
|
-
"r"
|
|
39
|
-
) as f:
|
|
40
|
-
config = yaml.safe_load(f)
|
|
41
|
-
if robot is not None:
|
|
42
|
-
return config[robot]
|
|
43
|
-
else:
|
|
44
|
-
return config
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class FPS:
|
|
48
|
-
def __init__(self, frame_window: int = 30):
|
|
49
|
-
self.frame_window = frame_window
|
|
50
|
-
self.frame_times = deque(maxlen=frame_window)
|
|
51
|
-
self.last_frame_time = time.perf_counter()
|
|
52
|
-
|
|
53
|
-
def measure(self) -> float:
|
|
54
|
-
current_time = time.perf_counter()
|
|
55
|
-
frame_delta = current_time - self.last_frame_time
|
|
56
|
-
self.last_frame_time = current_time
|
|
57
|
-
|
|
58
|
-
# Add frame time to rolling window
|
|
59
|
-
self.frame_times.append(frame_delta)
|
|
60
|
-
|
|
61
|
-
# Calculate FPS from average frame time
|
|
62
|
-
if len(self.frame_times) >= 2: # Need at least 2 samples
|
|
63
|
-
avg_frame_time = sum(self.frame_times) / len(self.frame_times)
|
|
64
|
-
fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0
|
|
65
|
-
else:
|
|
66
|
-
fps = 0
|
|
67
|
-
|
|
68
|
-
print(f"FPS: {fps}")
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luckyrobots
|
|
3
|
+
Version: 0.1.72
|
|
4
|
+
Summary: Robotics-AI Training in Hyperrealistic Game Environments
|
|
5
|
+
Project-URL: Homepage, https://github.com/luckyrobots/luckyrobots
|
|
6
|
+
Project-URL: Documentation, https://luckyrobots.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/luckyrobots/luckyrobots
|
|
8
|
+
Project-URL: Issues, https://github.com/luckyrobots/luckyrobots/issues
|
|
9
|
+
Author-email: Devrim Yasar <braces.verbose03@icloud.com>, Ethan Clark <ethan@luckyrobots.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
20
|
+
Requires-Dist: grpcio>=1.60.0
|
|
21
|
+
Requires-Dist: numpy>=1.24.0
|
|
22
|
+
Requires-Dist: opencv-python>=4.8.0
|
|
23
|
+
Requires-Dist: packaging>=23.0
|
|
24
|
+
Requires-Dist: protobuf>=4.25.0
|
|
25
|
+
Requires-Dist: psutil>=5.9.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: requests>=2.31.0
|
|
29
|
+
Requires-Dist: tqdm>=4.66.0
|
|
30
|
+
Requires-Dist: watchdog>=3.0.0
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img width="384" alt="Default_Logo_Horizontal@2x" src="https://github.com/user-attachments/assets/ae6ad53a-741e-4e7a-94cb-5a46a8e81398" />
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
Infinite synthetic data generation for embodied AI
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<div align="center">
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/luckyrobots/)
|
|
44
|
+
[](https://luckyrobots.readthedocs.io)
|
|
45
|
+
[](https://opensource.org/licenses/MIT)
|
|
46
|
+
[](https://pypi.org/project/luckyrobots/)
|
|
47
|
+
[](https://pypi.org/project/luckyrobots/)
|
|
48
|
+
[](https://discord.gg/5CH3wx3tAs)
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
https://github.com/user-attachments/assets/0ab2953d-b188-4af7-a225-71decdd2378c
|
|
53
|
+
|
|
54
|
+
# Lucky Robots
|
|
55
|
+
|
|
56
|
+
Hyperrealistic robotics simulation framework with Python API for embodied AI training and testing.
|
|
57
|
+
|
|
58
|
+
<p align="center">
|
|
59
|
+
<img width="49%" alt="Bedroom environment in LuckyEngine" src="https://github.com/user-attachments/assets/279a7864-9a8b-453e-8567-3a174f5db8ab" />
|
|
60
|
+
<img width="49%" alt="Open floor plan in LuckyEngine" src="https://github.com/user-attachments/assets/68c72b97-98ab-42b0-a065-8a4247b014c7" />
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
1. **Download LuckyEngine** from our [releases page](https://github.com/luckyrobots/luckyrobots/releases/latest) and set the path:
|
|
66
|
+
```bash
|
|
67
|
+
# Set environment variable (choose one method):
|
|
68
|
+
|
|
69
|
+
# Method 1: Set LUCKYENGINE_PATH directly to the executable
|
|
70
|
+
export LUCKYENGINE_PATH=/path/to/LuckyEngine # Linux/Mac
|
|
71
|
+
export LUCKYENGINE_PATH=/path/to/LuckyEngine.exe # Windows
|
|
72
|
+
|
|
73
|
+
# Method 2: Set LUCKYENGINE_HOME to the directory containing the executable
|
|
74
|
+
export LUCKYENGINE_HOME=/path/to/luckyengine/directory
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. **Install**
|
|
78
|
+
```bash
|
|
79
|
+
pip install luckyrobots
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. **Run Example**
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/luckyrobots/luckyrobots.git
|
|
85
|
+
cd luckyrobots/examples
|
|
86
|
+
python controller.py --skip-launch # If LuckyEngine is already running
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Basic Usage
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from luckyrobots import LuckyEngineClient
|
|
93
|
+
|
|
94
|
+
# Connect to LuckyEngine
|
|
95
|
+
client = LuckyEngineClient(
|
|
96
|
+
host="127.0.0.1",
|
|
97
|
+
port=50051,
|
|
98
|
+
robot_name="unitreego1",
|
|
99
|
+
)
|
|
100
|
+
client.wait_for_server()
|
|
101
|
+
|
|
102
|
+
# Optional: Fetch schema for named observation access
|
|
103
|
+
client.fetch_schema()
|
|
104
|
+
|
|
105
|
+
# Get RL observation
|
|
106
|
+
obs = client.get_observation()
|
|
107
|
+
print(f"Observation: {obs.observation[:5]}...") # Flat vector for RL
|
|
108
|
+
print(f"Timestamp: {obs.timestamp_ms}")
|
|
109
|
+
|
|
110
|
+
# Named access (if schema fetched)
|
|
111
|
+
# obs["proj_grav_x"] # Access by name
|
|
112
|
+
# obs.to_dict() # Convert to dict
|
|
113
|
+
|
|
114
|
+
# Send controls
|
|
115
|
+
client.send_control(controls=[0.1, 0.2, -0.1, ...])
|
|
116
|
+
|
|
117
|
+
# Get joint state (separate from RL observation)
|
|
118
|
+
joints = client.get_joint_state()
|
|
119
|
+
print(f"Positions: {joints.positions}")
|
|
120
|
+
print(f"Velocities: {joints.velocities}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API Overview
|
|
124
|
+
|
|
125
|
+
### Core Classes
|
|
126
|
+
|
|
127
|
+
**`LuckyEngineClient`** - Low-level gRPC client
|
|
128
|
+
- `wait_for_server(timeout)` - Wait for LuckyEngine connection
|
|
129
|
+
- `get_observation()` - Get RL observation vector
|
|
130
|
+
- `get_joint_state()` - Get joint positions/velocities
|
|
131
|
+
- `send_control(controls)` - Send actuator commands
|
|
132
|
+
- `get_agent_schema()` - Get observation/action names and sizes
|
|
133
|
+
- `reset_agent()` - Reset agent state
|
|
134
|
+
|
|
135
|
+
**`LuckyRobots`** - High-level wrapper (launches LuckyEngine)
|
|
136
|
+
- `start(scene, robot, task)` - Launch and connect
|
|
137
|
+
- `get_observation()` - Get observation
|
|
138
|
+
- `step(controls)` - Send controls and get next observation
|
|
139
|
+
|
|
140
|
+
### Models
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from luckyrobots import ObservationResponse, StateSnapshot
|
|
144
|
+
|
|
145
|
+
# ObservationResponse - returned by get_observation()
|
|
146
|
+
obs.observation # List[float] - flat RL observation vector
|
|
147
|
+
obs.actions # List[float] - last applied actions
|
|
148
|
+
obs.timestamp_ms # int - wall-clock timestamp
|
|
149
|
+
obs.frame_number # int - monotonic counter
|
|
150
|
+
obs["name"] # Named access (if schema fetched)
|
|
151
|
+
obs.to_dict() # Convert to name->value dict
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Available Robots & Environments
|
|
155
|
+
|
|
156
|
+
### Robots
|
|
157
|
+
- **unitreego1**: Quadruped robot
|
|
158
|
+
- **so100**: 6-DOF manipulator with gripper
|
|
159
|
+
- **stretch_v1**: Mobile manipulator
|
|
160
|
+
|
|
161
|
+
### Scenes
|
|
162
|
+
- **velocity**: Velocity control training
|
|
163
|
+
- **kitchen**: Residential kitchen environment
|
|
164
|
+
|
|
165
|
+
### Tasks
|
|
166
|
+
- **locomotion**: Walking/movement
|
|
167
|
+
- **pickandplace**: Object manipulation
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
### Setup with uv (recommended)
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Clone and enter repo
|
|
175
|
+
git clone https://github.com/luckyrobots/luckyrobots.git
|
|
176
|
+
cd luckyrobots
|
|
177
|
+
|
|
178
|
+
# Install uv if you haven't
|
|
179
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
180
|
+
|
|
181
|
+
# Create venv and install deps
|
|
182
|
+
uv sync
|
|
183
|
+
|
|
184
|
+
# Run tests
|
|
185
|
+
uv run pytest
|
|
186
|
+
|
|
187
|
+
# Run example
|
|
188
|
+
uv run python examples/controller.py --skip-launch
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Setup with pip
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git clone https://github.com/luckyrobots/luckyrobots.git
|
|
195
|
+
cd luckyrobots
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Regenerating gRPC Stubs
|
|
200
|
+
|
|
201
|
+
The Python gRPC stubs are in `src/luckyrobots/grpc/generated/` and are
|
|
202
|
+
generated from protos in `src/luckyrobots/grpc/proto/`.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python -m grpc_tools.protoc \
|
|
206
|
+
-I "src/luckyrobots/grpc/proto" \
|
|
207
|
+
--python_out="src/luckyrobots/grpc/generated" \
|
|
208
|
+
--grpc_python_out="src/luckyrobots/grpc/generated" \
|
|
209
|
+
src/luckyrobots/grpc/proto/*.proto
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Project Structure
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
src/luckyrobots/
|
|
216
|
+
├── client.py # LuckyEngineClient (main API)
|
|
217
|
+
├── luckyrobots.py # LuckyRobots high-level wrapper
|
|
218
|
+
├── models/ # Pydantic models
|
|
219
|
+
│ ├── observation.py # ObservationResponse, StateSnapshot
|
|
220
|
+
│ └── camera.py # CameraData, CameraShape
|
|
221
|
+
├── engine/ # Engine management
|
|
222
|
+
├── grpc/ # gRPC internals
|
|
223
|
+
│ ├── generated/ # Protobuf stubs
|
|
224
|
+
│ └── proto/ # .proto files
|
|
225
|
+
└── config/ # Robot configurations
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Contributing
|
|
229
|
+
|
|
230
|
+
1. Fork the repository
|
|
231
|
+
2. Create a feature branch
|
|
232
|
+
3. Make changes and add tests
|
|
233
|
+
4. Run `uv run ruff check .` and `uv run ruff format .`
|
|
234
|
+
5. Submit a pull request
|
|
235
|
+
|
|
236
|
+
## Architecture
|
|
237
|
+
|
|
238
|
+
Lucky Robots uses gRPC for communication:
|
|
239
|
+
|
|
240
|
+
- **LuckyEngine**: Physics + rendering backend (Unreal Engine + MuJoCo)
|
|
241
|
+
- **Python client**: Connects via gRPC (default `127.0.0.1:50051`)
|
|
242
|
+
|
|
243
|
+
### gRPC Services
|
|
244
|
+
|
|
245
|
+
| Service | Status | Description |
|
|
246
|
+
|---------|--------|-------------|
|
|
247
|
+
| MujocoService | ✅ Working | Joint state, controls |
|
|
248
|
+
| AgentService | ✅ Working | Observations, reset |
|
|
249
|
+
| SceneService | 🚧 Placeholder | Scene inspection |
|
|
250
|
+
| TelemetryService | 🚧 Placeholder | Telemetry streaming |
|
|
251
|
+
| CameraService | 🚧 Placeholder | Camera frames |
|
|
252
|
+
| ViewportService | 🚧 Placeholder | Viewport pixels |
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT License - see [LICENSE](LICENSE) file.
|
|
257
|
+
|
|
258
|
+
## Support
|
|
259
|
+
|
|
260
|
+
- **Issues**: [GitHub Issues](https://github.com/luckyrobots/luckyrobots/issues)
|
|
261
|
+
- **Discussions**: [GitHub Discussions](https://github.com/luckyrobots/luckyrobots/discussions)
|
|
262
|
+
- **Discord**: [Community Server](https://discord.gg/5CH3wx3tAs)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
luckyrobots/__init__.py,sha256=VWewiYhYbYCs1ZSD0KxozmG2jH2uLlBxsVgFIRivKTM,824
|
|
2
|
+
luckyrobots/client.py,sha256=3F0p46FndJv02D_pSZkOn4j2tkjV6RX1ziCFFEaJsr4,36129
|
|
3
|
+
luckyrobots/luckyrobots.py,sha256=-XkIt9h7si4GK_j5Wpknb7_h56CCWi8_d1woVMuIvxU,8957
|
|
4
|
+
luckyrobots/utils.py,sha256=62BqpSRBFykVRMZ5Ti2F-yKRDoyU3UNHeesDF_PVCFc,2958
|
|
5
|
+
luckyrobots/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
luckyrobots/config/robots.yaml,sha256=Wu2AgwhZxo9F-RLcqPJXHnqzjT8a0wR-rFBcecck_eE,5164
|
|
7
|
+
luckyrobots/engine/__init__.py,sha256=-3r7aLueDF7eyumqIhTvYqS8ZNnVSJUJEHG4ybsKrjc,546
|
|
8
|
+
luckyrobots/engine/check_updates.py,sha256=KGQHrj1LB0_vexktO01xx-ZvPOYKkto4VBb3H1KV7uI,7593
|
|
9
|
+
luckyrobots/engine/download.py,sha256=vIb5T40Jk2BnfjPo0f4KgW14Om6P4_mvPpZ9JAlkVvg,4029
|
|
10
|
+
luckyrobots/engine/manager.py,sha256=8vxSevaoXo-lw2G6y74lVc-Ms8xIELK0yLswydMiOQY,13388
|
|
11
|
+
luckyrobots/grpc/__init__.py,sha256=hPh-X_FEus2sxrfQB2fQjD754X5bMaUFTWm3mZmUhe0,153
|
|
12
|
+
luckyrobots/grpc/generated/__init__.py,sha256=u69M4E_2LwRQSD_n4wvQyMX7dryBCrIdsho59c52Crk,732
|
|
13
|
+
luckyrobots/grpc/generated/agent_pb2.py,sha256=5rXekXme6u5KCtEtwjkhHkUyxrfxpMKutG4KPZqNr1U,7195
|
|
14
|
+
luckyrobots/grpc/generated/agent_pb2_grpc.py,sha256=M9ZsjS1qhBm57-DVy6il_u_XeTpb6Qbntm45Rqp_f1k,10909
|
|
15
|
+
luckyrobots/grpc/generated/camera_pb2.py,sha256=2UDkgzMYdDZ53euPfLAndeI7k4wI9QH_Dvwkay6TAB4,2623
|
|
16
|
+
luckyrobots/grpc/generated/camera_pb2_grpc.py,sha256=hUXzsf2qCE-ffZzkF8El2UuuLu6nxjKSYPukId2uFP8,5278
|
|
17
|
+
luckyrobots/grpc/generated/common_pb2.py,sha256=0eZTxqlsZdTrgp2hgLy-hdDNtac5U3kkS_D9UbVy0e8,2045
|
|
18
|
+
luckyrobots/grpc/generated/common_pb2_grpc.py,sha256=MWix04c710srn9BtjDxa9-UkZz0wa-w8tB0EmYndSgg,886
|
|
19
|
+
luckyrobots/grpc/generated/hazel_rpc_pb2.py,sha256=XB1nGSVTYhpulRpS8Ja44KaCV5s-l1t_FPV4DJ5_ak8,1716
|
|
20
|
+
luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py,sha256=5bhVh5DmVDl2ZsdgN7mTnElDbgU142TOI1qm8a8eblw,889
|
|
21
|
+
luckyrobots/grpc/generated/media_pb2.py,sha256=TtGdBub2Pj1vG_jJJIZnIZealLD0s98wZ4KgGIrcn2c,1827
|
|
22
|
+
luckyrobots/grpc/generated/media_pb2_grpc.py,sha256=szeOZzqDnIBFEyjAtXEHNxytTb9hTvOkQj0PhvXumMM,885
|
|
23
|
+
luckyrobots/grpc/generated/mujoco_pb2.py,sha256=Oq1AdZ0h-svs7ntlaDZ-zvQdTPsulnaE7YaINvBIBpM,3476
|
|
24
|
+
luckyrobots/grpc/generated/mujoco_pb2_grpc.py,sha256=Rz8hyPvhB2nR-0Mej4Ly9ixbrdpPCxDafmL8H4x43Nk,8603
|
|
25
|
+
luckyrobots/grpc/generated/scene_pb2.py,sha256=ZQyv_csWjB42WLfHNe8aQVMB5U9nQnJ7fY9enac9VGQ,5382
|
|
26
|
+
luckyrobots/grpc/generated/scene_pb2_grpc.py,sha256=I7G2LqkKZwEPZV-gxWo_45ecBey-wuGZ26pwxlH0mGQ,12118
|
|
27
|
+
luckyrobots/grpc/generated/telemetry_pb2.py,sha256=LgVtxJv7ovtqlrgxG375aXADGt3qXwgpsIhIkJ3pLKE,2790
|
|
28
|
+
luckyrobots/grpc/generated/telemetry_pb2_grpc.py,sha256=8TuCSc7rnQZFNqhEPXUoJsWAnwvEvyyEI5GaqQUCSSs,5463
|
|
29
|
+
luckyrobots/grpc/generated/viewport_pb2.py,sha256=Z_5gBD23z7He_OACYkaIsi0o4LIESzOG7o_rO1lRLVs,3093
|
|
30
|
+
luckyrobots/grpc/generated/viewport_pb2_grpc.py,sha256=BkJqREcjIa3ZdNgZUxH_MvBWOte9VJJGi-KlFgYepog,5436
|
|
31
|
+
luckyrobots/grpc/proto/agent.proto,sha256=SUVR9EqNz5gB5FNt3n54sNXv3SkvYgXKywlZmZh5vio,8506
|
|
32
|
+
luckyrobots/grpc/proto/camera.proto,sha256=unLowpk4d0AGJgyKrbzTr1F_CxOV9dXRoXn-gO8eJVw,1097
|
|
33
|
+
luckyrobots/grpc/proto/common.proto,sha256=zQPj-Z8b8mAE2_eHlJ0L2_-JH-CYY_OJcv8046fAtQM,772
|
|
34
|
+
luckyrobots/grpc/proto/hazel_rpc.proto,sha256=gtFukd1B9WRbY2kWBgd5YQWSw4cobYf1YRYX6VEaULE,1229
|
|
35
|
+
luckyrobots/grpc/proto/media.proto,sha256=3Rhmiaw1zceQBFdwxX1ViFgH-cSr7mBjNEGHPaFmTsk,836
|
|
36
|
+
luckyrobots/grpc/proto/mujoco.proto,sha256=b7tBvYqqvcSjmqrEax6_75-P3fIHHZq63A1HVJKjx0U,1873
|
|
37
|
+
luckyrobots/grpc/proto/scene.proto,sha256=ZypWsUzpgplGsQPP0FXJpyY4rSxgWYGsniM4vofhnak,3075
|
|
38
|
+
luckyrobots/grpc/proto/telemetry.proto,sha256=PZiUoA0bpwuvsBUdoQQdBNGWOuhlBMKAiBpwqPZiJgY,1481
|
|
39
|
+
luckyrobots/grpc/proto/viewport.proto,sha256=gunF8cIrVgvoAX8FNER6gw_u-IlRWAR1Q6ts0zUuqr8,1324
|
|
40
|
+
luckyrobots/models/__init__.py,sha256=assqJIKZXvDGwCVM0grNLJCBWyHtKRBAaxdJb2dON10,332
|
|
41
|
+
luckyrobots/models/camera.py,sha256=e-CftAndHASfokhtUtU7ujGb2q_kcXZ6-2KKvY42CKg,3682
|
|
42
|
+
luckyrobots/models/observation.py,sha256=jbRiUTjOP2ZiSobsnNcks3aMPAJA6oPoKqiXwvrNQK0,4758
|
|
43
|
+
luckyrobots/models/randomization.py,sha256=AicqHovzG97Ge33wtNQE-_EvKxndjEHguTjCiHfhAV8,2753
|
|
44
|
+
luckyrobots-0.1.72.dist-info/METADATA,sha256=-liPc6bNv78N5A-PgTJ8YeiD5YIIU8BTO60zE-zV87Y,8379
|
|
45
|
+
luckyrobots-0.1.72.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
46
|
+
luckyrobots-0.1.72.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
|
|
47
|
+
luckyrobots-0.1.72.dist-info/RECORD,,
|