Skip to content

Refine URDF assembly component prefixes and name casing policy#202

Open
chase6305 wants to merge 1 commit intomainfrom
cjt/modify_urdf_assembly
Open

Refine URDF assembly component prefixes and name casing policy#202
chase6305 wants to merge 1 commit intomainfrom
cjt/modify_urdf_assembly

Conversation

@chase6305
Copy link
Collaborator

Description

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

  • Make component name prefixes configurable in a predictable, patch‑style way without exposing internal ordering details.
  • Centralize and expose the link/joint lower()/upper() behavior via a single name_case policy instead of hard‑coded transformations scattered across managers.
  • Ensure that changes to prefixes and naming policies are reflected in the assembly signature so that cached URDFs are rebuilt when naming semantics change.
  • Align URDFCfg and documentation with the new configuration surface.

Changes

  1. URDFAssemblyManager

    • Add support for per‑component name prefixes via the component_prefix property:
      • Internal storage remains _component_order_and_prefix: list[tuple[str, str | None]].
      • component_prefix acts as a patch‑style interface:
        • Accepts a list of (component_name, prefix) tuples.
        • Only existing component names (e.g. chassis, torso, left_arm, …) may be overridden.
        • Components not mentioned keep their original prefix.
        • Unknown component names raise a ValueError.
    • Introduce a global name casing policy via the optional name_case argument:
      • Signature: URDFAssemblyManager(..., name_case: dict[str, str] | None = None).
      • Valid keys: "joint", "link".
      • Valid values: "upper", "lower", "none".
      • Default behavior matches the legacy implementation:
        • joint names → upper case,
        • link names → lower case.
      • Provide a helper _apply_case(kind: str, name: str | None) -> str | None and remove scattered hard‑coded .lower() / .upper() calls in favor of this policy.
    • Propagate name_case to internal managers:
      • URDFComponentManager(mesh_manager, name_case=self._name_case)
      • URDFConnectionManager(self.base_link_name, name_case=self._name_case)
      • (Optional / if implemented) URDFSensorManager can also honor the same policy.
    • Extend assembly signature input:
      • When building component_info for URDFAssemblySignatureManager, inject:
        • __component_order_and_prefix__ = list(self.component_order_and_prefix)
        • __name_case__ = dict(self._name_case)
      • This ensures that changing prefixes or casing policy invalidates the cached assembly and forces a rebuild.
  2. URDFAssemblySignatureManager

    • Extend signature_data to include:
      • component_order_and_prefix
      • name_case
    • Handle the special metadata keys when iterating components:
      • __component_order_and_prefix__ → stored in signature_data["component_order_and_prefix"]
      • __name_case__ → stored in signature_data["name_case"]
    • Keep existing component hashing logic unchanged, so previous behavior is preserved aside from the new fields.
  3. URDFCfg integration

    • Add component_prefix support to URDFCfg:
      • Field type: list[tuple[str, str | None]] with a default matching URDFAssemblyManager’s internal order.
      • Initialization logic allows:
        • None → uses the default prefix list.
        • dict[str, str] → converted to list(component_prefix.items()) for convenience (e.g. {"left_hand": "l_", "right_hand": "r_"}).
        • list[tuple[str, str | None]] → used as is.
    • In assemble_urdf(), pass self.component_prefix directly to URDFAssemblyManager.component_prefix so that URDFCfg can drive per‑component prefixes from robot configs.
    • (If implemented) Add optional name_case to URDFCfg and forward it to URDFAssemblyManager to allow configuring naming policy from robot configuration.
  4. Component and connection managers (if part of this PR)

    • URDFComponentManager
      • Accept name_case: dict[str, str] | None = None and store internally.
      • Add a local _apply_case(kind, name) helper mirroring the assembly manager.
      • Replace hard‑coded .lower() / .upper() on link and joint names with the configured casing policy.
    • URDFConnectionManager
      • Accept name_case: dict[str, str] | None = None.
      • Use _apply_case("joint", ...) for generated joint names.
      • Use _apply_case("link", ...) for parent/child link names in connection joints.
    • (Optional) ComponentRegistry
      • If included, normalize component keys via a key_case policy while keeping logical component names stable in the public API.
  5. Documentation

    • Update urdf_assembly.md:
      • Add Component name prefixes (component_prefix) section describing:
        • The default prefix list.
        • Patch‑style semantics and error behavior on unknown components.
        • The fact that prefixes participate in the assembly signature, so changing them forces a rebuild.
      • Add Name casing policy (name_case) section describing:
        • Configuration via the URDFAssemblyManager constructor.
        • Valid keys/values and default behavior.
        • Propagation to internal managers and inclusion in the assembly signature.
        • The impact on cache invalidation.
      • Extend the Using with URDFCfg section to mention URDFCfg.component_prefix and its identical semantics to URDFAssemblyManager.component_prefix.

Backward compatibility

  • Default behavior is preserved:
    • Joint names remain upper‑cased.
    • Link names remain lower‑cased.
    • Default component prefix list and processing order are unchanged.
  • Existing code that does not set component_prefix or name_case should observe identical URDF outputs.
  • New configuration is opt‑in and only affects behavior when explicitly set.

Checklist

  • I have run the black . command to format the code base.
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • Dependencies have been updated, if applicable.

Copilot AI review requested due to automatic review settings March 26, 2026 06:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refines the URDF assembly toolchain’s naming configuration by introducing per-component prefix overrides, a global link/joint casing policy, and signature inputs that invalidate cached assemblies when naming semantics change. Also wires these knobs into URDFCfg and user config merging, and documents the new configuration surface.

Changes:

  • Add patch-style component_prefix and global name_case handling in URDF assembly, and include both in the assembly signature.
  • Update component/connection naming normalization to use a centralized casing policy helper.
  • Integrate new config fields through URDFCfg / config merging and extend documentation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
examples/sim/robot/dexforce_w1.py Formatting-only bracket/comma adjustment.
embodichain/toolkits/urdf_assembly/urdf_assembly_manager.py Adds name_case + component_prefix logic and injects naming metadata into signature; updates manager construction to pass casing policy.
embodichain/toolkits/urdf_assembly/signature.py Stores component_order_and_prefix + name_case metadata in signature JSON.
embodichain/toolkits/urdf_assembly/connection.py Adds name casing policy support for generated connection names and sensor attachment rewriting.
embodichain/toolkits/urdf_assembly/component.py Introduces casing policy and replaces hard-coded .lower()/.upper() with _apply_case.
embodichain/lab/sim/utility/cfg_utils.py Extends robot cfg merge to include sensors and URDF naming configuration fields.
embodichain/lab/sim/cfg.py Adds URDFCfg.component_prefix and forwards prefix/casing configuration into the assembly manager.
docs/source/features/toolkits/urdf_assembly.md Documents component_prefix and name_case, plus URDFCfg integration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 128 to +132
):
self.logger = setup_urdf_logging()

# Global name normalization strategy for this assembly. By default,
# this preserves the legacy behavior: link names are lowercase and
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 constructor hard-codes the default name casing policy but does not accept a name_case parameter. This conflicts with the documented usage (URDFAssemblyManager(name_case=...)) and will raise TypeError for users following the docs. Consider adding an optional name_case argument to __init__ (and validating/merging it) or updating the docs to reflect the property-based configuration.

Copilot uses AI. Check for mistakes.
Comment on lines 239 to 243
# Add sensor links and joints to the main lists
for link in sensor_urdf.findall("link"):
# Ensure sensor link names are lowercase
link.set("name", link.get("name").lower())
link.set("name", self._apply_case("link", link.get("name")))
joints.append(link) # This should be added to links list instead
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.

add_sensor_attachments appends <link> elements into the joints list (joints.append(link)), which will cause links to be emitted in the joint section if this method is used. Either accept a separate links list here (and append links to it) or rename/adjust the method so it cannot mix element types.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +246 to +265
##### 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:
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.
Comment on lines 724 to 740
if use_signature_check:
# Calculate current assembly signature
# Calculate current assembly signature. In addition to the component
# registry contents, include the current component_order_and_prefix
# so that changes to name prefixes also invalidate the cache.
component_info = self.component_registry.all().copy()
component_info["__component_order_and_prefix__"] = list(
self.component_order_and_prefix
)
# Also include the assembly-wide name_case policy so that
# renaming rules (e.g. link/joint casing) participate in the
# signature. This ensures that changing naming strategy forces
# a rebuild.
component_info["__name_case__"] = dict(self._name_case)

assembly_signature = self.signature_manager.calculate_assembly_signature(
self.component_registry.all(), output_path
component_info, output_path
)
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.

New naming semantics (component_prefix patching + name_case) affect URDF outputs and caching behavior, but there are no tests asserting: (1) unknown component keys raise, (2) per-component prefix patches preserve default ordering, (3) name_case changes affect link/joint names, and (4) signature changes when either setting changes. Adding focused unit tests with minimal URDF XML fixtures would prevent regressions.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +316 to +320
if not isinstance(new_prefixes, list) or not all(
isinstance(item, tuple) and len(item) == 2 for item in new_prefixes
):
raise ValueError(
"component_order_and_prefix must be a list of (component_name, prefix) tuples."
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 component_prefix setter error message (and subsequent validation messages) refer to component_order_and_prefix, which is an internal name. Since this is the public configuration surface, the exception text should reference component_prefix (and ideally mention the patch-style semantics / allowed component keys) to avoid confusing users.

Copilot uses AI. Check for mistakes.
Comment on lines 208 to 212
if prefix:
new_name = self._generate_unique_name(
orig_name, prefix, global_link_names
).lower()
)
else:
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.

When prefix is set, link names produced by _generate_unique_name(...) are no longer normalized via _apply_case("link", ...). This makes the emitted link names inconsistent with the configured name_case policy and can also break the collision checks that rely on global_link_names being normalized.

Copilot uses AI. Check for mistakes.
Comment on lines +342 to 346
# 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()):
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.

_generate_unique_name claims it operates on a normalized form for uniqueness checks, but new_name is never normalized before comparing against existing_names. Since callers build existing_names using _apply_case(...), this can miss collisions (e.g., Left_Link vs left_link) and generate duplicate names. Normalize base_name/new_name according to the relevant case policy before the uniqueness loop.

Copilot uses AI. Check for mistakes.
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.
Comment on lines +242 to +245
self._name_case = new_name_case
self.component_manager.name_case = new_name_case
self.sensor_manager.name_case = new_name_case

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.

name_case setter validates that the input is a dict and contains both keys, but it does not validate the values. If a caller passes an unsupported mode (e.g. "uppercase"), it will be accepted and _apply_case will silently fall back to returning the original name (since the mode won't match "upper"/"lower"). This contradicts the documented valid values; consider validating modes and raising a ValueError (or normalizing/merging with defaults) to fail fast.

Suggested change
self._name_case = new_name_case
self.component_manager.name_case = new_name_case
self.sensor_manager.name_case = new_name_case
# Validate and normalize the case modes to ensure they match the supported values.
allowed_modes = {"upper", "lower", "none"}
validated_name_case: dict[str, str] = {}
for kind in ("joint", "link"):
mode = new_name_case.get(kind)
if not isinstance(mode, str):
raise ValueError(
f"name_case['{kind}'] must be a string; got {type(mode).__name__}."
)
normalized_mode = mode.lower()
if normalized_mode not in allowed_modes:
raise ValueError(
f"Unsupported case mode {mode!r} for '{kind}'. "
f"Expected one of: {', '.join(sorted(allowed_modes))}."
)
validated_name_case[kind] = normalized_mode
self._name_case = validated_name_case
self.component_manager.name_case = validated_name_case
self.sensor_manager.name_case = validated_name_case

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI commented Mar 26, 2026

@chase6305 I've opened a new pull request, #203, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Mar 26, 2026

@chase6305 I've opened a new pull request, #204, to work on those changes. Once the pull request is ready, I'll request review from you.


class URDFComponentManager:
"""Responsible for loading, renaming, and processing meshes for a single component."""
"""Responsible for loading, renaming, and processing meshes for a single component.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add some tests for URDF Assembly tool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants