sw2robot 0.4.1__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 (84) hide show
  1. sw2robot/__init__.py +37 -0
  2. sw2robot/editor/__init__.py +21 -0
  3. sw2robot/editor/__main__.py +4 -0
  4. sw2robot/editor/_autolimits_cli.py +106 -0
  5. sw2robot/editor/_vendor/__init__.py +1 -0
  6. sw2robot/editor/_vendor/rc_config/__init__.py +26 -0
  7. sw2robot/editor/_vendor/rc_config/export.py +188 -0
  8. sw2robot/editor/_vendor/rc_config/gazebo_generator.py +121 -0
  9. sw2robot/editor/_vendor/rc_config/imitation_generator.py +123 -0
  10. sw2robot/editor/_vendor/rc_config/moveit_generator.py +124 -0
  11. sw2robot/editor/_vendor/rc_config/servo_mapping.py +100 -0
  12. sw2robot/editor/_vendor/rc_config/urdf_parser.py +160 -0
  13. sw2robot/editor/autoinit.py +82 -0
  14. sw2robot/editor/cli.py +53 -0
  15. sw2robot/editor/core.py +894 -0
  16. sw2robot/editor/package_uri.py +82 -0
  17. sw2robot/editor/state.py +107 -0
  18. sw2robot/editor/update.py +415 -0
  19. sw2robot/editor/web/axis-markers.js +309 -0
  20. sw2robot/editor/web/batch-look.js +311 -0
  21. sw2robot/editor/web/boot.js +168 -0
  22. sw2robot/editor/web/bootstrap.js +72 -0
  23. sw2robot/editor/web/box-select.js +142 -0
  24. sw2robot/editor/web/bulk-edit.js +234 -0
  25. sw2robot/editor/web/camera-reroot.js +234 -0
  26. sw2robot/editor/web/capture-progress.js +362 -0
  27. sw2robot/editor/web/coacd-preview.js +315 -0
  28. sw2robot/editor/web/diagnostics.js +154 -0
  29. sw2robot/editor/web/dom.js +29 -0
  30. sw2robot/editor/web/endcoords-gizmo.js +408 -0
  31. sw2robot/editor/web/export-box.js +149 -0
  32. sw2robot/editor/web/export-names.js +95 -0
  33. sw2robot/editor/web/face-pick.js +524 -0
  34. sw2robot/editor/web/file-browser.js +265 -0
  35. sw2robot/editor/web/frames.js +306 -0
  36. sw2robot/editor/web/i18n.js +1027 -0
  37. sw2robot/editor/web/index.html +610 -0
  38. sw2robot/editor/web/joint-rows.js +127 -0
  39. sw2robot/editor/web/joint-type.js +300 -0
  40. sw2robot/editor/web/keycast.js +92 -0
  41. sw2robot/editor/web/link-info.js +377 -0
  42. sw2robot/editor/web/link-look.js +248 -0
  43. sw2robot/editor/web/lists.js +164 -0
  44. sw2robot/editor/web/load.js +309 -0
  45. sw2robot/editor/web/main.js +76 -0
  46. sw2robot/editor/web/mass-editor.js +280 -0
  47. sw2robot/editor/web/mass-heatmap.js +213 -0
  48. sw2robot/editor/web/mimic.js +159 -0
  49. sw2robot/editor/web/play-mode.js +180 -0
  50. sw2robot/editor/web/root-frame.js +161 -0
  51. sw2robot/editor/web/selection.js +391 -0
  52. sw2robot/editor/web/session-log.js +277 -0
  53. sw2robot/editor/web/state.js +128 -0
  54. sw2robot/editor/web/style.css +433 -0
  55. sw2robot/editor/web/subassembly-choices.js +613 -0
  56. sw2robot/editor/web/subassembly-preview.js +381 -0
  57. sw2robot/editor/web/three-setup.js +27 -0
  58. sw2robot/editor/web/tree.js +586 -0
  59. sw2robot/editor/webserver.py +7750 -0
  60. sw2robot/exporter/__init__.py +1 -0
  61. sw2robot/exporter/build.py +43 -0
  62. sw2robot/exporter/export.py +871 -0
  63. sw2robot/exporter/extract.py +62 -0
  64. sw2robot/exporter/geometry.py +135 -0
  65. sw2robot/exporter/inertia.py +56 -0
  66. sw2robot/exporter/jointcfg.py +170 -0
  67. sw2robot/exporter/mesh.py +936 -0
  68. sw2robot/exporter/model.py +4639 -0
  69. sw2robot/exporter/ros_export.py +1571 -0
  70. sw2robot/exporter/state.py +210 -0
  71. sw2robot/exporter/sw2urdf_import/__init__.py +38 -0
  72. sw2robot/exporter/sw2urdf_import/_common.py +162 -0
  73. sw2robot/exporter/sw2urdf_import/bridge.py +598 -0
  74. sw2robot/exporter/sw2urdf_import/geometric.py +318 -0
  75. sw2robot/exporter/sw2urdf_import/naming.py +255 -0
  76. sw2robot/exporter/sw2urdf_import/payload.py +484 -0
  77. sw2robot/exporter/swcom.py +907 -0
  78. sw2robot/exporter/urdf_writer.py +845 -0
  79. sw2robot/exporter/validate.py +236 -0
  80. sw2robot-0.4.1.dist-info/METADATA +452 -0
  81. sw2robot-0.4.1.dist-info/RECORD +84 -0
  82. sw2robot-0.4.1.dist-info/WHEEL +4 -0
  83. sw2robot-0.4.1.dist-info/entry_points.txt +4 -0
  84. sw2robot-0.4.1.dist-info/licenses/LICENSE +202 -0
sw2robot/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """SolidWorks assembly -> URDF, plus a browser editor.
2
+
3
+ Sub-packages:
4
+ - ``sw2robot.exporter`` -- the exporter (SolidWorks .sldasm -> graph.json -> URDF)
5
+ - ``sw2robot.editor`` -- the browser editor on top of the graph
6
+ """
7
+
8
+
9
+ def _detect_version() -> str:
10
+ """The running version, single-sourced from ``pyproject.toml``.
11
+
12
+ Order: installed package metadata (works in a wheel/pip install AND in the
13
+ frozen .exe, which bundles the dist-info via build_exe.py's
14
+ ``--copy-metadata sw2robot``) -> the repo's pyproject.toml (a bare source
15
+ checkout that was never installed, e.g. the test run with pythonpath=".")
16
+ -> a sentinel. The self-update check (``sw2robot.editor.update``) compares
17
+ this against the latest GitHub Release tag, so it must reflect the build."""
18
+ try:
19
+ from importlib.metadata import PackageNotFoundError, version
20
+ try:
21
+ return version("sw2robot")
22
+ except PackageNotFoundError:
23
+ pass
24
+ except Exception:
25
+ pass
26
+ try:
27
+ import os
28
+ import tomllib
29
+ pp = os.path.join(os.path.dirname(os.path.dirname(__file__)),
30
+ "pyproject.toml")
31
+ with open(pp, "rb") as f:
32
+ return tomllib.load(f)["project"]["version"]
33
+ except Exception:
34
+ return "0.0.0+unknown"
35
+
36
+
37
+ __version__ = _detect_version()
@@ -0,0 +1,21 @@
1
+ """sw2robot.editor -- the headless core bridging sw2robot.exporter
2
+ (CAD->module) and the ROS config export.
3
+
4
+ Design (a standing directive):
5
+ - **State**: ``RobotCompilerState`` (Pydantic) is the single source of truth for
6
+ a CAD-derived module being configured. It serializes to JSON, so it moves to
7
+ a REST/WebSocket payload unchanged.
8
+ - **Core API** (``core``): pure functions that mutate State / produce artifacts.
9
+ A web-editor handler or a FastAPI endpoint is a *thin* wrapper over these.
10
+ - **Headless**: the whole pipeline (CAD build -> edit -> ROS export) runs from
11
+ the CLI (``python -m sw2robot.editor``) with no GUI and no SolidWorks.
12
+
13
+ It uses the *pure* halves: ``sw2robot.exporter.export.build`` (no SolidWorks)
14
+ and the vendored ``sw2robot.editor._vendor.rc_config`` (ROS/MoveIt/Gazebo
15
+ config generators).
16
+ """
17
+
18
+ from . import core
19
+ from .state import JointEdit, RobotCompilerState
20
+
21
+ __all__ = ["JointEdit", "RobotCompilerState", "core"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,106 @@
1
+ """Self-collision joint-limit sweep as a standalone subprocess (JSON on stdout).
2
+
3
+ The webserver shells out to this instead of sweeping in-process: the sweep is
4
+ CPU-bound and releases the GIL (numpy / python-fcl), so running it inside the
5
+ threaded HTTP server makes it thrash the GIL against the browser's idle
6
+ keep-alive connection threads -- the classic CPython convoy, which inflated an
7
+ 8 s sweep to ~90 s just by having a page open. A fresh process has its own GIL
8
+ and no server threads, so it stays at the true ~8 s (+~3 s to load the model).
9
+
10
+ python -m sw2robot.editor._autolimits_cli <urdf> <step_deg> <max_deg> \
11
+ [margin_deg] [margin_mm]
12
+ -> stdout: {"results": [{"child","lower","upper","continuous"}, ...]}
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ import tempfile
22
+
23
+
24
+ def _fast_urdf(urdf):
25
+ """Return a URDF path whose mesh refs prefer the ``.3dxml.glb`` caches.
26
+
27
+ skrobot loads the SolidWorks ``.3dxml`` (mm, slow XML parse -- the heavy
28
+ ones are ~135 ms each and dominate load) and scales mm->m. The web server
29
+ already converts each to a ``.3dxml.glb`` (metres) on package open; loading
30
+ those is ~5x faster and skrobot reads them at native scale, so the geometry
31
+ is identical. We rewrite the URDF to point at the .glb wherever the cache
32
+ exists (falling back to .3dxml otherwise) and write it beside the original
33
+ so the ``../meshes`` relative paths still resolve. Returns (path, is_temp).
34
+ """
35
+ txt = open(urdf, encoding="utf-8").read()
36
+ base = os.path.dirname(urdf)
37
+
38
+ def repl(m):
39
+ fn = m.group(1)
40
+ full = os.path.normpath(os.path.join(base, fn))
41
+ return f'filename="{fn}.glb"' if os.path.exists(full + ".glb") \
42
+ else m.group(0)
43
+
44
+ txt2 = re.sub(r'filename="([^"]+\.3dxml)"', repl, txt)
45
+ if txt2 == txt:
46
+ return urdf, False
47
+ fd, tmp = tempfile.mkstemp(suffix=".urdf", dir=base)
48
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
49
+ f.write(txt2)
50
+ return tmp, True
51
+
52
+
53
+ def _emit(**ev):
54
+ """One progress event as a JSON line on STDERR (stdout is reserved for the
55
+ final results JSON the caller parses). The webserver reads these live to
56
+ drive the UI's per-joint progress bar."""
57
+ sys.stderr.write(json.dumps(ev) + "\n")
58
+ sys.stderr.flush()
59
+
60
+
61
+ def main():
62
+ urdf, step_deg, max_deg = sys.argv[1], float(sys.argv[2]), float(sys.argv[3])
63
+ # optional backoff margins: degrees (revolute), mm (prismatic)
64
+ margin_deg = float(sys.argv[4]) if len(sys.argv) > 4 else 2.0
65
+ margin_mm = float(sys.argv[5]) if len(sys.argv) > 5 else 2.0
66
+ from skrobot.models.urdf import RobotModelFromURDF
67
+
68
+ from sw2robot.editor import autoinit
69
+
70
+ _emit(event="loading") # model load is the slow first ~2 s
71
+ # skrobot (>=0.3.16) resolves package:// meshes itself, so load the URDF
72
+ # (glb-accelerated) directly -- no pre-resolved temp copy needed.
73
+ load_urdf, is_tmp = _fast_urdf(urdf)
74
+ try:
75
+ robot = RobotModelFromURDF(urdf_file=load_urdf)
76
+ finally:
77
+ if is_tmp:
78
+ try:
79
+ os.remove(load_urdf)
80
+ except OSError:
81
+ pass
82
+ meshes = autoinit.link_meshes(robot)
83
+
84
+ total = sum(1 for j in robot.joint_list
85
+ if type(j).__name__ in ("RotationalJoint", "LinearJoint"))
86
+ _emit(event="start", total=total)
87
+
88
+ def on_start(name, i, n):
89
+ # emit BEFORE each joint's sweep so the bar advances and names the joint
90
+ # currently being worked (a single joint can take seconds on a big model)
91
+ _emit(event="joint", i=i, total=n, joint=name)
92
+
93
+ results = autoinit.sweep_limits(
94
+ robot, meshes, step_deg=step_deg, max_deg=max_deg,
95
+ margin_deg=margin_deg, margin_mm=margin_mm,
96
+ refine=True, on_start=on_start)
97
+ out = [{"child": v["child"], "lower": v["lower"], "upper": v["upper"],
98
+ "continuous": v["continuous"]}
99
+ for v in results.values() if v.get("child")]
100
+ # ONLY the JSON goes to stdout (skrobot's URDF warnings go to stderr)
101
+ sys.stdout.write(json.dumps({"results": out}))
102
+ sys.stdout.flush()
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
@@ -0,0 +1 @@
1
+ """Vendored third-party code (kept verbatim)."""
@@ -0,0 +1,26 @@
1
+ """
2
+ Configuration file generators for robot URDFs.
3
+
4
+ This module provides generators for:
5
+ - MoveIt2 configuration (SRDF, controllers, etc.)
6
+ - Gazebo configuration (world, plugins, etc.)
7
+ - Imitation Learning configuration (observation/action spaces)
8
+ - Servo mapping configuration (joint to servo ID mapping)
9
+ """
10
+
11
+ from .export import export_all_configs
12
+ from .gazebo_generator import generate_gazebo_config
13
+ from .imitation_generator import generate_il_config
14
+ from .moveit_generator import generate_controllers_yaml, generate_srdf
15
+ from .servo_mapping import generate_servo_mapping_yaml
16
+ from .urdf_parser import parse_urdf_content
17
+
18
+ __all__ = [
19
+ "export_all_configs",
20
+ "generate_controllers_yaml",
21
+ "generate_gazebo_config",
22
+ "generate_il_config",
23
+ "generate_servo_mapping_yaml",
24
+ "generate_srdf",
25
+ "parse_urdf_content",
26
+ ]
@@ -0,0 +1,188 @@
1
+ """
2
+ Configuration export utilities.
3
+
4
+ Creates ZIP archives containing all configuration files.
5
+ """
6
+
7
+ import io
8
+ import zipfile
9
+ from typing import Any
10
+
11
+ from .gazebo_generator import generate_gazebo_config, generate_ros2_control_xacro
12
+ from .imitation_generator import generate_il_config
13
+ from .moveit_generator import generate_controllers_yaml, generate_srdf
14
+ from .servo_mapping import generate_servo_mapping_yaml
15
+
16
+
17
+ def export_all_configs(
18
+ urdf_content: str,
19
+ joints: list[dict[str, Any]],
20
+ servo_mappings: list[dict[str, Any]],
21
+ planning_groups: list[dict[str, Any]],
22
+ controllers: list[dict[str, Any]],
23
+ disabled_collision_pairs: list[tuple[str, str]],
24
+ gazebo_physics: dict[str, Any],
25
+ gazebo_plugins: list[dict[str, Any]],
26
+ il_observation: dict[str, Any],
27
+ il_action: dict[str, Any],
28
+ robot_name: str = "robot",
29
+ export_options: dict[str, bool] | None = None,
30
+ ) -> bytes:
31
+ """
32
+ Export all configuration files as a ZIP archive.
33
+
34
+ Parameters
35
+ ----------
36
+ urdf_content : str
37
+ Original URDF content.
38
+ joints : list
39
+ Parsed joint information.
40
+ servo_mappings : list
41
+ Servo mapping configurations.
42
+ planning_groups : list
43
+ MoveIt planning group configurations.
44
+ controllers : list
45
+ Controller configurations.
46
+ disabled_collision_pairs : list
47
+ List of disabled collision link pairs.
48
+ gazebo_physics : dict
49
+ Gazebo physics settings.
50
+ gazebo_plugins : list
51
+ Gazebo plugin configurations.
52
+ il_observation : dict
53
+ IL observation space configuration.
54
+ il_action : dict
55
+ IL action space configuration.
56
+ robot_name : str
57
+ Name of the robot for file naming.
58
+
59
+ Returns
60
+ -------
61
+ bytes
62
+ ZIP file contents as bytes.
63
+ """
64
+ # Default export options if not provided
65
+ if export_options is None:
66
+ export_options = {
67
+ "includeUrdf": True,
68
+ "includeServoMapping": True,
69
+ "includeMoveIt": True,
70
+ "includeGazebo": True,
71
+ "includeImitationLearning": True,
72
+ }
73
+
74
+ # Create in-memory ZIP file
75
+ zip_buffer = io.BytesIO()
76
+
77
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
78
+ # URDF (original)
79
+ if export_options.get("includeUrdf", True):
80
+ zf.writestr(f"{robot_name}/urdf/{robot_name}.urdf", urdf_content)
81
+
82
+ # Servo mapping
83
+ if export_options.get("includeServoMapping", True):
84
+ servo_yaml = generate_servo_mapping_yaml(servo_mappings)
85
+ zf.writestr(f"{robot_name}/config/servo_mapping.yaml", servo_yaml)
86
+
87
+ # MoveIt SRDF and controllers
88
+ if export_options.get("includeMoveIt", True):
89
+ srdf_content = generate_srdf(
90
+ robot_name=robot_name,
91
+ planning_groups=planning_groups,
92
+ disabled_collision_pairs=[tuple(p) for p in disabled_collision_pairs],
93
+ )
94
+ zf.writestr(f"{robot_name}/config/{robot_name}.srdf", srdf_content)
95
+
96
+ controllers_yaml = generate_controllers_yaml(controllers)
97
+ zf.writestr(f"{robot_name}/config/controllers.yaml", controllers_yaml)
98
+
99
+ # Gazebo config
100
+ if export_options.get("includeGazebo", True):
101
+ gazebo_config = generate_gazebo_config(gazebo_physics, gazebo_plugins)
102
+ zf.writestr(f"{robot_name}/config/gazebo.xml", gazebo_config)
103
+
104
+ ros2_control_xacro = generate_ros2_control_xacro(joints)
105
+ zf.writestr(f"{robot_name}/urdf/ros2_control.xacro", ros2_control_xacro)
106
+
107
+ # IL config
108
+ if export_options.get("includeImitationLearning", True):
109
+ il_config = generate_il_config(il_observation, il_action, joints)
110
+ zf.writestr(f"{robot_name}/config/il_config.yaml", il_config)
111
+
112
+ # README (always include)
113
+ readme = _generate_readme(robot_name, export_options)
114
+ zf.writestr(f"{robot_name}/README.md", readme)
115
+
116
+ return zip_buffer.getvalue()
117
+
118
+
119
+ def _generate_readme(robot_name: str, export_options: dict[str, bool]) -> str:
120
+ """Generate a README file for the configuration package."""
121
+ contents = []
122
+ usage_sections = []
123
+
124
+ if export_options.get("includeUrdf", True):
125
+ contents.append(f"- `urdf/{robot_name}.urdf` - Robot URDF description")
126
+
127
+ if export_options.get("includeGazebo", True):
128
+ contents.append("- `urdf/ros2_control.xacro` - ros2_control hardware interface configuration")
129
+ contents.append("- `config/gazebo.xml` - Gazebo physics and plugin configuration")
130
+ usage_sections.append(f"""### Gazebo
131
+
132
+ Include the ros2_control xacro in your robot description:
133
+
134
+ ```xml
135
+ <xacro:include filename="$(find {robot_name})/urdf/ros2_control.xacro" />
136
+ ```""")
137
+
138
+ if export_options.get("includeServoMapping", True):
139
+ contents.append("- `config/servo_mapping.yaml` - Joint to servo ID mapping")
140
+
141
+ if export_options.get("includeMoveIt", True):
142
+ contents.append(f"- `config/{robot_name}.srdf` - MoveIt2 SRDF (semantic robot description)")
143
+ contents.append("- `config/controllers.yaml` - ros2_control controller configuration")
144
+ usage_sections.append(f"""### MoveIt2
145
+
146
+ Include the SRDF in your MoveIt2 launch files:
147
+
148
+ ```python
149
+ srdf_file = os.path.join(pkg_share, 'config', '{robot_name}.srdf')
150
+ ```
151
+
152
+ ### Controllers
153
+
154
+ Load the controller configuration:
155
+
156
+ ```yaml
157
+ ros2_control_node:
158
+ ros__parameters:
159
+ robot_description: $(command 'cat $(find {robot_name})/urdf/{robot_name}.urdf')
160
+ ```""")
161
+
162
+ if export_options.get("includeImitationLearning", True):
163
+ contents.append("- `config/il_config.yaml` - Imitation Learning configuration")
164
+ usage_sections.append("""### Imitation Learning
165
+
166
+ Load the IL configuration in your training script:
167
+
168
+ ```python
169
+ import yaml
170
+ with open('config/il_config.yaml') as f:
171
+ config = yaml.safe_load(f)
172
+ ```""")
173
+
174
+ contents_str = "\n".join(contents)
175
+ usage_str = "\n\n".join(usage_sections)
176
+
177
+ return f"""# {robot_name} Configuration Package
178
+
179
+ Generated by Robot Compiler.
180
+
181
+ ## Contents
182
+
183
+ {contents_str}
184
+
185
+ ## Usage
186
+
187
+ {usage_str}
188
+ """
@@ -0,0 +1,121 @@
1
+ """
2
+ Gazebo configuration file generators.
3
+
4
+ Generates world and plugin configuration files.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+
10
+ def generate_gazebo_config(
11
+ physics: dict[str, Any],
12
+ plugins: list[dict[str, Any]],
13
+ ) -> str:
14
+ """
15
+ Generate Gazebo world configuration snippet.
16
+
17
+ Parameters
18
+ ----------
19
+ physics : dict
20
+ Physics configuration with:
21
+ - solver: Physics engine (ode, bullet, simbody, dart)
22
+ - stepSize: Simulation step size in seconds
23
+ - realTimeFactor: Real-time factor
24
+ plugins : list
25
+ List of plugin configurations with:
26
+ - name: Plugin name
27
+ - filename: Plugin library filename
28
+ - enabled: Whether plugin is enabled
29
+ - parameters: Dict of plugin parameters
30
+
31
+ Returns
32
+ -------
33
+ str
34
+ Gazebo SDF/world configuration snippet.
35
+ """
36
+ solver = physics.get("solver", "ode")
37
+ step_size = physics.get("stepSize", 0.001)
38
+ rtf = physics.get("realTimeFactor", 1.0)
39
+
40
+ lines = [
41
+ "<!-- Gazebo Configuration Generated by Robot Compiler -->",
42
+ "",
43
+ f'<physics name="default_physics" type="{solver}">',
44
+ f" <max_step_size>{step_size}</max_step_size>",
45
+ f" <real_time_factor>{rtf}</real_time_factor>",
46
+ f" <real_time_update_rate>{int(1 / step_size)}</real_time_update_rate>",
47
+ "</physics>",
48
+ "",
49
+ ]
50
+
51
+ # Plugins
52
+ for plugin in plugins:
53
+ if not plugin.get("enabled", True):
54
+ continue
55
+
56
+ name = plugin["name"]
57
+ filename = plugin["filename"]
58
+ params = plugin.get("parameters", {})
59
+
60
+ lines.append(f'<plugin filename="{filename}" name="{name}">')
61
+ for key, value in params.items():
62
+ lines.append(f" <{key}>{value}</{key}>")
63
+ lines.append("</plugin>")
64
+ lines.append("")
65
+
66
+ return "\n".join(lines)
67
+
68
+
69
+ def generate_ros2_control_xacro(
70
+ joints: list[dict[str, Any]],
71
+ hardware_interface: str = "gazebo_ros2_control/GazeboSystem",
72
+ ) -> str:
73
+ """
74
+ Generate ros2_control URDF/Xacro snippet for Gazebo.
75
+
76
+ Parameters
77
+ ----------
78
+ joints : list
79
+ List of joint configurations with:
80
+ - name: Joint name
81
+ - type: Joint type (revolute, prismatic, etc.)
82
+ hardware_interface : str
83
+ Hardware interface plugin name.
84
+
85
+ Returns
86
+ -------
87
+ str
88
+ ros2_control Xacro snippet.
89
+ """
90
+ lines = [
91
+ "<!-- ros2_control Configuration for Gazebo -->",
92
+ "<!-- Generated by Robot Compiler -->",
93
+ "",
94
+ '<ros2_control name="GazeboSystem" type="system">',
95
+ " <hardware>",
96
+ f" <plugin>{hardware_interface}</plugin>",
97
+ " </hardware>",
98
+ ]
99
+
100
+ for joint in joints:
101
+ if joint.get("type") == "fixed":
102
+ continue
103
+
104
+ name = joint["name"]
105
+ lines.append(f' <joint name="{name}">')
106
+ lines.append(' <command_interface name="position"/>')
107
+ lines.append(' <command_interface name="velocity"/>')
108
+ lines.append(' <state_interface name="position"/>')
109
+ lines.append(' <state_interface name="velocity"/>')
110
+ lines.append(' <state_interface name="effort"/>')
111
+ lines.append(" </joint>")
112
+
113
+ lines.append("</ros2_control>")
114
+ lines.append("")
115
+ lines.append("<gazebo>")
116
+ lines.append(' <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">')
117
+ lines.append(" <parameters>$(find robot_config)/config/controllers.yaml</parameters>")
118
+ lines.append(" </plugin>")
119
+ lines.append("</gazebo>")
120
+
121
+ return "\n".join(lines)
@@ -0,0 +1,123 @@
1
+ """
2
+ Imitation Learning configuration generators.
3
+
4
+ Generates configuration files for RL/IL training.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+
10
+ def generate_il_config(
11
+ observation: dict[str, Any],
12
+ action: dict[str, Any],
13
+ joints: list[dict[str, Any]],
14
+ ) -> str:
15
+ """
16
+ Generate Imitation Learning configuration YAML.
17
+
18
+ Parameters
19
+ ----------
20
+ observation : dict
21
+ Observation space configuration with:
22
+ - includeJointPositions: bool
23
+ - includeJointVelocities: bool
24
+ - includeEndEffectorPose: bool
25
+ - customObservations: list of custom observation names
26
+ action : dict
27
+ Action space configuration with:
28
+ - actionType: 'joint_positions', 'joint_velocities', or 'end_effector_delta'
29
+ - selectedJoints: list of joint names
30
+ - actionScale: float multiplier for actions
31
+ joints : list
32
+ List of joint configurations for dimension calculation.
33
+
34
+ Returns
35
+ -------
36
+ str
37
+ YAML configuration content.
38
+ """
39
+ # Calculate dimensions
40
+ actuated_joints = [j for j in joints if j.get("type") != "fixed"]
41
+ n_joints = len(actuated_joints)
42
+
43
+ obs_dim = 0
44
+ if observation.get("includeJointPositions", True):
45
+ obs_dim += n_joints
46
+ if observation.get("includeJointVelocities", True):
47
+ obs_dim += n_joints
48
+ if observation.get("includeEndEffectorPose", False):
49
+ obs_dim += 7 # position (3) + quaternion (4)
50
+
51
+ custom_obs = observation.get("customObservations", [])
52
+ obs_dim += len(custom_obs)
53
+
54
+ action_joints = action.get("selectedJoints", [])
55
+ action_dim = len(action_joints)
56
+
57
+ lines = [
58
+ "# Imitation Learning Configuration",
59
+ "# Generated by Robot Compiler",
60
+ "",
61
+ "robot:",
62
+ " observation_space:",
63
+ f" dim: {obs_dim}",
64
+ f" include_joint_positions: {str(observation.get('includeJointPositions', True)).lower()}",
65
+ f" include_joint_velocities: {str(observation.get('includeJointVelocities', True)).lower()}",
66
+ f" include_end_effector_pose: {str(observation.get('includeEndEffectorPose', False)).lower()}",
67
+ ]
68
+
69
+ if custom_obs:
70
+ lines.append(" custom_observations:")
71
+ for obs in custom_obs:
72
+ lines.append(f" - {obs}")
73
+
74
+ lines.extend(
75
+ [
76
+ "",
77
+ " action_space:",
78
+ f" dim: {action_dim}",
79
+ f" type: {action.get('actionType', 'joint_positions')}",
80
+ f" scale: {action.get('actionScale', 1.0)}",
81
+ " joints:",
82
+ ]
83
+ )
84
+
85
+ for joint in action_joints:
86
+ lines.append(f" - {joint}")
87
+
88
+ # Add joint limits for reference
89
+ lines.extend(
90
+ [
91
+ "",
92
+ " joint_limits:",
93
+ ]
94
+ )
95
+
96
+ for joint in actuated_joints:
97
+ if joint["name"] in action_joints:
98
+ lines.append(f" {joint['name']}:")
99
+ lines.append(f" lower: {joint.get('lowerLimit', 0.0)}")
100
+ lines.append(f" upper: {joint.get('upperLimit', 0.0)}")
101
+ lines.append(f" velocity: {joint.get('velocityLimit', 0.0)}")
102
+
103
+ # Add training hints
104
+ lines.extend(
105
+ [
106
+ "",
107
+ "# Training Configuration",
108
+ "training:",
109
+ " horizon: 1000",
110
+ " batch_size: 256",
111
+ " learning_rate: 3.0e-4",
112
+ " gamma: 0.99",
113
+ " clip_range: 0.2",
114
+ "",
115
+ "# Normalization",
116
+ "normalization:",
117
+ " normalize_observations: true",
118
+ " normalize_rewards: true",
119
+ " clip_observations: 10.0",
120
+ ]
121
+ )
122
+
123
+ return "\n".join(lines)