-
Notifications
You must be signed in to change notification settings - Fork 9
Refine URDF assembly component prefixes and name casing policy #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3f5cc6b
9f58058
8dfabff
1fa223d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ##### 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): |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| ("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, | ||
|
|
@@ -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. | ||
|
|
@@ -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 {} | ||
|
|
@@ -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: | ||
|
|
@@ -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( | ||
|
|
@@ -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, | ||
| ) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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( | ||||||||||||||||||
|
||||||||||||||||||
| base_cfg.urdf_cfg.add_sensor( | |
| base_cfg.urdf_cfg.add_sensor( | |
| sensor_name=sensor.get("sensor_name"), |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
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.
| 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: |
There was a problem hiding this comment.
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=...), butURDFAssemblyManager.__init__in this PR does not accept aname_caseargument. Either add the constructor parameter (as documented) or update the docs to the supported usage (manager.name_case = {...}) to avoid users hittingTypeError.