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,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
|
+
]
|
lang2sim/core/config.py
ADDED
|
@@ -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()
|