Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 173 additions & 7 deletions docs/source/features/toolkits/urdf_assembly.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The tool provides a programmatic way to:
```python
from pathlib import Path
import numpy as np
from embedichain.toolkits.urdf_assembly import URDFAssemblyManager
from embodichain.toolkits.urdf_assembly import URDFAssemblyManager

# Initialize the assembly manager
manager = URDFAssemblyManager()
Expand Down Expand Up @@ -201,6 +201,72 @@ Get all attached sensors.
manager.get_attached_sensors() -> dict
```

##### Component name prefixes (`component_prefix`)

`URDFAssemblyManager` uses `component_prefix` to configure name prefixes for
each supported component type. This attribute is a list of 2-tuples:

- Form: `[(component_name, prefix), ...]`
- The default value is:

```python
[
("chassis", None),
("legs", None),
("torso", None),
("head", None),
("left_arm", "left_"),
("right_arm", "right_"),
("left_hand", "left_"),
("right_hand", "right_"),
("arm", None),
("hand", None),
]
```

You can configure it in a *patch-style* manner via the property:

```python
# Only override prefixes for existing components; do not introduce
# new component names.
manager.component_prefix = [
("left_arm", "L_"),
("right_arm", "R_"),
("left_hand", "L_"),
("right_hand", "R_"),
]
```

Semantics:

- Only components that already exist in the default configuration (e.g. `chassis/torso/left_arm/...`) may be overridden; new component names are not allowed.
- Components not listed in `new_prefixes` keep their original prefix.
- If `new_prefixes` contains an unknown component name, a `ValueError` is raised indicating that new component types cannot be introduced.

##### Name casing policy (`name_case`)

`URDFAssemblyManager` supports a global name casing policy that controls how
link and joint names are normalized during assembly. This is configured on
the manager instance after construction:

```python
manager = URDFAssemblyManager()
manager.name_case = {
"joint": "upper", # or "lower" / "none"
"link": "lower", # or "upper" / "none"
}

Semantics:

- Valid keys: `"joint"`, `"link"`.
- Valid values: `"upper"`, `"lower"`, `"none"`.
Comment on lines +252 to +262
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name_case example code block is missing its closing triple-backtick fence. As written, the subsequent “Semantics:” section is still inside the Python code block, which will break Markdown rendering (and can fail doc builds if the project lint/checks docs). Add a closing ``` after the manager.name_case = {...} snippet.

Copilot uses AI. Check for mistakes.
- Default behavior matches the legacy implementation:
- joints are normalized to **UPPERCASE**,
- links are normalized to **lowercase**.
- This policy is propagated to the internal component and connection managers,
and is also included in the assembly signature. Changing `name_case` will
therefore force a rebuild of the assembled URDF.

## Using with URDFCfg for Robot Creation

The URDF Assembly Tool can be used directly with `URDFCfg` to create robots with multiple components in the simulation. This is the recommended approach when building robots from assembled URDF files.
Expand All @@ -210,7 +276,7 @@ The URDF Assembly Tool can be used directly with `URDFCfg` to create robots with
The `URDFCfg` class provides a convenient way to define multi-component robots:

```python
from embedichain.lab.sim.cfg import RobotCfg, URDFCfg
from embodichain.lab.sim.cfg import RobotCfg, URDFCfg

cfg = RobotCfg(
uid="my_robot",
Expand All @@ -232,6 +298,27 @@ cfg = RobotCfg(
)
```

When using `URDFCfg` to build multi-component robots, you can pass custom
component prefixes to the internal `URDFAssemblyManager` via
`URDFCfg.component_prefix`. Its semantics are identical to
`URDFAssemblyManager.component_prefix`:

- Each element is a `(component_name, prefix)` tuple.
- Only prefixes for components that exist in the default configuration may be overridden; no new component names can be added.
- Components not explicitly listed keep their original prefix.

Example:

```python
urdf_cfg = URDFCfg(
components=[...],
)
urdf_cfg.component_prefix = [
("left_arm", "L_"),
("right_arm", "R_"),
]
```

### Complete Example

Here's a complete example from `scripts/tutorials/sim/create_robot.py`:
Expand All @@ -241,14 +328,14 @@ import numpy as np
import torch
from scipy.spatial.transform import Rotation as R

from embedichain.lab.sim import SimulationManager, SimulationManagerCfg
from embedichain.lab.sim.objects import Robot
from embedichain.lab.sim.cfg import (
from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
from embodichain.lab.sim.objects import Robot
from embodichain.lab.sim.cfg import (
JointDrivePropertiesCfg,
RobotCfg,
URDFCfg,
)
from embedichain.data import get_data_path
from embodichain.data import get_data_path


def create_robot(sim):
Expand All @@ -269,7 +356,6 @@ def create_robot(sim):
# Define transformation for hand attachment
hand_transform = np.eye(4)
hand_transform[:3, :3] = R.from_rotvec([90, 0, 0], degrees=True).as_matrix()
hand_transform[2, 3] = 0.02 # 2cm offset along z-axis

# Create robot configuration
cfg = RobotCfg(
Expand Down Expand Up @@ -300,6 +386,86 @@ def create_robot(sim):
return robot


# Initialize simulation and create robot
sim = SimulationManager(SimulationManagerCfg(headless=True, num_envs=4))
robot = create_robot(sim)
print(f"Robot created with {robot.dof} joints")
```

```python
import numpy as np
import torch
from scipy.spatial.transform import Rotation as R

from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
from embodichain.lab.sim.objects import Robot
from embodichain.lab.sim.cfg import (
JointDrivePropertiesCfg,
RobotCfg,
URDFCfg,
)
from embodichain.data import get_data_path


def create_robot(sim):
"""Create and configure a robot with arm and hand components."""

# Get URDF paths for robot components
arm_urdf_path = get_data_path("Rokae/SR5/SR5.urdf")
hand_urdf_path = get_data_path(
"BrainCoHandRevo1/BrainCoLeftHand/BrainCoLeftHand.urdf"
)

# Define transformation for hand attachment
hand_transform = np.eye(4)
hand_transform[:3, :3] = R.from_rotvec([90, 0, 0], degrees=True).as_matrix()

left_arm_base_xpos = np.eye(4)
left_arm_base_xpos[1, 3] = 0.3

right_arm_base_xpos = np.eye(4)
right_arm_base_xpos[1, 3] = -0.3

# Create robot configuration
cfg = RobotCfg(
uid="dual_sr5",
urdf_cfg=URDFCfg(
components=[
{
"component_type": "left_arm",
"urdf_path": arm_urdf_path,
"transform": left_arm_base_xpos,
},
{
"component_type": "right_arm",
"urdf_path": arm_urdf_path,
"transform": right_arm_base_xpos,
},
{
"component_type": "left_hand",
"urdf_path": hand_urdf_path,
"transform": hand_transform,
},
{
"component_type": "right_hand",
"urdf_path": hand_urdf_path,
"transform": hand_transform,
},
],
component_prefix=[("left_arm", "L_"), ("right_arm", "R_"), ("left_hand", "left_"), ("right_hand", "right_")],
name_case={
"joint": "lower",
"link": "lower",
}
),
Comment thread
chase6305 marked this conversation as resolved.
)

# Add robot to simulation
robot: Robot = sim.add_robot(cfg=cfg)

return robot


# Initialize simulation and create robot
sim = SimulationManager(SimulationManagerCfg(headless=True, num_envs=4))
robot = create_robot(sim)
Expand Down
83 changes: 83 additions & 0 deletions embodichain/lab/sim/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,34 @@ class URDFCfg:
fpath_prefix: str = EMBODICHAIN_DEFAULT_DATA_ROOT + "/assembled"
"""Output directory prefix for the assembled URDF file."""

component_prefix: List[tuple[str, Union[str, None]]] = field(
default_factory=lambda: [
("chassis", None),
("legs", None),
("torso", None),
("head", None),
("left_arm", "left_"),
("right_arm", "right_"),
("left_hand", "left_"),
("right_hand", "right_"),
("arm", None),
("hand", None),
]
)
"""Component name prefixes used during URDF assembly.

Preferred form is a list of ``(component_name, prefix)`` tuples. For
convenience, a mapping ``{component_name: prefix}`` is also accepted when
constructing :class:`URDFCfg` and will be normalized internally.
"""

name_case: dict[str, str] = field(
default_factory=lambda: {
"joint": "upper",
"link": "lower",
}
)

def __init__(
self,
components: list[dict[str, str | np.ndarray]] | None = None,
Expand All @@ -855,6 +883,8 @@ def __init__(
fpath_prefix: str = EMBODICHAIN_DEFAULT_DATA_ROOT + "/assembled",
use_signature_check: bool = True,
base_link_name: str = "base_link",
component_prefix: list[tuple[str, str | None]] | None = None,
name_case: dict[str, str] | None = None,
):
"""
Initialize URDFCfg with optional list of components and output path settings.
Expand All @@ -871,6 +901,9 @@ def __init__(
fpath_prefix (str): Output directory prefix for the assembled URDF file.
use_signature_check (bool): Whether to use signature check when merging URDFs.
base_link_name (str): Name of the base link in the assembled robot.
component_prefix (list[tuple[str, str | None]] | None): Optional
list of (component_type, prefix) pairs to override default
component name prefixes.
"""
self.components = {}
self.sensors = sensors or {}
Expand All @@ -880,6 +913,36 @@ def __init__(
self.fname = fname
self.fpath_prefix = fpath_prefix

# Initialize component prefixes (patch-style mapping per component type)
if component_prefix is None:
# Use the same default as the dataclass field
self.component_prefix = [
("chassis", None),
("legs", None),
("torso", None),
("head", None),
("left_arm", "left_"),
("right_arm", "right_"),
("left_hand", "left_"),
("right_hand", "right_"),
("arm", None),
("hand", None),
]
elif isinstance(component_prefix, dict):
# Allow dict-style config: {"left_hand": "l_", ...}
self.component_prefix = list(component_prefix.items())
else:
# Assume caller provided a list of (component_name, prefix) tuples
self.component_prefix = component_prefix

if name_case is None:
self.name_case = {
"joint": "upper",
"link": "lower",
}
else:
self.name_case = name_case

# Auto-add components if provided
if components:
for comp_config in components:
Expand Down Expand Up @@ -1041,6 +1104,22 @@ def assemble_urdf(self) -> str:
# If there are multiple components, merge them into a single URDF file.
manager = URDFAssemblyManager()
manager.base_link_name = self.base_link_name

if self.component_prefix is None:
self.component_prefix = [
("left_arm", "left_"),
("right_arm", "right_"),
("left_hand", "left_"),
("right_hand", "right_"),
]
if isinstance(self.component_prefix, dict):
self.component_prefix = list(self.component_prefix.items())
# Forward configured component prefixes to the assembly manager
manager.component_prefix = self.component_prefix

if self.name_case is not None:
manager.name_case = self.name_case

for comp_type, comp_config in components:
params = comp_config.get("params", {})
success = manager.add_component(
Expand Down Expand Up @@ -1094,12 +1173,16 @@ def from_dict(cls, init_dict: Dict) -> "URDFCfg":
fpath = init_dict.get("fpath", None)
use_signature_check = init_dict.get("use_signature_check", True)
base_link_name = init_dict.get("base_link_name", "base_link")
component_prefix = init_dict.get("component_prefix", None)
name_case = init_dict.get("name_case", None)
return cls(
components=components,
sensors=sensors,
fpath=fpath,
use_signature_check=use_signature_check,
base_link_name=base_link_name,
component_prefix=component_prefix,
name_case=name_case,
)


Expand Down
Loading
Loading