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.
Files changed (65) hide show
  1. lang2sim/__init__.py +47 -0
  2. lang2sim/agent/__init__.py +5 -0
  3. lang2sim/agent/nemotron_client.py +109 -0
  4. lang2sim/agent/react_loop.py +848 -0
  5. lang2sim/agent/tools.py +998 -0
  6. lang2sim/cli/app.py +183 -0
  7. lang2sim/cli/screens/__init__.py +8 -0
  8. lang2sim/cli/screens/command_palette_modal.py +146 -0
  9. lang2sim/cli/screens/landing_screen.py +96 -0
  10. lang2sim/cli/screens/sessions_modal.py +236 -0
  11. lang2sim/cli/screens/workspace_screen.py +709 -0
  12. lang2sim/cli/widgets/__init__.py +16 -0
  13. lang2sim/cli/widgets/action_pill.py +67 -0
  14. lang2sim/cli/widgets/command_card.py +265 -0
  15. lang2sim/cli/widgets/plan_card.py +106 -0
  16. lang2sim/cli/widgets/prompt_card.py +96 -0
  17. lang2sim/cli/widgets/telemetry_sidebar.py +117 -0
  18. lang2sim/cli/widgets/thought_box.py +64 -0
  19. lang2sim/compiler/__init__.py +15 -0
  20. lang2sim/compiler/sdf_compiler.py +65 -0
  21. lang2sim/compiler/sir_generator.py +46 -0
  22. lang2sim/compiler/sir_validator.py +87 -0
  23. lang2sim/core/__init__.py +50 -0
  24. lang2sim/core/config.py +116 -0
  25. lang2sim/core/models.py +279 -0
  26. lang2sim/core/session_manager.py +189 -0
  27. lang2sim/perturbation/__init__.py +50 -0
  28. lang2sim/perturbation/_compat.py +19 -0
  29. lang2sim/perturbation/gazebo_bridge.py +520 -0
  30. lang2sim/perturbation/mcp_server.py +129 -0
  31. lang2sim/perturbation/routers/__init__.py +19 -0
  32. lang2sim/perturbation/routers/world.py +110 -0
  33. lang2sim/perturbation/sdf_snippets.py +112 -0
  34. lang2sim/runtime/__init__.py +7 -0
  35. lang2sim/runtime/control_api/__init__.py +7 -0
  36. lang2sim/runtime/control_api/client.py +187 -0
  37. lang2sim/runtime/control_api/server.py +504 -0
  38. lang2sim/runtime/display.py +141 -0
  39. lang2sim/runtime/docker_runner.py +286 -0
  40. lang2sim/runtime/doctor.py +220 -0
  41. lang2sim/runtime/hardware_probe.py +144 -0
  42. lang2sim/runtime/health_monitor.py +66 -0
  43. lang2sim/runtime/launch_builder.py +137 -0
  44. lang2sim/runtime/nav2_dispatcher.py +215 -0
  45. lang2sim/runtime/platform.py +137 -0
  46. lang2sim/runtime/ros_bridge.py +183 -0
  47. lang2sim/runtime/run_folder.py +86 -0
  48. lang2sim/runtime/self_healing.py +387 -0
  49. lang2sim/runtime/session.py +136 -0
  50. lang2sim/runtime/world_check.py +197 -0
  51. lang2sim/telemetry/__init__.py +85 -0
  52. lang2sim/telemetry/nqi_scorer.py +645 -0
  53. lang2sim/telemetry/ros_subscribers.py +489 -0
  54. lang2sim/telemetry/trajectory.py +258 -0
  55. lang2sim/telemetry/vision.py +477 -0
  56. lang2sim/world_gen/__init__.py +16 -0
  57. lang2sim/world_gen/catalog_loader.py +83 -0
  58. lang2sim/world_gen/map_slicer.py +131 -0
  59. lang2sim/world_gen/shapely_solver.py +256 -0
  60. lang2sim-2.0.0.dist-info/METADATA +53 -0
  61. lang2sim-2.0.0.dist-info/RECORD +65 -0
  62. lang2sim-2.0.0.dist-info/WHEEL +5 -0
  63. lang2sim-2.0.0.dist-info/entry_points.txt +2 -0
  64. lang2sim-2.0.0.dist-info/licenses/LICENSE +21 -0
  65. lang2sim-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ """
2
+ Collapsible Thought Box Widget
3
+ File: lang2sim/cli/widgets/thought_box.py
4
+ """
5
+
6
+ from textual.widget import Widget
7
+ from textual.widgets import Static
8
+ from textual.reactive import reactive
9
+ from textual.events import Click
10
+
11
+ class ThoughtBox(Widget):
12
+ """Collapsible reasoning thought block matching OpenCode UX (+ Thought: 1.2s)."""
13
+
14
+ DEFAULT_CSS = """
15
+ ThoughtBox {
16
+ height: auto;
17
+ margin-bottom: 1;
18
+ }
19
+ """
20
+
21
+ is_collapsed = reactive(True)
22
+ duration_str = reactive("1.2s")
23
+ thought_content = reactive("")
24
+
25
+ def __init__(self, duration_str: str = "1.2s", content: str = "", **kwargs):
26
+ super().__init__(classes="thought-box", **kwargs)
27
+ self.duration_str = duration_str
28
+ self.thought_content = content or "Evaluating warehouse boundaries and spatial constraints..."
29
+
30
+ def compose(self):
31
+ yield Static(self._format_header(), id="thought-header", classes="thought-header")
32
+ yield Static(self.thought_content, id="thought-body", classes="thought-content")
33
+
34
+ def _format_header(self) -> str:
35
+ symbol = "+" if self.is_collapsed else "−"
36
+ hint = "" if self.is_collapsed else " (click to collapse)"
37
+ return f"{symbol} Thought: {self.duration_str}{hint}"
38
+
39
+ def on_click(self, event: Click) -> None:
40
+ self.toggle()
41
+
42
+ def toggle(self) -> None:
43
+ self.is_collapsed = not self.is_collapsed
44
+ header = self.query_one("#thought-header", Static)
45
+ body = self.query_one("#thought-body", Static)
46
+ header.update(self._format_header())
47
+ body.display = not self.is_collapsed
48
+
49
+ def on_mount(self) -> None:
50
+ body = self.query_one("#thought-body", Static)
51
+ body.display = not self.is_collapsed
52
+
53
+ def update_content(self, content: str, duration_str: str = "") -> None:
54
+ """Dynamically update thought text and duration during streaming or completion."""
55
+ self.thought_content = content
56
+ if duration_str:
57
+ self.duration_str = duration_str
58
+ try:
59
+ header = self.query_one("#thought-header", Static)
60
+ body = self.query_one("#thought-body", Static)
61
+ header.update(self._format_header())
62
+ body.update(self.thought_content)
63
+ except Exception:
64
+ pass
@@ -0,0 +1,15 @@
1
+ """
2
+ Simulation world and launch compilers for Sim2Lang.
3
+ """
4
+
5
+ from lang2sim.compiler.sdf_compiler import HarmonicSdfCompiler, compile_world_sdf
6
+ from lang2sim.compiler.sir_validator import SIRValidator
7
+ from lang2sim.compiler.sir_generator import SIRGenerator, generate_simulation_ir
8
+
9
+ __all__ = [
10
+ "HarmonicSdfCompiler",
11
+ "compile_world_sdf",
12
+ "SIRValidator",
13
+ "SIRGenerator",
14
+ "generate_simulation_ir",
15
+ ]
@@ -0,0 +1,65 @@
1
+ """
2
+ Gazebo Harmonic (SDF 1.9+) World Compiler.
3
+ File: lang2sim/compiler/sdf_compiler.py
4
+ """
5
+
6
+ from pathlib import Path
7
+ from jinja2 import Environment, FileSystemLoader
8
+
9
+ from lang2sim.core.models import SimSpec
10
+ from lang2sim.core.config import config
11
+
12
+
13
+ class HarmonicSdfCompiler:
14
+ """
15
+ Compiles canonical SimSpec models into physically accurate Gazebo Harmonic
16
+ SDF 1.9+ XML worlds.
17
+ """
18
+
19
+ def __init__(self, templates_dir: Path = config.templates_dir):
20
+ self.templates_dir = templates_dir
21
+ self.env = Environment(
22
+ loader=FileSystemLoader(str(self.templates_dir)),
23
+ autoescape=False,
24
+ trim_blocks=True,
25
+ lstrip_blocks=True,
26
+ )
27
+ self.template = self.env.get_template("world_harmonic.sdf.jinja")
28
+
29
+ def compile(
30
+ self,
31
+ sim_spec: SimSpec,
32
+ output_path: Path,
33
+ world_name: str = "default_world",
34
+ target: str = "harmonic",
35
+ ) -> Path:
36
+ """
37
+ Renders the SDF template for target simulator and writes the result to disk.
38
+ """
39
+ output_path.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ context = {
42
+ "world_name": world_name,
43
+ "target": target,
44
+ "ambient_light": sim_spec.environment.ambient_light_luminance,
45
+ "surface_friction": sim_spec.environment.surface_friction,
46
+ "sky": sim_spec.environment.sky_weather,
47
+ "room": sim_spec.environment.room_dimensions,
48
+ "wall_height": sim_spec.environment.wall_height,
49
+ "objects": sim_spec.objects,
50
+ }
51
+
52
+ rendered_xml = self.template.render(context)
53
+
54
+ with open(output_path, "w", encoding="utf-8") as f:
55
+ f.write(rendered_xml)
56
+
57
+ return output_path
58
+
59
+
60
+ def compile_world_sdf(
61
+ sim_spec: SimSpec, output_path: Path, world_name: str = "default_world"
62
+ ) -> Path:
63
+ """Convenience helper to compile a SimSpec into a world.sdf file."""
64
+ compiler = HarmonicSdfCompiler()
65
+ return compiler.compile(sim_spec, output_path, world_name)
@@ -0,0 +1,46 @@
1
+ """
2
+ Simulation Intermediate Representation (SIR) Generator.
3
+ File: lang2sim/compiler/sir_generator.py
4
+ """
5
+
6
+ from typing import Optional
7
+ from lang2sim.core.models import SimSpec, SimulationIR
8
+ from lang2sim.world_gen.shapely_solver import ShapelySpatialSolver
9
+ from lang2sim.compiler.sir_validator import SIRValidator
10
+
11
+
12
+ class SIRGenerator:
13
+ """
14
+ Compiles raw SimSpec declarations into validated SimulationIR graphs.
15
+ """
16
+
17
+ def __init__(self):
18
+ self.solver = ShapelySpatialSolver()
19
+ self.validator = SIRValidator()
20
+
21
+ def generate(self, sim_spec: SimSpec) -> SimulationIR:
22
+ """
23
+ Solves placement for unplaced objects and builds the canonical SimulationIR.
24
+ """
25
+ placed_objects, corridors = self.solver.solve_layout(
26
+ environment=sim_spec.environment,
27
+ objects=sim_spec.objects,
28
+ spawn_pose=sim_spec.mission.initial_pose,
29
+ goal_pose=sim_spec.mission.goal_pose,
30
+ )
31
+
32
+ resolved_spec = sim_spec.model_copy(update={"objects": placed_objects})
33
+ is_valid, validation_messages = self.validator.validate(resolved_spec)
34
+
35
+ return SimulationIR(
36
+ spec=resolved_spec,
37
+ aisle_corridors=corridors,
38
+ is_valid=is_valid,
39
+ validation_messages=validation_messages,
40
+ )
41
+
42
+
43
+ def generate_simulation_ir(sim_spec: SimSpec) -> SimulationIR:
44
+ """Convenience helper to generate validated SimulationIR."""
45
+ generator = SIRGenerator()
46
+ return generator.generate(sim_spec)
@@ -0,0 +1,87 @@
1
+ """
2
+ Simulation Intermediate Representation (SIR) Validator.
3
+ File: lang2sim/compiler/sir_validator.py
4
+ """
5
+
6
+ from typing import List, Tuple
7
+ from shapely.geometry import box, Point
8
+
9
+ from lang2sim.core.models import SimSpec, SimulationIR
10
+ from lang2sim.core.config import (
11
+ ROBOT_CIRCUMSCRIBED_RADIUS,
12
+ MIN_AISLE_CORRIDOR_WIDTH,
13
+ MIN_WALL_STANDOFF,
14
+ )
15
+ from lang2sim.world_gen.shapely_solver import ShapelySpatialSolver
16
+
17
+
18
+ class SIRValidator:
19
+ """
20
+ Physical feasibility and safety validator for Simulation Intermediate Representations.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ min_corridor_width: float = MIN_AISLE_CORRIDOR_WIDTH,
26
+ wall_standoff: float = MIN_WALL_STANDOFF,
27
+ robot_radius: float = ROBOT_CIRCUMSCRIBED_RADIUS,
28
+ ):
29
+ self.min_corridor_width = min_corridor_width
30
+ self.wall_standoff = wall_standoff
31
+ self.robot_radius = robot_radius
32
+ self.solver = ShapelySpatialSolver(
33
+ min_corridor_width=min_corridor_width,
34
+ wall_standoff=wall_standoff,
35
+ robot_radius=robot_radius,
36
+ )
37
+
38
+ def validate(self, sim_spec: SimSpec) -> Tuple[bool, List[str]]:
39
+ """
40
+ Validates geometric feasibility, bounds, non-intersection, and reachability.
41
+ """
42
+ errors: List[str] = []
43
+ env = sim_spec.environment
44
+ room_poly = self.solver.create_room_polygon(env.room_dimensions)
45
+
46
+ # 1. Validate room dimensions
47
+ if env.room_dimensions.x <= 0 or env.room_dimensions.y <= 0:
48
+ errors.append(f"Invalid room dimensions: {env.room_dimensions}")
49
+
50
+ # 2. Validate object boundaries and overlaps
51
+ placed_polygons = []
52
+ for obj in sim_spec.objects:
53
+ poly = self.solver.create_obb(
54
+ obj.pose.x, obj.pose.y, obj.dimensions.x, obj.dimensions.y, obj.pose.yaw
55
+ )
56
+
57
+ # Check inside room
58
+ if not room_poly.contains(poly):
59
+ errors.append(f"Object '{obj.id}' is outside room boundaries at ({obj.pose.x}, {obj.pose.y})")
60
+
61
+ # Check overlap with existing objects
62
+ for idx, existing in enumerate(placed_polygons):
63
+ if poly.intersects(existing):
64
+ errors.append(f"Collision detected: Object '{obj.id}' overlaps with object '{sim_spec.objects[idx].id}'")
65
+
66
+ placed_polygons.append(poly)
67
+
68
+ # 3. Validate robot initial spawn pose
69
+ spawn_pt = Point(sim_spec.mission.initial_pose.x, sim_spec.mission.initial_pose.y)
70
+ if not room_poly.contains(spawn_pt):
71
+ errors.append(f"Spawn pose ({spawn_pt.x}, {spawn_pt.y}) is outside room bounds")
72
+
73
+ for idx, poly in enumerate(placed_polygons):
74
+ if poly.distance(spawn_pt) < (self.robot_radius + 0.1):
75
+ errors.append(f"Spawn pose in collision with object '{sim_spec.objects[idx].id}'")
76
+
77
+ # 4. Validate goal pose
78
+ goal_pt = Point(sim_spec.mission.goal_pose.x, sim_spec.mission.goal_pose.y)
79
+ if not room_poly.contains(goal_pt):
80
+ errors.append(f"Goal pose ({goal_pt.x}, {goal_pt.y}) is outside room bounds")
81
+
82
+ for idx, poly in enumerate(placed_polygons):
83
+ if poly.distance(goal_pt) < (self.robot_radius + 0.1):
84
+ errors.append(f"Goal pose in collision with object '{sim_spec.objects[idx].id}'")
85
+
86
+ is_valid = len(errors) == 0
87
+ return is_valid, errors
@@ -0,0 +1,50 @@
1
+ """Core data contracts, schemas, and configurations for Sim2Lang."""
2
+
3
+ from lang2sim.core.models import (
4
+ Pose2D,
5
+ Vector3D,
6
+ SkyWeatherSpec,
7
+ EnvironmentSpec,
8
+ ObjectSpec,
9
+ CameraPerturbation,
10
+ LidarPerturbation,
11
+ SensorNoiseSpec,
12
+ DynamicObstacleTrigger,
13
+ MissionSpec,
14
+ SimSpec,
15
+ SimulationIR,
16
+ SimStartRequest,
17
+ RunManifest,
18
+ SimStopRequest,
19
+ SimStatusResponse,
20
+ NavInitialPoseRequest,
21
+ NavGoalRequest,
22
+ NavWaypointsRequest,
23
+ NavStatus,
24
+ )
25
+ from lang2sim.core.config import Config, config
26
+
27
+ __all__ = [
28
+ "Pose2D",
29
+ "Vector3D",
30
+ "SkyWeatherSpec",
31
+ "EnvironmentSpec",
32
+ "ObjectSpec",
33
+ "CameraPerturbation",
34
+ "LidarPerturbation",
35
+ "SensorNoiseSpec",
36
+ "DynamicObstacleTrigger",
37
+ "MissionSpec",
38
+ "SimSpec",
39
+ "SimulationIR",
40
+ "SimStartRequest",
41
+ "RunManifest",
42
+ "SimStopRequest",
43
+ "SimStatusResponse",
44
+ "NavInitialPoseRequest",
45
+ "NavGoalRequest",
46
+ "NavWaypointsRequest",
47
+ "NavStatus",
48
+ "Config",
49
+ "config",
50
+ ]
@@ -0,0 +1,116 @@
1
+ """
2
+ Runtime and Workspace Configuration Manager
3
+ File: lang2sim/core/config.py
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ from datetime import datetime
10
+
11
+ def _env(*names: str, default: str) -> str:
12
+ """First non-empty env var among `names` (L2S_* names are what the sim container sets)."""
13
+ for n in names:
14
+ v = os.getenv(n)
15
+ if v:
16
+ return v
17
+ return default
18
+
19
+
20
+ # Root paths. Inside the sim container the entrypoint sets L2S_PROJECTS_DIR /
21
+ # L2S_CATALOG_DIR to the bind-mount targets, so the same code resolves to the
22
+ # host's ~/.lang2sim/projects on both sides.
23
+ DEFAULT_LANG2SIM_ROOT = Path(os.path.expanduser(_env("L2S_ROOT", default="~/.lang2sim")))
24
+ PROJECTS_DIR = Path(_env("L2S_PROJECTS_DIR", default=str(DEFAULT_LANG2SIM_ROOT / "projects")))
25
+ CATALOG_DIR = Path(_env("L2S_CATALOG_DIR", default=str(Path(__file__).resolve().parent.parent.parent / "catalog")))
26
+
27
+ # Container-side mount points (contract with docker/sim/Dockerfile).
28
+ CONTAINER_PROJECTS_DIR = "/lang2sim/projects"
29
+ CONTAINER_CATALOG_DIR = "/lang2sim/catalog"
30
+ CONTAINER_SRC_DIR = "/opt/lang2sim/src"
31
+
32
+ # Pinned Container Image and Network Defaults (Dev 3 synced).
33
+ # CI publishes :staging, :main, :sha-<short>, and :<semver> for v* tags; the
34
+ # `-vnc` suffix selects the noVNC variant.
35
+ DEFAULT_SIM_IMAGE = _env("L2S_IMAGE", "LANG2SIM_IMAGE", default="ghcr.io/duttaneel07/lang2sim-sim:staging")
36
+ DEFAULT_CONTROL_PORT = int(_env("L2S_API_PORT", "CONTROL_API_PORT", default="9090"))
37
+ DEFAULT_FOXGLOVE_PORT = int(_env("L2S_FOXGLOVE_PORT", "FOXGLOVE_PORT", default="8765"))
38
+ DEFAULT_NOVNC_PORT = int(_env("L2S_NOVNC_PORT", "NOVNC_PORT", default="6080"))
39
+
40
+ # Physical Robot Kinematics (TurtleBot3 Waffle standard)
41
+ ROBOT_CIRCUMSCRIBED_RADIUS = 0.22 # meters
42
+ ROBOT_SAFETY_MARGIN = 0.30 # meters
43
+ MIN_AISLE_CORRIDOR_WIDTH = 2.20 # meters
44
+ MIN_WALL_STANDOFF = 0.50 # meters
45
+ MAP_RESOLUTION = 0.05 # meters per pixel
46
+
47
+
48
+ def ensure_directories():
49
+ """Ensure ~/.lang2sim and projects directory exist."""
50
+ DEFAULT_LANG2SIM_ROOT.mkdir(parents=True, exist_ok=True)
51
+ PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
52
+
53
+
54
+ def get_projects_dir() -> Path:
55
+ """Returns the base projects directory."""
56
+ ensure_directories()
57
+ return PROJECTS_DIR
58
+
59
+
60
+ def get_active_project_dir(project_name: str = "active_project") -> Path:
61
+ """Returns an isolated project directory."""
62
+ ensure_directories()
63
+ p_dir = PROJECTS_DIR / project_name
64
+ p_dir.mkdir(parents=True, exist_ok=True)
65
+ return p_dir
66
+
67
+
68
+ def get_run_dir(project_name: str = "active_project", timestamp: Optional[str] = None) -> Path:
69
+ """
70
+ Returns an isolated timestamped run directory matching Dev 3 convention:
71
+ ~/.lang2sim/projects/<project_name>/runs/<timestamp>/
72
+ """
73
+ p_dir = get_active_project_dir(project_name)
74
+ runs_dir = p_dir / "runs"
75
+ runs_dir.mkdir(parents=True, exist_ok=True)
76
+ if timestamp is None:
77
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
78
+ run_dir = runs_dir / timestamp
79
+ run_dir.mkdir(parents=True, exist_ok=True)
80
+ return run_dir
81
+
82
+
83
+ def get_catalog_dir() -> Path:
84
+ """Returns the catalog directory."""
85
+ return CATALOG_DIR
86
+
87
+
88
+ class Config:
89
+ """Object-oriented wrapper around configuration settings."""
90
+
91
+ def __init__(self, workspace_root: Optional[Path] = None):
92
+ self.workspace_root = workspace_root or Path(__file__).resolve().parent.parent.parent
93
+ self.catalog_dir = CATALOG_DIR
94
+ self.templates_dir = self.workspace_root / "lang2sim" / "compiler" / "templates"
95
+ self.base_projects_dir = PROJECTS_DIR
96
+
97
+ self.sim_image_tag = DEFAULT_SIM_IMAGE
98
+ self.control_api_port = DEFAULT_CONTROL_PORT
99
+ self.foxglove_port = DEFAULT_FOXGLOVE_PORT
100
+ self.novnc_port = DEFAULT_NOVNC_PORT
101
+
102
+ self.robot_circumscribed_radius = ROBOT_CIRCUMSCRIBED_RADIUS
103
+ self.robot_safety_margin = ROBOT_SAFETY_MARGIN
104
+ self.min_aisle_corridor_width = MIN_AISLE_CORRIDOR_WIDTH
105
+ self.min_wall_standoff = MIN_WALL_STANDOFF
106
+ self.map_resolution = MAP_RESOLUTION
107
+
108
+ def get_project_dir(self, project_name: str = "active_project") -> Path:
109
+ return get_active_project_dir(project_name)
110
+
111
+ def create_run_dir(self, project_name: str = "active_project", timestamp: Optional[str] = None) -> Path:
112
+ return get_run_dir(project_name, timestamp)
113
+
114
+
115
+ # Global singleton instance
116
+ config = Config()