Skip to content
Open
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
89 changes: 89 additions & 0 deletions docs/source/features/toolkits/urdf_assembly.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,74 @@ 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 via
the optional `name_case` argument in the constructor:

```python
Comment on lines +248 to +252
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Docs show URDFAssemblyManager(name_case=...), but URDFAssemblyManager.__init__ in this PR does not accept a name_case argument. Either add the constructor parameter (as documented) or update the docs to the supported usage (manager.name_case = {...}) to avoid users hitting TypeError.

Copilot uses AI. Check for mistakes.
manager = URDFAssemblyManager(
name_case={
"joint": "upper", # or "lower" / "none"
"link": "lower", # or "upper" / "none"
}
)
```

Semantics:

- Valid keys: `"joint"`, `"link"`.
- Valid values: `"upper"`, `"lower"`, `"none"`.
- Default behavior matches the legacy implementation:
Comment on lines +246 to +265
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The documentation states that name_case is configured via a URDFAssemblyManager(..., name_case=...) constructor argument, but the current URDFAssemblyManager.__init__ signature does not accept this parameter. Update the docs to match the actual API, or implement the constructor argument as documented.

Suggested change
##### 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 via
the optional `name_case` argument in the constructor:
```python
manager = URDFAssemblyManager(
name_case={
"joint": "upper", # or "lower" / "none"
"link": "lower", # or "upper" / "none"
}
)
```
Semantics:
- Valid keys: `"joint"`, `"link"`.
- Valid values: `"upper"`, `"lower"`, `"none"`.
- Default behavior matches the legacy implementation:
##### Name casing policy
`URDFAssemblyManager` applies a global name casing policy that controls how
link and joint names are normalized during assembly.
Semantics:
- The policy applies separately to joint names and link names.
- Current default behavior (matching the legacy implementation):

Copilot uses AI. Check for mistakes.
- 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 Down Expand Up @@ -232,6 +300,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 Down
81 changes: 81 additions & 0 deletions embodichain/lab/sim/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,27 @@ 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),
Comment on lines +849 to +853
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

URDFCfg introduces configurable name_case via __init__, but it’s not declared as a config field like component_prefix. If URDFCfg is expected to be serializable/introspectable (as a @configclass), consider adding name_case as a field with a default matching legacy behavior.

Copilot uses AI. Check for mistakes.
("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.
"""

def __init__(
self,
components: list[dict[str, str | np.ndarray]] | None = None,
Expand All @@ -855,6 +876,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 +894,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 +906,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 +1097,27 @@ 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
else:
manager.name_case = {
"joint": "upper",
"link": "lower",
}

for comp_type, comp_config in components:
params = comp_config.get("params", {})
success = manager.add_component(
Expand Down Expand Up @@ -1094,12 +1171,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
12 changes: 12 additions & 0 deletions embodichain/lab/sim/utility/cfg_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ def merge_robot_cfg(base_cfg: RobotCfg, override_cfg_dict: dict[str, any]) -> Ro
urdf_path=component.get("urdf_path"),
transform=component.get("transform"),
)
for sensor in user_urdf_cfg.get("sensors", []):
base_cfg.urdf_cfg.add_sensor(
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

URDFCfg.add_sensor(...) requires a sensor_name positional argument, but this merge path calls add_sensor(sensor_type=..., urdf_path=..., ...) without providing it. This will raise a TypeError when override_cfg_dict['urdf_cfg']['sensors'] is present. Pass sensor_name=sensor.get('sensor_name') (or clearly define which field is the name) and forward the rest as kwargs.

Suggested change
base_cfg.urdf_cfg.add_sensor(
base_cfg.urdf_cfg.add_sensor(
sensor_name=sensor.get("sensor_name"),

Copilot uses AI. Check for mistakes.
sensor_type=sensor.get("sensor_type"),
urdf_path=sensor.get("urdf_path"),
transform=sensor.get("transform"),
)
component_prefix = user_urdf_cfg.get("component_prefix", None)
if component_prefix:
base_cfg.urdf_cfg.component_prefix = component_prefix
name_case = user_urdf_cfg.get("name_case", None)
if name_case:
Comment on lines +170 to +173
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

These merges use truthiness checks (if component_prefix: / if name_case:), which prevents explicitly setting empty-but-intentional values (e.g., component_prefix=[] meaning “no overrides”). Prefer is not None checks so empty lists/dicts are applied.

Suggested change
if component_prefix:
base_cfg.urdf_cfg.component_prefix = component_prefix
name_case = user_urdf_cfg.get("name_case", None)
if name_case:
if component_prefix is not None:
base_cfg.urdf_cfg.component_prefix = component_prefix
name_case = user_urdf_cfg.get("name_case", None)
if name_case is not None:

Copilot uses AI. Check for mistakes.
base_cfg.urdf_cfg.name_case = name_case
else:
logger.log_warning(
"urdf_cfg should be a dictionary. Skipping urdf_cfg merge."
Expand Down
Loading