Refine URDF assembly component prefixes and name casing policy#236
Conversation
There was a problem hiding this comment.
Pull request overview
Refines the URDF assembly pipeline’s naming semantics by introducing configurable per-component prefixes, a global link/joint casing policy, and extending the assembly signature so naming-policy changes invalidate cached assemblies.
Changes:
- Add
component_prefixpatch-style configuration and include prefix/order +name_casein the signature inputs. - Introduce a reusable
NameNormalizerand propagate casing behavior into component/connection naming. - Update
URDFCfgand documentation to expose/describe the new configuration surface.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
embodichain/toolkits/urdf_assembly/urdf_assembly_manager.py |
Adds global name_case, per-component prefix patching, signature metadata injection, and material merging/fallbacks during merge. |
embodichain/toolkits/urdf_assembly/signature.py |
Extends signature payload to include component prefix/order and casing policy metadata. |
embodichain/toolkits/urdf_assembly/name_normalizer.py |
New helper for consistent link/joint name normalization. |
embodichain/toolkits/urdf_assembly/connection.py |
Refactors connection logic and normalizes names via NameNormalizer; improves transform formatting and sensor legacy attachment handling. |
embodichain/toolkits/urdf_assembly/component.py |
Applies configurable casing policy when generating/rewriting link and joint names. |
embodichain/lab/sim/cfg.py |
Adds URDFCfg.component_prefix and URDFCfg.name_case and forwards them into URDFAssemblyManager. |
embodichain/lab/sim/robots/cobotmagic.py |
Minor formatting fix (trailing commas). |
docs/source/features/toolkits/urdf_assembly.md |
Documents component_prefix and name_case and updates import paths/examples. |
Comments suppressed due to low confidence (1)
embodichain/toolkits/urdf_assembly/component.py:326
_generate_unique_name()checksif new_name in existing_namesbefore the caller applies the configured case policy. Sinceexisting_namesis built from already-normalized link/joint names, butnew_namehere is not normalized, collisions can be missed (e.g. existingleft_linkvs generatedLeft_Link), and after_apply_case()the final written name can collide. Normalizebase_name/new_nameusing the same case policy (or normalizeexisting_namesconsistently) before performing uniqueness checks and suffixing.
# For uniqueness checks we always operate on a normalized form that is
# consistent with the link case policy. This keeps collisions and
# generated names aligned with how names are written back to the URDF.
base_name = orig_name
if prefix and not orig_name.lower().startswith(prefix.lower()):
base_name = f"{prefix}{orig_name}"
new_name = base_name
# Ensure the new name is unique
if new_name in existing_names:
counter = 1
unique_name = f"{new_name}_{counter}"
while unique_name in existing_names:
counter += 1
unique_name = f"{new_name}_{counter}"
new_name = unique_name
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
embodichain/toolkits/urdf_assembly/component.py:324
_generate_unique_name()checks uniqueness againstexisting_namesusing the rawnew_name(which may differ in case), but callers later apply_apply_case()before writing names back into the URDF. With non-defaultname_case, this can generate duplicates (e.g.,new_name='Left_Foo'not found in{'left_foo'}, then_apply_case('link', ...)producesleft_foowhich collides). Normalize the candidate name to the relevant case policy before the uniqueness checks (or passkindinto_generate_unique_nameso both link/joint paths can do consistent normalization).
# For uniqueness checks we always operate on a normalized form that is
# consistent with the link case policy. This keeps collisions and
# generated names aligned with how names are written back to the URDF.
base_name = orig_name
if prefix and not orig_name.lower().startswith(prefix.lower()):
base_name = f"{prefix}{orig_name}"
new_name = base_name
# Ensure the new name is unique
if new_name in existing_names:
counter = 1
unique_name = f"{new_name}_{counter}"
while unique_name in existing_names:
counter += 1
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ```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"`. |
There was a problem hiding this comment.
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.
| # Add sensor links to the links list (ensure lowercase + uniqueness) | ||
| for link in sensor_urdf.findall("link"): | ||
| # Ensure sensor link names are lowercase | ||
| link.set("name", link.get("name").lower()) | ||
| joints.append(link) # This should be added to links list instead | ||
| raw_name = link.get("name") | ||
| if not raw_name: | ||
| continue | ||
|
|
||
| normalized_raw = self._apply_case("link", raw_name) | ||
| if not normalized_raw: | ||
| continue | ||
|
|
||
| base_name = normalized_raw | ||
| sensor_suffix = str(sensor_name).lower() | ||
| if sensor_suffix and sensor_suffix not in base_name: | ||
| base_name = f"{base_name}_{sensor_suffix}" | ||
|
|
||
| unique_name = self._make_unique(base_name, existing_link_names) | ||
| link.set("name", unique_name) | ||
|
|
||
| link_name_map[normalized_raw] = unique_name | ||
| processed_link_names.append(unique_name) | ||
| existing_link_names.add(unique_name) | ||
| links.append(link) | ||
|
|
||
| # Add sensor joints to the joints list (ensure uppercase + update link references) | ||
| for joint in sensor_urdf.findall("joint"): |
There was a problem hiding this comment.
Comment says “ensure lowercase + uniqueness”, but this code now applies the configurable name_case policy via _apply_case("link", ...), so it may be upper/none as well. Update the comment to reflect that names are normalized according to the configured policy (and then made unique).
Summary
This PR refines the URDF assembly pipeline with configurable component name prefixes, a global name casing policy for links and joints, and extended signature semantics so that naming‑related configuration changes correctly invalidate cached assemblies.

Motivation
lower()/upper()behavior via a singlename_casepolicy instead of hard‑coded transformations scattered across managers.URDFCfgand documentation with the new configuration surface.Changes
URDFAssemblyManager
component_prefixproperty:_component_order_and_prefix: list[tuple[str, str | None]].component_prefixacts as a patch‑style interface:(component_name, prefix)tuples.chassis,torso,left_arm, …) may be overridden.ValueError.name_caseargument:URDFAssemblyManager(..., name_case: dict[str, str] | None = None)."joint","link"."upper","lower","none"._apply_case(kind: str, name: str | None) -> str | Noneand remove scattered hard‑coded.lower()/.upper()calls in favor of this policy.name_caseto internal managers:URDFComponentManager(mesh_manager, name_case=self._name_case)URDFConnectionManager(self.base_link_name, name_case=self._name_case)URDFSensorManagercan also honor the same policy.component_infoforURDFAssemblySignatureManager, inject:__component_order_and_prefix__ = list(self.component_order_and_prefix)__name_case__ = dict(self._name_case)URDFAssemblySignatureManager
signature_datato include:component_order_and_prefixname_case__component_order_and_prefix__→ stored insignature_data["component_order_and_prefix"]__name_case__→ stored insignature_data["name_case"]URDFCfg integration
component_prefixsupport toURDFCfg:list[tuple[str, str | None]]with a default matchingURDFAssemblyManager’s internal order.None→ uses the default prefix list.dict[str, str]→ converted tolist(component_prefix.items())for convenience (e.g.{"left_hand": "l_", "right_hand": "r_"}).list[tuple[str, str | None]]→ used as is.assemble_urdf(), passself.component_prefixdirectly toURDFAssemblyManager.component_prefixso thatURDFCfgcan drive per‑component prefixes from robot configs.name_casetoURDFCfgand forward it toURDFAssemblyManagerto allow configuring naming policy from robot configuration.Component and connection managers (if part of this PR)
name_case: dict[str, str] | None = Noneand store internally._apply_case(kind, name)helper mirroring the assembly manager..lower()/.upper()on link and joint names with the configured casing policy.name_case: dict[str, str] | None = None._apply_case("joint", ...)for generated joint names._apply_case("link", ...)for parent/child link names in connection joints.key_casepolicy while keeping logical component names stable in the public API.Documentation
component_prefix) section describing:name_case) section describing:URDFAssemblyManagerconstructor.URDFCfg.component_prefixand its identical semantics toURDFAssemblyManager.component_prefix.Backward compatibility
component_prefixorname_caseshould observe identical URDF outputs.Checklist
black .command to format the code base.