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,110 @@
1
+ """FastAPI router for dynamic simulation stress mutations (/v1/world).
2
+
3
+ Mounted inside the simulation container (Control API server, :9090)
4
+ or accessible via ControlClient on host.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, List, Optional
10
+ from fastapi import APIRouter, FastAPI, HTTPException
11
+ from pydantic import BaseModel, Field
12
+
13
+ from lang2sim.perturbation._compat import Pose2D
14
+ from lang2sim.perturbation.gazebo_bridge import GazeboBridge, get_bridge
15
+
16
+ router = APIRouter(prefix="/world", tags=["world_mutation"])
17
+
18
+
19
+ class SpawnRequest(BaseModel):
20
+ model_name: str = Field("wooden_crate", description="Asset type to spawn")
21
+ x: float = Field(2.0, description="X coordinate in meters")
22
+ y: float = Field(0.0, description="Y coordinate in meters")
23
+ z: float = Field(0.5, description="Z coordinate in meters")
24
+ yaw: float = Field(0.0, description="Heading rotation in radians")
25
+ linear_velocity: Optional[List[float]] = Field(None, description="Initial [vx, vy, vz]")
26
+ angular_velocity: Optional[List[float]] = Field(None, description="Initial [wx, wy, wz]")
27
+ entity_name: Optional[str] = Field(None, description="Optional custom entity name")
28
+
29
+
30
+ class LightRequest(BaseModel):
31
+ diffuse: float = Field(0.5, ge=0.0, le=1.0, description="Sun diffuse reflectance")
32
+ specular: float = Field(0.1, ge=0.0, le=1.0, description="Sun specular reflectance")
33
+ intensity: Optional[float] = Field(None, ge=0.0, le=1.0, description="Overall luminance multiplier")
34
+ light_name: str = Field("sun", description="Target light entity")
35
+
36
+
37
+ class ResetRequest(BaseModel):
38
+ mode: str = Field("world", description="Reset scope ('world' or 'robot')")
39
+
40
+
41
+ class ChassisPerturbationRequest(BaseModel):
42
+ force_x: float = Field(10.0, description="X impulse force / velocity bias")
43
+ force_y: float = Field(0.0, description="Y impulse force / velocity bias")
44
+ force_z: float = Field(0.0, description="Z angular impulse / yaw velocity bias")
45
+ duration_s: float = Field(0.2, ge=0.01, le=5.0, description="Impulse duration in seconds")
46
+ robot_model: str = Field("turtlebot3_waffle", description="Target robot model name")
47
+
48
+
49
+ @router.post("/spawn")
50
+ async def spawn_entity(req: SpawnRequest) -> dict[str, Any]:
51
+ bridge: GazeboBridge = get_bridge()
52
+ res = await bridge.spawn_entity(
53
+ req.model_name,
54
+ Pose2D(x=req.x, y=req.y, yaw=req.yaw),
55
+ )
56
+ if not res.ok:
57
+ raise HTTPException(status_code=500, detail=res.to_dict())
58
+ return res.to_dict()
59
+
60
+
61
+ @router.post("/light")
62
+ async def set_light(req: LightRequest) -> dict[str, Any]:
63
+ bridge: GazeboBridge = get_bridge()
64
+ lum = req.intensity if req.intensity is not None else req.diffuse
65
+ res = await bridge.set_light(lum)
66
+ if not res.ok:
67
+ raise HTTPException(status_code=500, detail=res.to_dict())
68
+ return res.to_dict()
69
+
70
+
71
+ @router.post("/reset")
72
+ async def reset_world(req: Optional[ResetRequest] = None) -> dict[str, Any]:
73
+ bridge: GazeboBridge = get_bridge()
74
+ mode = req.mode if req and req.mode else "world"
75
+ res = await bridge.reset(mode)
76
+ if not res.ok:
77
+ raise HTTPException(status_code=500, detail=res.to_dict())
78
+ return res.to_dict()
79
+
80
+
81
+ @router.post("/chassis")
82
+ async def apply_chassis(req: ChassisPerturbationRequest) -> dict[str, Any]:
83
+ bridge: GazeboBridge = get_bridge()
84
+ res = await bridge.apply_perturbation(
85
+ Pose2D(x=req.force_x, y=req.force_y, yaw=req.force_z),
86
+ duration_s=req.duration_s,
87
+ )
88
+ if not res.ok:
89
+ raise HTTPException(status_code=500, detail=res.to_dict())
90
+ return res.to_dict()
91
+
92
+
93
+ @router.get("/health")
94
+ def world_health() -> dict[str, Any]:
95
+ bridge: GazeboBridge = get_bridge()
96
+ h = bridge.health()
97
+ h["ok"] = h.get("status") in ("ready", "healthy", "ok")
98
+ return h
99
+
100
+
101
+ @router.get("/entities")
102
+ def list_entities() -> dict[str, Any]:
103
+ bridge: GazeboBridge = get_bridge()
104
+ entities = bridge.list_spawned_entities()
105
+ return {"backend": type(bridge).__name__, "count": len(entities), "entities": entities}
106
+
107
+
108
+ def mount_world_router(app: FastAPI, prefix: str = "/v1") -> None:
109
+ """Convenience helper to mount the world router into any FastAPI app."""
110
+ app.include_router(router, prefix=prefix)
@@ -0,0 +1,112 @@
1
+ """Fallback inline SDF definitions for dynamic obstacle models.
2
+
3
+ Used when pre-compiled models are not found on the local filesystem or Fuel catalog.
4
+ Provides physics collision geometries, inertial tensors, and basic PBR/diffuse visuals.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ _WOODEN_CRATE_SDF = """<?xml version="1.0" ?>
10
+ <sdf version="1.6">
11
+ <model name="{name}">
12
+ <static>false</static>
13
+ <link name="link">
14
+ <inertial>
15
+ <mass>10.0</mass>
16
+ <inertia>
17
+ <ixx>0.6</ixx><ixy>0</ixy><ixz>0</ixz>
18
+ <iyy>0.6</iyy><iyz>0</iyz><izz>0.6</izz>
19
+ </inertia>
20
+ </inertial>
21
+ <collision name="collision">
22
+ <geometry>
23
+ <box><size>0.6 0.6 0.6</size></box>
24
+ </geometry>
25
+ </collision>
26
+ <visual name="visual">
27
+ <geometry>
28
+ <box><size>0.6 0.6 0.6</size></box>
29
+ </geometry>
30
+ <material>
31
+ <ambient>0.6 0.4 0.2 1</ambient>
32
+ <diffuse>0.6 0.4 0.2 1</diffuse>
33
+ </material>
34
+ </visual>
35
+ </link>
36
+ </model>
37
+ </sdf>
38
+ """
39
+
40
+ _BARREL_SDF = """<?xml version="1.0" ?>
41
+ <sdf version="1.6">
42
+ <model name="{name}">
43
+ <static>false</static>
44
+ <link name="link">
45
+ <inertial>
46
+ <mass>15.0</mass>
47
+ <inertia>
48
+ <ixx>1.2</ixx><ixy>0</ixy><ixz>0</ixz>
49
+ <iyy>1.2</iyy><iyz>0</iyz><izz>0.675</izz>
50
+ </inertia>
51
+ </inertial>
52
+ <collision name="collision">
53
+ <geometry>
54
+ <cylinder><radius>0.3</radius><length>0.9</length></cylinder>
55
+ </geometry>
56
+ </collision>
57
+ <visual name="visual">
58
+ <geometry>
59
+ <cylinder><radius>0.3</radius><length>0.9</length></cylinder>
60
+ </geometry>
61
+ <material>
62
+ <ambient>0.2 0.3 0.6 1</ambient>
63
+ <diffuse>0.2 0.3 0.6 1</diffuse>
64
+ </material>
65
+ </visual>
66
+ </link>
67
+ </model>
68
+ </sdf>
69
+ """
70
+
71
+ _CARGO_BOX_SDF = """<?xml version="1.0" ?>
72
+ <sdf version="1.6">
73
+ <model name="{name}">
74
+ <static>false</static>
75
+ <link name="link">
76
+ <inertial>
77
+ <mass>20.0</mass>
78
+ <inertia>
79
+ <ixx>1.2</ixx><ixy>0</ixy><ixz>0</ixz>
80
+ <iyy>2.2</iyy><iyz>0</iyz><izz>2.2</izz>
81
+ </inertia>
82
+ </inertial>
83
+ <collision name="collision">
84
+ <geometry>
85
+ <box><size>1.0 0.6 0.6</size></box>
86
+ </geometry>
87
+ </collision>
88
+ <visual name="visual">
89
+ <geometry>
90
+ <box><size>1.0 0.6 0.6</size></box>
91
+ </geometry>
92
+ <material>
93
+ <ambient>0.5 0.5 0.5 1</ambient>
94
+ <diffuse>0.7 0.7 0.2 1</diffuse>
95
+ </material>
96
+ </visual>
97
+ </link>
98
+ </model>
99
+ </sdf>
100
+ """
101
+
102
+ MODELS = {
103
+ "wooden_crate": _WOODEN_CRATE_SDF,
104
+ "barrel": _BARREL_SDF,
105
+ "cargo_box": _CARGO_BOX_SDF,
106
+ }
107
+
108
+
109
+ def get_inline_sdf(model_name: str, entity_name: str) -> str:
110
+ """Return inline SDF string for known models, formatted with entity name."""
111
+ template = MODELS.get(model_name, _WOODEN_CRATE_SDF)
112
+ return template.format(name=entity_name).strip()
@@ -0,0 +1,7 @@
1
+ """Part 3: container runtime, display/GPU selection, control API, Nav2 launch.
2
+
3
+ Host side: platform, hardware_probe, display, docker_runner, doctor, control_api.client
4
+ Container side: control_api.server (FastAPI on :9090, needs the `sim` extra)
5
+
6
+ Keep this module import-light: the TUI imports it on startup.
7
+ """
@@ -0,0 +1,7 @@
1
+ """Control API: FastAPI server inside the sim container, async client on the host.
2
+
3
+ Versioned under /v1. Import `client` on the host; `server` needs fastapi/uvicorn
4
+ (present in the sim image, or `pip install -e .[sim]`).
5
+ """
6
+
7
+ API_PREFIX = "/v1"
@@ -0,0 +1,187 @@
1
+ """
2
+ Async control API client. Runs on the HOST (TUI / agent tools / doctor).
3
+ File: lang2sim/runtime/control_api/client.py
4
+
5
+ async with ControlClient() as api:
6
+ await api.wait_until_ready(timeout=60)
7
+ await api.sim_start(SimStartRequest(...))
8
+ print(await api.sim_status())
9
+
10
+ Uses httpx; pass `transport=httpx.ASGITransport(app)` in tests to talk to the
11
+ server in-process.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import importlib
18
+ import time
19
+ from typing import Any, AsyncIterator, Dict, List, Optional, Sequence
20
+
21
+ import httpx
22
+
23
+ from lang2sim.core.models import (
24
+ NavGoalRequest,
25
+ NavInitialPoseRequest,
26
+ NavStatus,
27
+ NavWaypointsRequest,
28
+ Pose2D,
29
+ SimStartRequest,
30
+ SimStatus,
31
+ SimStatusResponse,
32
+ SimStopRequest,
33
+ )
34
+ from lang2sim.runtime.control_api import API_PREFIX
35
+
36
+ # NB: `from lang2sim.core import config` yields the Config *instance* (core/__init__
37
+ # re-exports it under the module's name), so load the module explicitly.
38
+ cfg = importlib.import_module("lang2sim.core.config")
39
+
40
+
41
+ class ControlAPIError(RuntimeError):
42
+ def __init__(self, message: str, status_code: Optional[int] = None):
43
+ super().__init__(message)
44
+ self.status_code = status_code
45
+
46
+
47
+ class ControlAPIUnavailable(ControlAPIError):
48
+ """Container not up / API not listening yet."""
49
+
50
+
51
+ def default_base_url() -> str:
52
+ return f"http://127.0.0.1:{cfg.DEFAULT_CONTROL_PORT}"
53
+
54
+
55
+ class ControlClient:
56
+ def __init__(self, base_url: Optional[str] = None, timeout: float = 10.0,
57
+ transport: Optional[httpx.AsyncBaseTransport] = None):
58
+ self.base_url = (base_url or default_base_url()).rstrip("/")
59
+ self._http = httpx.AsyncClient(base_url=self.base_url + API_PREFIX,
60
+ timeout=timeout, transport=transport)
61
+
62
+ async def __aenter__(self) -> "ControlClient":
63
+ return self
64
+
65
+ async def __aexit__(self, *exc: Any) -> None:
66
+ await self.aclose()
67
+
68
+ async def aclose(self) -> None:
69
+ await self._http.aclose()
70
+
71
+ async def _request(self, method: str, path: str, **kw: Any) -> Any:
72
+ try:
73
+ r = await self._http.request(method, path, **kw)
74
+ except (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError,
75
+ httpx.ConnectTimeout) as e:
76
+ raise ControlAPIUnavailable(f"control API not reachable at {self.base_url}: {e}") from e
77
+ if r.status_code >= 400:
78
+ try:
79
+ detail = r.json().get("detail", r.text)
80
+ except ValueError:
81
+ detail = r.text
82
+ raise ControlAPIError(f"{method} {path} -> {r.status_code}: {detail}", r.status_code)
83
+ return r.json()
84
+
85
+ # --- endpoints -------------------------------------------------------
86
+ async def health(self) -> Dict[str, Any]:
87
+ return await self._request("GET", "/health")
88
+
89
+ async def info(self) -> Dict[str, Any]:
90
+ return await self._request("GET", "/info")
91
+
92
+ async def sim_status(self) -> SimStatusResponse:
93
+ return SimStatusResponse.model_validate(await self._request("GET", "/sim/status"))
94
+
95
+ async def sim_start(self, req: SimStartRequest) -> SimStatusResponse:
96
+ data = await self._request("POST", "/sim/start", json=req.model_dump(mode="json"))
97
+ return SimStatusResponse.model_validate(data)
98
+
99
+ async def sim_stop(self, force: bool = False) -> SimStatusResponse:
100
+ data = await self._request("POST", "/sim/stop",
101
+ json=SimStopRequest(force=force).model_dump(mode="json"))
102
+ return SimStatusResponse.model_validate(data)
103
+
104
+ async def sim_logs(self, tail: int = 200) -> List[str]:
105
+ return (await self._request("GET", "/sim/logs", params={"tail": tail}))["lines"]
106
+
107
+ # --- Part 4 world mutation endpoints ---------------------------------
108
+ async def world_spawn(self, req: Dict[str, Any]) -> Dict[str, Any]:
109
+ return await self._request("POST", "/world/spawn", json=req)
110
+
111
+ async def world_light(self, req: Dict[str, Any]) -> Dict[str, Any]:
112
+ return await self._request("POST", "/world/light", json=req)
113
+
114
+ async def world_reset(self, req: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
115
+ return await self._request("POST", "/world/reset", json=req or {})
116
+
117
+ async def world_chassis(self, req: Dict[str, Any]) -> Dict[str, Any]:
118
+ return await self._request("POST", "/world/chassis", json=req)
119
+
120
+ async def world_health(self) -> Dict[str, Any]:
121
+ return await self._request("GET", "/world/health")
122
+
123
+ async def world_entities(self) -> Dict[str, Any]:
124
+ return await self._request("GET", "/world/entities")
125
+
126
+ # --- navigation and telemetry -----------------------------------------------
127
+ async def nav_initial_pose(self, pose: Pose2D) -> NavStatus:
128
+ data = await self._request("POST", "/nav/initial_pose",
129
+ json=NavInitialPoseRequest(pose=pose).model_dump(mode="json"))
130
+ return NavStatus.model_validate(data)
131
+
132
+ async def nav_goal(self, goal: Pose2D, timeout_seconds: float = 120.0) -> NavStatus:
133
+ body = NavGoalRequest(goal_pose=goal, timeout_seconds=timeout_seconds).model_dump(mode="json")
134
+ return NavStatus.model_validate(await self._request("POST", "/nav/goal", json=body))
135
+
136
+ async def nav_waypoints(self, waypoints: Sequence[Pose2D], timeout_seconds: float = 180.0) -> NavStatus:
137
+ body = NavWaypointsRequest(waypoints=list(waypoints), timeout_seconds=timeout_seconds).model_dump(mode="json")
138
+ return NavStatus.model_validate(await self._request("POST", "/nav/waypoints", json=body))
139
+
140
+ async def nav_cancel(self) -> NavStatus:
141
+ return NavStatus.model_validate(await self._request("POST", "/nav/cancel"))
142
+
143
+ async def nav_status(self) -> NavStatus:
144
+ return NavStatus.model_validate(await self._request("GET", "/nav/status"))
145
+
146
+ async def telemetry(self) -> SimStatus:
147
+ return SimStatus.model_validate(await self._request("GET", "/telemetry"))
148
+
149
+ async def nav_feedback(self, hz: float = 5.0) -> AsyncIterator[Dict[str, Any]]:
150
+ """Stream {"nav": NavStatus, "telemetry": SimStatus, "sim": {...}} dicts from WS /v1/nav/feedback.
151
+
152
+ async for msg in api.nav_feedback():
153
+ print(msg["nav"]["distance_remaining"])
154
+ """
155
+ import json
156
+
157
+ import websockets
158
+
159
+ url = self.base_url.replace("http", "ws", 1) + API_PREFIX + f"/nav/feedback?hz={hz}"
160
+ try:
161
+ async with websockets.connect(url) as ws:
162
+ async for raw in ws:
163
+ yield json.loads(raw)
164
+ except OSError as e:
165
+ raise ControlAPIUnavailable(f"feedback stream not reachable at {url}: {e}") from e
166
+
167
+ async def wait_nav_done(self, poll_s: float = 1.0, timeout: float = 1800.0) -> NavStatus:
168
+ """Poll /nav/status until the current task is no longer active."""
169
+ deadline = time.monotonic() + timeout
170
+ while True:
171
+ st = await self.nav_status()
172
+ if not st.is_active or time.monotonic() > deadline:
173
+ return st
174
+ await asyncio.sleep(poll_s)
175
+
176
+ # --- helpers ---------------------------------------------------------
177
+ async def wait_until_ready(self, timeout: float = 60.0, interval: float = 0.5) -> Dict[str, Any]:
178
+ """Poll /health until it answers (container boot + uvicorn start)."""
179
+ deadline = time.monotonic() + timeout
180
+ last: Optional[Exception] = None
181
+ while time.monotonic() < deadline:
182
+ try:
183
+ return await self.health()
184
+ except ControlAPIUnavailable as e:
185
+ last = e
186
+ await asyncio.sleep(interval)
187
+ raise ControlAPIUnavailable(f"control API not ready after {timeout:.0f}s ({last})")