lang2sim 2.0.0__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.
- lang2sim/__init__.py +47 -0
- lang2sim/agent/__init__.py +5 -0
- lang2sim/agent/nemotron_client.py +109 -0
- lang2sim/agent/react_loop.py +848 -0
- lang2sim/agent/tools.py +998 -0
- lang2sim/cli/app.py +183 -0
- lang2sim/cli/screens/__init__.py +8 -0
- lang2sim/cli/screens/command_palette_modal.py +146 -0
- lang2sim/cli/screens/landing_screen.py +96 -0
- lang2sim/cli/screens/sessions_modal.py +236 -0
- lang2sim/cli/screens/workspace_screen.py +709 -0
- lang2sim/cli/widgets/__init__.py +16 -0
- lang2sim/cli/widgets/action_pill.py +67 -0
- lang2sim/cli/widgets/command_card.py +265 -0
- lang2sim/cli/widgets/plan_card.py +106 -0
- lang2sim/cli/widgets/prompt_card.py +96 -0
- lang2sim/cli/widgets/telemetry_sidebar.py +117 -0
- lang2sim/cli/widgets/thought_box.py +64 -0
- lang2sim/compiler/__init__.py +15 -0
- lang2sim/compiler/sdf_compiler.py +65 -0
- lang2sim/compiler/sir_generator.py +46 -0
- lang2sim/compiler/sir_validator.py +87 -0
- lang2sim/core/__init__.py +50 -0
- lang2sim/core/config.py +116 -0
- lang2sim/core/models.py +279 -0
- lang2sim/core/session_manager.py +189 -0
- lang2sim/perturbation/__init__.py +50 -0
- lang2sim/perturbation/_compat.py +19 -0
- lang2sim/perturbation/gazebo_bridge.py +520 -0
- lang2sim/perturbation/mcp_server.py +129 -0
- lang2sim/perturbation/routers/__init__.py +19 -0
- lang2sim/perturbation/routers/world.py +110 -0
- lang2sim/perturbation/sdf_snippets.py +112 -0
- lang2sim/runtime/__init__.py +7 -0
- lang2sim/runtime/control_api/__init__.py +7 -0
- lang2sim/runtime/control_api/client.py +187 -0
- lang2sim/runtime/control_api/server.py +504 -0
- lang2sim/runtime/display.py +141 -0
- lang2sim/runtime/docker_runner.py +286 -0
- lang2sim/runtime/doctor.py +220 -0
- lang2sim/runtime/hardware_probe.py +144 -0
- lang2sim/runtime/health_monitor.py +66 -0
- lang2sim/runtime/launch_builder.py +137 -0
- lang2sim/runtime/nav2_dispatcher.py +215 -0
- lang2sim/runtime/platform.py +137 -0
- lang2sim/runtime/ros_bridge.py +183 -0
- lang2sim/runtime/run_folder.py +86 -0
- lang2sim/runtime/self_healing.py +387 -0
- lang2sim/runtime/session.py +136 -0
- lang2sim/runtime/world_check.py +197 -0
- lang2sim/telemetry/__init__.py +85 -0
- lang2sim/telemetry/nqi_scorer.py +645 -0
- lang2sim/telemetry/ros_subscribers.py +489 -0
- lang2sim/telemetry/trajectory.py +258 -0
- lang2sim/telemetry/vision.py +477 -0
- lang2sim/world_gen/__init__.py +16 -0
- lang2sim/world_gen/catalog_loader.py +83 -0
- lang2sim/world_gen/map_slicer.py +131 -0
- lang2sim/world_gen/shapely_solver.py +256 -0
- lang2sim-2.0.0.dist-info/METADATA +53 -0
- lang2sim-2.0.0.dist-info/RECORD +65 -0
- lang2sim-2.0.0.dist-info/WHEEL +5 -0
- lang2sim-2.0.0.dist-info/entry_points.txt +2 -0
- lang2sim-2.0.0.dist-info/licenses/LICENSE +21 -0
- lang2sim-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zero-SLAM 2D Occupancy Map Slicer.
|
|
3
|
+
File: lang2sim/world_gen/map_slicer.py
|
|
4
|
+
|
|
5
|
+
Converts 3D world geometric bounds and Shapely object polygons directly into
|
|
6
|
+
standard Nav2 P5 Portable Gray Map (map.pgm) and YAML metadata (map.yaml)
|
|
7
|
+
in < 2 seconds, completely eliminating Cartographer / SLAM overhead.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Tuple, Optional
|
|
13
|
+
from shapely.geometry import Polygon, Point
|
|
14
|
+
|
|
15
|
+
from lang2sim.core.models import Vector3D, ObjectSpec
|
|
16
|
+
from lang2sim.core.config import MAP_RESOLUTION
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ZeroSlamMapSlicer:
|
|
20
|
+
"""
|
|
21
|
+
Direct geometric rasterizer converting room and obstacle polygons
|
|
22
|
+
into 2D occupancy grids for Nav2.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, resolution: float = MAP_RESOLUTION):
|
|
26
|
+
self.resolution = resolution # 0.05 m/pixel
|
|
27
|
+
|
|
28
|
+
def slice_and_export(
|
|
29
|
+
self,
|
|
30
|
+
room_dim: Vector3D,
|
|
31
|
+
objects: List[ObjectSpec],
|
|
32
|
+
output_dir: Path,
|
|
33
|
+
map_name: str = "map",
|
|
34
|
+
) -> Tuple[Path, Path]:
|
|
35
|
+
"""
|
|
36
|
+
Rasters the 2D floorplan and writes map.pgm + map.yaml.
|
|
37
|
+
"""
|
|
38
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
pgm_path = output_dir / f"{map_name}.pgm"
|
|
40
|
+
yaml_path = output_dir / f"{map_name}.yaml"
|
|
41
|
+
|
|
42
|
+
width_m = room_dim.x
|
|
43
|
+
height_m = room_dim.y
|
|
44
|
+
|
|
45
|
+
cols = int(math.ceil(width_m / self.resolution))
|
|
46
|
+
rows = int(math.ceil(height_m / self.resolution))
|
|
47
|
+
|
|
48
|
+
origin_x = -(width_m / 2.0)
|
|
49
|
+
origin_y = -(height_m / 2.0)
|
|
50
|
+
|
|
51
|
+
# 254 = Free space (white), 0 = Occupied (black)
|
|
52
|
+
# Using a flat bytearray of size rows * cols
|
|
53
|
+
grid = bytearray([254] * (rows * cols))
|
|
54
|
+
|
|
55
|
+
def set_pixel(c: int, r: int, val: int):
|
|
56
|
+
if 0 <= c < cols and 0 <= r < rows:
|
|
57
|
+
grid[r * cols + c] = val
|
|
58
|
+
|
|
59
|
+
# 1. Draw outer walls (wall thickness = 0.2m -> 4 pixels)
|
|
60
|
+
wall_px = max(2, int(0.20 / self.resolution))
|
|
61
|
+
for r in range(rows):
|
|
62
|
+
for c in range(cols):
|
|
63
|
+
if (
|
|
64
|
+
r < wall_px
|
|
65
|
+
or r >= rows - wall_px
|
|
66
|
+
or c < wall_px
|
|
67
|
+
or c >= cols - wall_px
|
|
68
|
+
):
|
|
69
|
+
set_pixel(c, r, 0)
|
|
70
|
+
|
|
71
|
+
# 2. Rasterize each obstacle
|
|
72
|
+
for obj in objects:
|
|
73
|
+
cx = obj.pose.x
|
|
74
|
+
cy = obj.pose.y
|
|
75
|
+
dx = obj.dimensions.x
|
|
76
|
+
dy = obj.dimensions.y
|
|
77
|
+
yaw = obj.pose.yaw
|
|
78
|
+
|
|
79
|
+
# Create Shapely OBB for precise polygon point-in-polygon containment
|
|
80
|
+
from lang2sim.world_gen.shapely_solver import ShapelySpatialSolver
|
|
81
|
+
solver = ShapelySpatialSolver()
|
|
82
|
+
poly = solver.create_obb(cx, cy, dx, dy, yaw)
|
|
83
|
+
min_x, min_y, max_x, max_y = poly.bounds
|
|
84
|
+
|
|
85
|
+
c_min = max(0, int((min_x - origin_x) / self.resolution))
|
|
86
|
+
c_max = min(cols, int(math.ceil((max_x - origin_x) / self.resolution)))
|
|
87
|
+
r_min = max(0, int((min_y - origin_y) / self.resolution))
|
|
88
|
+
r_max = min(rows, int(math.ceil((max_y - origin_y) / self.resolution)))
|
|
89
|
+
|
|
90
|
+
for r in range(r_min, r_max):
|
|
91
|
+
for c in range(c_min, c_max):
|
|
92
|
+
world_x = origin_x + (c + 0.5) * self.resolution
|
|
93
|
+
world_y = origin_y + (r + 0.5) * self.resolution
|
|
94
|
+
if poly.contains(Point(world_x, world_y)):
|
|
95
|
+
set_pixel(c, r, 0)
|
|
96
|
+
|
|
97
|
+
# 3. Write P5 Grayscale PGM file directly
|
|
98
|
+
# Standard P5 format: Header\n<cols> <rows>\n<max_val>\n<raw_bytes>
|
|
99
|
+
header = f"P5\n{cols} {rows}\n255\n".encode("ascii")
|
|
100
|
+
with open(pgm_path, "wb") as f:
|
|
101
|
+
f.write(header)
|
|
102
|
+
# PGM rows run top to bottom and Nav2's map_server puts the first row at
|
|
103
|
+
# the map's max y; grid row 0 is min y (origin), so write rows in reverse.
|
|
104
|
+
for r in range(rows - 1, -1, -1):
|
|
105
|
+
f.write(grid[r * cols:(r + 1) * cols])
|
|
106
|
+
|
|
107
|
+
# 4. Write Nav2 map.yaml metadata
|
|
108
|
+
yaml_content = f"""image: {map_name}.pgm
|
|
109
|
+
mode: trinary
|
|
110
|
+
resolution: {self.resolution}
|
|
111
|
+
origin: [{origin_x:.3f}, {origin_y:.3f}, 0.000]
|
|
112
|
+
negate: 0
|
|
113
|
+
occupied_thresh: 0.65
|
|
114
|
+
free_thresh: 0.25
|
|
115
|
+
"""
|
|
116
|
+
with open(yaml_path, "w", encoding="utf-8") as f:
|
|
117
|
+
f.write(yaml_content)
|
|
118
|
+
|
|
119
|
+
return pgm_path, yaml_path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def slice_map_from_layout(
|
|
123
|
+
room_dim: Vector3D,
|
|
124
|
+
objects: List[ObjectSpec],
|
|
125
|
+
output_dir: Path,
|
|
126
|
+
map_name: str = "map",
|
|
127
|
+
resolution: float = MAP_RESOLUTION,
|
|
128
|
+
) -> Tuple[Path, Path]:
|
|
129
|
+
"""Convenience function to generate zero-SLAM map assets."""
|
|
130
|
+
slicer = ZeroSlamMapSlicer(resolution=resolution)
|
|
131
|
+
return slicer.slice_and_export(room_dim, objects, output_dir, map_name)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shapely 2.0 Spatial Placement & Oriented Bounding Box (OBB) Solver.
|
|
3
|
+
File: lang2sim/world_gen/shapely_solver.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
from typing import List, Optional, Tuple
|
|
8
|
+
from shapely.geometry import Polygon, Point, box
|
|
9
|
+
from shapely import affinity
|
|
10
|
+
|
|
11
|
+
from lang2sim.core.models import (
|
|
12
|
+
Pose2D,
|
|
13
|
+
Vector3D,
|
|
14
|
+
EnvironmentSpec,
|
|
15
|
+
ObjectSpec,
|
|
16
|
+
AisleCorridor,
|
|
17
|
+
)
|
|
18
|
+
from lang2sim.core.config import (
|
|
19
|
+
ROBOT_CIRCUMSCRIBED_RADIUS,
|
|
20
|
+
MIN_AISLE_CORRIDOR_WIDTH,
|
|
21
|
+
MIN_WALL_STANDOFF,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ShapelySpatialSolver:
|
|
26
|
+
"""
|
|
27
|
+
Computational geometry solver ensuring 100% collision-free placement
|
|
28
|
+
and guaranteed Nav2 navigation corridors (> 2.2m).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
min_corridor_width: float = MIN_AISLE_CORRIDOR_WIDTH,
|
|
34
|
+
wall_standoff: float = MIN_WALL_STANDOFF,
|
|
35
|
+
robot_radius: float = ROBOT_CIRCUMSCRIBED_RADIUS,
|
|
36
|
+
):
|
|
37
|
+
self.min_corridor_width = min_corridor_width
|
|
38
|
+
self.wall_standoff = wall_standoff
|
|
39
|
+
self.robot_radius = robot_radius
|
|
40
|
+
|
|
41
|
+
def create_room_polygon(self, room_dim: Vector3D) -> Polygon:
|
|
42
|
+
"""
|
|
43
|
+
Creates outer room boundary centered at world origin (0, 0).
|
|
44
|
+
X in [-W/2, W/2], Y in [-H/2, H/2].
|
|
45
|
+
"""
|
|
46
|
+
half_w = room_dim.x / 2.0
|
|
47
|
+
half_h = room_dim.y / 2.0
|
|
48
|
+
return box(-half_w, -half_h, half_w, half_h)
|
|
49
|
+
|
|
50
|
+
def create_obb(
|
|
51
|
+
self, x: float, y: float, size_x: float, size_y: float, yaw: float = 0.0
|
|
52
|
+
) -> Polygon:
|
|
53
|
+
"""
|
|
54
|
+
Creates an Oriented Bounding Box (OBB) polygon for an obstacle.
|
|
55
|
+
"""
|
|
56
|
+
half_x = size_x / 2.0
|
|
57
|
+
half_y = size_y / 2.0
|
|
58
|
+
unrotated = box(x - half_x, y - half_y, x + half_x, y + half_y)
|
|
59
|
+
if abs(yaw) > 1e-4:
|
|
60
|
+
return affinity.rotate(unrotated, yaw, use_radians=True, origin=(x, y))
|
|
61
|
+
return unrotated
|
|
62
|
+
|
|
63
|
+
def is_valid_placement(
|
|
64
|
+
self,
|
|
65
|
+
candidate_poly: Polygon,
|
|
66
|
+
placed_polygons: List[Polygon],
|
|
67
|
+
room_poly: Polygon,
|
|
68
|
+
reserved_points: Optional[List[Point]] = None,
|
|
69
|
+
enforce_corridor: bool = True,
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Checks if candidate polygon satisfies:
|
|
73
|
+
1. Inside room with minimum wall standoff (>= 0.5m).
|
|
74
|
+
2. Non-intersecting with all existing placed obstacles.
|
|
75
|
+
3. Maintains minimum corridor buffer (>= 2.2m / 2 from boundaries).
|
|
76
|
+
4. Does not collide with reserved points (robot spawn pose, goal target).
|
|
77
|
+
"""
|
|
78
|
+
# 1. Must be completely within room
|
|
79
|
+
if not room_poly.contains(candidate_poly):
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
# 2. Check wall standoff
|
|
83
|
+
if room_poly.boundary.distance(candidate_poly) < self.wall_standoff:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# 3. Check direct collision with existing obstacles
|
|
87
|
+
for p in placed_polygons:
|
|
88
|
+
if candidate_poly.intersects(p):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
# 4. Check reserved points (e.g. initial spawn pose and goal)
|
|
92
|
+
if reserved_points:
|
|
93
|
+
clearance_radius = self.robot_radius + 0.30
|
|
94
|
+
for pt in reserved_points:
|
|
95
|
+
if candidate_poly.distance(pt) < clearance_radius:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# 5. Check corridor buffer between objects
|
|
99
|
+
if enforce_corridor and placed_polygons:
|
|
100
|
+
buffer_dist = self.min_corridor_width / 2.0
|
|
101
|
+
buffered_candidate = candidate_poly.buffer(buffer_dist)
|
|
102
|
+
for p in placed_polygons:
|
|
103
|
+
# If buffered candidate overlaps with existing obstacle, corridor is < min_corridor
|
|
104
|
+
if buffered_candidate.intersects(p):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def solve_layout(
|
|
110
|
+
self,
|
|
111
|
+
environment: EnvironmentSpec,
|
|
112
|
+
objects: List[ObjectSpec],
|
|
113
|
+
spawn_pose: Optional[Pose2D] = None,
|
|
114
|
+
goal_pose: Optional[Pose2D] = None,
|
|
115
|
+
) -> Tuple[List[ObjectSpec], List[AisleCorridor]]:
|
|
116
|
+
"""
|
|
117
|
+
Solves placement for a list of objects in the given environment.
|
|
118
|
+
Preserves specified poses if collision-free, otherwise packs them
|
|
119
|
+
in structured rows ensuring >= 2.2m navigation corridors.
|
|
120
|
+
"""
|
|
121
|
+
room_poly = self.create_room_polygon(environment.room_dimensions)
|
|
122
|
+
placed_specs: List[ObjectSpec] = []
|
|
123
|
+
placed_polygons: List[Polygon] = []
|
|
124
|
+
|
|
125
|
+
reserved_points: List[Point] = []
|
|
126
|
+
if spawn_pose:
|
|
127
|
+
reserved_points.append(Point(spawn_pose.x, spawn_pose.y))
|
|
128
|
+
if goal_pose:
|
|
129
|
+
reserved_points.append(Point(goal_pose.x, goal_pose.y))
|
|
130
|
+
|
|
131
|
+
# Separate items with predetermined valid poses vs unplaced items
|
|
132
|
+
unplaced_items: List[ObjectSpec] = []
|
|
133
|
+
|
|
134
|
+
for obj in objects:
|
|
135
|
+
candidate_poly = self.create_obb(
|
|
136
|
+
obj.pose.x,
|
|
137
|
+
obj.pose.y,
|
|
138
|
+
obj.dimensions.x,
|
|
139
|
+
obj.dimensions.y,
|
|
140
|
+
obj.pose.yaw,
|
|
141
|
+
)
|
|
142
|
+
# If default (0, 0), we treat it as requiring procedural placement
|
|
143
|
+
if abs(obj.pose.x) < 1e-3 and abs(obj.pose.y) < 1e-3:
|
|
144
|
+
unplaced_items.append(obj)
|
|
145
|
+
elif self.is_valid_placement(
|
|
146
|
+
candidate_poly,
|
|
147
|
+
placed_polygons,
|
|
148
|
+
room_poly,
|
|
149
|
+
reserved_points,
|
|
150
|
+
enforce_corridor=False,
|
|
151
|
+
):
|
|
152
|
+
placed_specs.append(obj)
|
|
153
|
+
placed_polygons.append(candidate_poly)
|
|
154
|
+
else:
|
|
155
|
+
unplaced_items.append(obj)
|
|
156
|
+
|
|
157
|
+
# Place remaining items in structured rows
|
|
158
|
+
half_w = environment.room_dimensions.x / 2.0 - self.wall_standoff
|
|
159
|
+
half_h = environment.room_dimensions.y / 2.0 - self.wall_standoff
|
|
160
|
+
|
|
161
|
+
# Multi-row search grid
|
|
162
|
+
y_step = self.min_corridor_width + 1.2
|
|
163
|
+
y_positions = [-y_step, y_step, -y_step / 1.5, y_step / 1.5, 0.0]
|
|
164
|
+
|
|
165
|
+
for obj in unplaced_items:
|
|
166
|
+
placed = False
|
|
167
|
+
dx = obj.dimensions.x
|
|
168
|
+
dy = obj.dimensions.y
|
|
169
|
+
|
|
170
|
+
for y_candidate in y_positions:
|
|
171
|
+
if abs(y_candidate) + dy / 2.0 > half_h:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
# Search X with fine granularity to navigate around reserved points (spawn/goal)
|
|
175
|
+
min_x = -half_w + dx / 2.0
|
|
176
|
+
max_x = half_w - dx / 2.0
|
|
177
|
+
curr_x = min_x
|
|
178
|
+
|
|
179
|
+
while curr_x <= max_x:
|
|
180
|
+
candidate_poly = self.create_obb(curr_x, y_candidate, dx, dy, 0.0)
|
|
181
|
+
|
|
182
|
+
if self.is_valid_placement(
|
|
183
|
+
candidate_poly,
|
|
184
|
+
placed_polygons,
|
|
185
|
+
room_poly,
|
|
186
|
+
reserved_points,
|
|
187
|
+
enforce_corridor=True,
|
|
188
|
+
):
|
|
189
|
+
updated_obj = obj.model_copy(
|
|
190
|
+
update={"pose": Pose2D(x=round(curr_x, 2), y=round(y_candidate, 2), yaw=0.0)}
|
|
191
|
+
)
|
|
192
|
+
placed_specs.append(updated_obj)
|
|
193
|
+
placed_polygons.append(candidate_poly)
|
|
194
|
+
placed = True
|
|
195
|
+
break
|
|
196
|
+
curr_x += 0.5
|
|
197
|
+
|
|
198
|
+
if placed:
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
# Fallback placement: try rotated orientations along perimeter walls
|
|
202
|
+
if not placed:
|
|
203
|
+
for angle in [0.0, math.pi / 2.0]:
|
|
204
|
+
dim_x = dx if angle == 0.0 else dy
|
|
205
|
+
dim_y = dy if angle == 0.0 else dx
|
|
206
|
+
min_x = -half_w + dim_x / 2.0
|
|
207
|
+
max_x = half_w - dim_x / 2.0
|
|
208
|
+
|
|
209
|
+
for y_candidate in [-half_h + dim_y / 2.0, half_h - dim_y / 2.0]:
|
|
210
|
+
curr_x = min_x
|
|
211
|
+
while curr_x <= max_x:
|
|
212
|
+
candidate_poly = self.create_obb(curr_x, y_candidate, dim_x, dim_y, angle)
|
|
213
|
+
if self.is_valid_placement(
|
|
214
|
+
candidate_poly,
|
|
215
|
+
placed_polygons,
|
|
216
|
+
room_poly,
|
|
217
|
+
reserved_points,
|
|
218
|
+
enforce_corridor=False,
|
|
219
|
+
):
|
|
220
|
+
updated_obj = obj.model_copy(
|
|
221
|
+
update={"pose": Pose2D(x=round(curr_x, 2), y=round(y_candidate, 2), yaw=round(angle, 4))}
|
|
222
|
+
)
|
|
223
|
+
placed_specs.append(updated_obj)
|
|
224
|
+
placed_polygons.append(candidate_poly)
|
|
225
|
+
placed = True
|
|
226
|
+
break
|
|
227
|
+
curr_x += 0.5
|
|
228
|
+
if placed:
|
|
229
|
+
break
|
|
230
|
+
if placed:
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
# Calculate verified main navigation corridors
|
|
234
|
+
corridors: List[AisleCorridor] = []
|
|
235
|
+
half_w_corr = environment.room_dimensions.x / 2.0 - 1.0
|
|
236
|
+
corridors.append(
|
|
237
|
+
AisleCorridor(
|
|
238
|
+
corridor_id="central_main_aisle",
|
|
239
|
+
width=self.min_corridor_width,
|
|
240
|
+
start_point=Pose2D(x=-half_w_corr, y=0.0, yaw=0.0),
|
|
241
|
+
end_point=Pose2D(x=half_w_corr, y=0.0, yaw=0.0),
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return placed_specs, corridors
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def solve_spatial_layout(
|
|
249
|
+
environment: EnvironmentSpec,
|
|
250
|
+
objects: List[ObjectSpec],
|
|
251
|
+
spawn_pose: Optional[Pose2D] = None,
|
|
252
|
+
goal_pose: Optional[Pose2D] = None,
|
|
253
|
+
) -> Tuple[List[ObjectSpec], List[AisleCorridor]]:
|
|
254
|
+
"""Convenience wrapper for the Shapely spatial solver."""
|
|
255
|
+
solver = ShapelySpatialSolver()
|
|
256
|
+
return solver.solve_layout(environment, objects, spawn_pose, goal_pose)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lang2sim
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Natural Language to Executable Robot Simulation with Dynamic Stress Testing
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pydantic>=2.6.0
|
|
9
|
+
Requires-Dist: shapely>=2.0.0
|
|
10
|
+
Requires-Dist: numpy>=1.24.0
|
|
11
|
+
Requires-Dist: pillow>=10.0.0
|
|
12
|
+
Requires-Dist: jinja2>=3.1.0
|
|
13
|
+
Requires-Dist: textual>=0.70.0
|
|
14
|
+
Requires-Dist: rich>=13.7.0
|
|
15
|
+
Requires-Dist: openai>=1.14.0
|
|
16
|
+
Requires-Dist: fastmcp>=0.1.0
|
|
17
|
+
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: mss>=9.0.0
|
|
19
|
+
Requires-Dist: websockets>=12.0
|
|
20
|
+
Requires-Dist: pytest>=7.4.0
|
|
21
|
+
Provides-Extra: sim
|
|
22
|
+
Requires-Dist: fastapi<1,>=0.110; extra == "sim"
|
|
23
|
+
Requires-Dist: uvicorn[standard]<1,>=0.29; extra == "sim"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Lang2Sim
|
|
30
|
+
|
|
31
|
+
> Natural Language to Executable Robot Simulation with Dynamic Stress Testing.
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
Sim2Lang provides an interactive, full-screen Terminal User Interface (TUI) copilot (`lang2sim`) powered by NVIDIA Nemotron 3 Ultra 550B A55B. It autonomously compiles natural language into procedurally generated Gazebo `.sdf` worlds, zero-SLAM 2D occupancy navigation grids, containerized native desktop Gazebo/RViz2 GUI launches with X11 pass-through, and closed-loop telemetry benchmark scoring ($NQI$).
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Launch
|
|
42
|
+
```bash
|
|
43
|
+
lang2sim
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Run a simulation (Part 3)
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e ".[sim,dev]"
|
|
49
|
+
python scripts/run_sim.py --fixture small_warehouse --mission
|
|
50
|
+
```
|
|
51
|
+
Per-OS setup, the run-folder contract, the control API and troubleshooting:
|
|
52
|
+
[docs/part3-runtime.md](docs/part3-runtime.md).
|
|
53
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
lang2sim/__init__.py,sha256=4wsy-YNkEBJrINg9c7VoL9Db3jqyqYLCYRqUOQXTTFI,1490
|
|
2
|
+
lang2sim/agent/__init__.py,sha256=W-rMJgY6OH7sBuX7C3kaHVCKhMXzdTEFBivhShj_eSs,213
|
|
3
|
+
lang2sim/agent/nemotron_client.py,sha256=a-u-s3na5EOqzm7j4oTACwXqtg1mwmzG2VL_-rBZ6b4,3883
|
|
4
|
+
lang2sim/agent/react_loop.py,sha256=CP3Td2lNYQUOL281eXMJKaYEAWZceZ2boihiI-e0zcY,40481
|
|
5
|
+
lang2sim/agent/tools.py,sha256=oTpgM7h-n0NiYrlgEDJ7_eH0MOXZE7GOThEL0qtwml4,40746
|
|
6
|
+
lang2sim/cli/app.py,sha256=A6LRwLGtmeY06DbDjS_R_He9mefpibitxypmZC0uR4c,7554
|
|
7
|
+
lang2sim/cli/screens/__init__.py,sha256=5v3rzuOANUHKQV6vJiAjHTMmgQBfkdJeaRJDfmNraY8,275
|
|
8
|
+
lang2sim/cli/screens/command_palette_modal.py,sha256=-UyTj2-1ZHVUTYRNnAKAAIUtA75OMUGSnZGwNQKLkl4,6245
|
|
9
|
+
lang2sim/cli/screens/landing_screen.py,sha256=m-bUYQeoDHHU0SSR9Ai1hWP5b2GNLLWT39A2rJeVfgA,3747
|
|
10
|
+
lang2sim/cli/screens/sessions_modal.py,sha256=gnfXa-Iz567zfn2BH25kJHD2B0oWYhFYS8iOUaaXNWU,8633
|
|
11
|
+
lang2sim/cli/screens/workspace_screen.py,sha256=vUsHxU5nnmGHSAro6Dl2aq5UT3aNmNEZ3k4jzeSpF9k,32243
|
|
12
|
+
lang2sim/cli/widgets/__init__.py,sha256=iFpbhwWOUSLliJUD1Ys4CtdxeVzwzusHKRAkM-Sf2EM,355
|
|
13
|
+
lang2sim/cli/widgets/action_pill.py,sha256=hJOEZJDOencAfkL4Bc5l0Ml-7-Re73FZFXv2Ojx5IR8,2151
|
|
14
|
+
lang2sim/cli/widgets/command_card.py,sha256=kN9dxg5_R2QbScLzQwfvAa7-ZR2_mleI4v7w7nm4RZQ,8581
|
|
15
|
+
lang2sim/cli/widgets/plan_card.py,sha256=7DhNV0wYER7QxrPd0fORT5fIclT4diGiKcbQNlvA82o,3396
|
|
16
|
+
lang2sim/cli/widgets/prompt_card.py,sha256=4-S1EXEeXhPeBC3QYJp0n89XRJYD3geFJrFdQT7vpjM,2414
|
|
17
|
+
lang2sim/cli/widgets/telemetry_sidebar.py,sha256=wPKGp3yviGS2HJgj1L3dbAiSHCsf_jVf4yFcMUnyqVE,5133
|
|
18
|
+
lang2sim/cli/widgets/thought_box.py,sha256=-V7XdDaRqZL9uJ7uM7FbFQh5eJYd4_UZqeOS9SXiHOw,2288
|
|
19
|
+
lang2sim/compiler/__init__.py,sha256=cquaTxY0dJQs3XyOQfY_sGDOfwcPOUqdkAZ-bP9l0_k,418
|
|
20
|
+
lang2sim/compiler/sdf_compiler.py,sha256=NWWv9ym69oW1JC3OsEUN1Ensvac5D9lwZfV2WcPGwXQ,2048
|
|
21
|
+
lang2sim/compiler/sir_generator.py,sha256=uun8NTF6Y-GDUkmC0Q8qTPB3wjQ0vYrRjYxyGS10dr8,1520
|
|
22
|
+
lang2sim/compiler/sir_validator.py,sha256=2OAQDfdR2-toElKTsKjbBJaspF2fe8iB3IgI41Tn9Ow,3404
|
|
23
|
+
lang2sim/core/__init__.py,sha256=B2Z6mTcp_7wnsaXgxxlG9lOuuO4KAXDfDKAkImLFPCM,1028
|
|
24
|
+
lang2sim/core/config.py,sha256=CrPj8lC--DiPm80RNjQoURRwgXrBIbOdhqtbCg41b08,4425
|
|
25
|
+
lang2sim/core/models.py,sha256=ITk1KPH8smrgpWsXfYhE_p6Ei6JTElc2odBfM1rq55E,13468
|
|
26
|
+
lang2sim/core/session_manager.py,sha256=mIN-Psh_U7zUcD3iMf-tyJyzoWO_y20x8_S_1CopS-4,6546
|
|
27
|
+
lang2sim/perturbation/__init__.py,sha256=EoDgx_g9YJ5UTBy9Cls0Sx7ubxXVZdyVxTseBOiTNuk,1109
|
|
28
|
+
lang2sim/perturbation/_compat.py,sha256=Km0MlEjLrk2oxpRLXwNt1IW4je2w2tpCWocaIqH6bRE,653
|
|
29
|
+
lang2sim/perturbation/gazebo_bridge.py,sha256=SBoe2zQ2oxc4XDtNdxrBg8ncifWD0A5apHYLPdijYdI,21094
|
|
30
|
+
lang2sim/perturbation/mcp_server.py,sha256=8Qb_mGlewxRMlwJY46NyYXiGp4XugUhCPF2Ig0ZZf6w,4692
|
|
31
|
+
lang2sim/perturbation/sdf_snippets.py,sha256=5KwRI2iluUrDMGLMIQTHIx2bBYBARW0Qd-EkmYUU9AU,2992
|
|
32
|
+
lang2sim/perturbation/routers/__init__.py,sha256=BZi3_h7RcDE0oSKxhgbH0dYLcLQhKm1Nf9HWCrXjHX0,408
|
|
33
|
+
lang2sim/perturbation/routers/world.py,sha256=_yONosMk7KEqteW0wHPGhhEjcLFoL03LT4qdQEhQ6Jw,4230
|
|
34
|
+
lang2sim/runtime/__init__.py,sha256=qER5-OyjkYDV4O9mPaSpoKl8A_Rh8bOjrT1E8Uu5scQ,317
|
|
35
|
+
lang2sim/runtime/display.py,sha256=aISU4WQ9zv6gU_T34AIFg7i0I7r7OL8Z8hjmfx3YWWI,5573
|
|
36
|
+
lang2sim/runtime/docker_runner.py,sha256=SK5cBwtXnv9UuYW7kCyplxVERyu5o3ozHIXAwNXqQb0,11035
|
|
37
|
+
lang2sim/runtime/doctor.py,sha256=CdjLC37Se9TMCF1e87IG7nzFFKEHZZByJhhOY1L3AdY,9756
|
|
38
|
+
lang2sim/runtime/hardware_probe.py,sha256=5rE4X0jBBxF6QzmjsHaCMv0Sk1pPoGKh049YtGQCF-U,4684
|
|
39
|
+
lang2sim/runtime/health_monitor.py,sha256=Cy-Lt1SX7imHD809eJaw3BHVZ1E-V2YlwBzVDI_m6lw,2825
|
|
40
|
+
lang2sim/runtime/launch_builder.py,sha256=OHqcnZLPgJe-tca6jk2m1sfUhLWj19o0HtXIDsSiS0o,6205
|
|
41
|
+
lang2sim/runtime/nav2_dispatcher.py,sha256=6RuXjlrlYg2HQpyJlJGE-tCKCrjQ5ARKxmvmkgXKu5g,8692
|
|
42
|
+
lang2sim/runtime/platform.py,sha256=KgnOsoBH98Z729678J1SG9D5GgMsZVWar0jATqeUhQk,4991
|
|
43
|
+
lang2sim/runtime/ros_bridge.py,sha256=8akxjMvgtx4PfSCPotj3Lg1tnFivXo1UZqcqm24BrW8,7702
|
|
44
|
+
lang2sim/runtime/run_folder.py,sha256=tSdRH9jcEj6guc1innSbS-az5wiXNJX_ojcCnXLUnyM,3452
|
|
45
|
+
lang2sim/runtime/self_healing.py,sha256=pxXxZhs55qFIWUKfgZ2kDzs0lzoqX49Ne9KEv-oEQWI,17529
|
|
46
|
+
lang2sim/runtime/session.py,sha256=InbipEQlRMPC64kj0RYCyXftkBwScdeYm9CcKzhWZWA,6126
|
|
47
|
+
lang2sim/runtime/world_check.py,sha256=lfhWjEUEtkvuQUDWs5hfQZPVsafGjVcXD40IQ-5rmCA,8108
|
|
48
|
+
lang2sim/runtime/control_api/__init__.py,sha256=kHogjfpqCAfsE4fBaiNlt64XIOTcZACDrNOAF8ntUsI,245
|
|
49
|
+
lang2sim/runtime/control_api/client.py,sha256=6wqeqrWVr9BYzHC10fsjp7kZpeXqLqIgF26TXZg2ooM,7804
|
|
50
|
+
lang2sim/runtime/control_api/server.py,sha256=9u3orHR4pq4pemeeW1JGGZrOUW-PVv3j8KBZDxgwGXY,20313
|
|
51
|
+
lang2sim/telemetry/__init__.py,sha256=llBfVQqUspUoN1YHC7WoXINpEi2tpmhYRGHLqpKRuts,1966
|
|
52
|
+
lang2sim/telemetry/nqi_scorer.py,sha256=IN3cPDfKDraoHvpNPrLlNRUcd-rgClpheaaZG3ZPiVg,26953
|
|
53
|
+
lang2sim/telemetry/ros_subscribers.py,sha256=zAO49AwDwP8Coi9hMtUr-H2Jy562wwofdk0Tk9fS9Vc,19124
|
|
54
|
+
lang2sim/telemetry/trajectory.py,sha256=5-i5l0Q2IdiO0N2zxoG2BRXRDQckHJcuEac5Ydgqk5I,10078
|
|
55
|
+
lang2sim/telemetry/vision.py,sha256=EWz2mhDPw8pLf8tzSc7smD_-5B7q0lSNMVPM3ZSuyRQ,17155
|
|
56
|
+
lang2sim/world_gen/__init__.py,sha256=7Fcl4lVE402UjFnd6rGz-o_wECeEn9wajZ3GLMm0_MM,476
|
|
57
|
+
lang2sim/world_gen/catalog_loader.py,sha256=irArZgjeyCB_-JvzPN2EEHOQZTl4j3F-cP_zG68uqDo,3331
|
|
58
|
+
lang2sim/world_gen/map_slicer.py,sha256=4P5f9-4EO9pBqPxRv9xzBKqGKxWNLqXymXiWH_Fyuhw,4659
|
|
59
|
+
lang2sim/world_gen/shapely_solver.py,sha256=ehzJvOQ89z1qtfK2VHuDCLdYeSdkW_XjMQx6cGUR6-s,9626
|
|
60
|
+
lang2sim-2.0.0.dist-info/licenses/LICENSE,sha256=obd9QomNbPEL6QYz5RsBqc2LhZc9ja3n4023r1b5MOg,1067
|
|
61
|
+
lang2sim-2.0.0.dist-info/METADATA,sha256=3MZhOig1m_Fhhsjj3Thv1kMqnkn48GUNE9tPNbYgLjs,1722
|
|
62
|
+
lang2sim-2.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
63
|
+
lang2sim-2.0.0.dist-info/entry_points.txt,sha256=UHbZPiD0vqV3XBH3st2-QWg8f24yrFNikScs0inX8nw,51
|
|
64
|
+
lang2sim-2.0.0.dist-info/top_level.txt,sha256=uYNJ4yakAafOAf4K_m2b_xVxJ1cQCvxUR3nkR8qH6XY,9
|
|
65
|
+
lang2sim-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Neel Dutta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lang2sim
|