Skip to content

Updates URDF/MJCF importer to use the latest Isaac Sim importer#5394

Merged
kellyguo11 merged 12 commits into
isaac-sim:developfrom
stevfeng:dev/stevfeng/update_importers
May 15, 2026
Merged

Updates URDF/MJCF importer to use the latest Isaac Sim importer#5394
kellyguo11 merged 12 commits into
isaac-sim:developfrom
stevfeng:dev/stevfeng/update_importers

Conversation

@stevfeng
Copy link
Copy Markdown

Description

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
List any dependencies that are required for this change.

In Isaac Sim, we have added more capabilities to handle joint presets, fixed joints, and other properties to the importers, so we can simplify the isaac lab importer workflow.

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (existing functionality will not work without user modification)
  • Documentation update

Screenshots

Please attach before and after screenshots of the change if applicable.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the changelog and the corresponding version in the extension's config/extension.toml file
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Apr 24, 2026
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

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

🤖 Isaac Lab Review Bot

Summary

This PR refactors the URDF and MJCF converters to delegate the full conversion pipeline to Isaac Sim's native importers (URDFImporter/URDFImporterConfig and MJCFImporter/MJCFImporterConfig). The PR removes ~750 lines of duplicated IsaacLab-specific post-processing code (fix-base insertion, joint drives, link density, ArticulationRootAPI relocation) and replaces it with a thin translation layer. The urdf_utils.py module is reduced to a re-export shim forwarding to the canonical Isaac Sim implementation.

Architecture Impact

High impact, well-contained. The changes affect the asset conversion pipeline which runs offline before simulation. The public API (UrdfConverterCfg, MjcfConverterCfg, merge_fixed_joints) is preserved. However:

  • Downstream callers using UrdfConverterCfg.collision_from_visuals will see a type annotation change (was missing, now bool)
  • Tests validate the new pipeline produces equivalent USD output
  • No runtime simulation impact — converters only affect USD generation

Implementation Verdict

Minor fixes needed. The refactor is architecturally sound, but there are a few issues with config field types, missing type annotations, and a potential test fragility.

Test Coverage

Good coverage. The test file has been updated and covers:

  • Config change detection for lazy conversion
  • Drive type/gains application (scalar and per-joint dict)
  • fix_base creates FixedJoint
  • merge_fixed_joints XML-level and full pipeline
  • Deprecated NaturalFrequencyGainsCfg warning
  • collision_from_visuals, self_collision, link_density

Missing: No test for ros_package_paths resolution with package:// URIs. This is a new config field without coverage.

CI Status

No CI checks available yet.

Findings

🔴 Critical: urdf_converter_cfg.py:174 — ros_package_paths type mismatch with Isaac Sim API

The config declares ros_package_paths: list[dict[str, str]] = [] and the docstring says "Each entry is a dictionary with keys name and path". However, in urdf_converter.py:96, it's passed as:

ros_package_paths=list(cfg.ros_package_paths),

The URDFImporterConfig.ros_package_paths in Isaac Sim may expect a different structure (likely list[tuple[str, str]] or a flat dict). Without verifying the Isaac Sim API contract, this could cause silent failures or exceptions when users actually try to resolve package:// URIs. The test suite does not exercise this path.

🟡 Warning: mjcf_converter_cfg.py:91-92 — override_gain_prm and override_bias_prm should validate length

The docstrings specify these are "10 floats" but there's no validation. If a user passes a list of wrong length, the error will surface deep in the Isaac Sim importer with a confusing message. Consider adding a validate_config hook:

def validate_config(self):
    if self.override_gain_prm is not None and len(self.override_gain_prm) != 10:
        raise ValueError("override_gain_prm must have exactly 10 elements")

🟡 Warning: urdf_converter.py:88-89 — collision_type literal values may not match Isaac Sim enum

The UrdfConverterCfg.collision_type accepts "Bounding Sphere" and "Bounding Cube" (with spaces), and this string is passed directly to URDFImporterConfig(collision_type=...). If the Isaac Sim importer expects different casing (e.g., "BoundingSphere") or an enum, this will fail. The test test_collider_type_convex_decomposition uses collider_type (which doesn't exist in the config — should be collision_type), so the test isn't actually validating this.

🔴 Critical: test_urdf_converter.py:622 — Test uses non-existent config field collider_type

config.collider_type = "convex_decomposition"  # WRONG: field is `collision_type`

This line sets a non-existent attribute. The test passes because the default collision_type="Convex Hull" is used. The test name and docstring claim to verify "Convex Decomposition" but it's actually testing "Convex Hull". Additionally, the value should be "Convex Decomposition" (with capitals and space) per the Literal type.

🟡 Warning: urdf_converter_cfg.py:171 — merge_mesh added but not forwarded to importer

In urdf_converter.py:_convert_asset, the config field merge_mesh is correctly forwarded. However, looking at the diff, this was an existing field that's now properly wired up. Good.

🔵 Improvement: urdf_utils.py — Missing fallback for Isaac Sim versions without the import

The module unconditionally imports from isaacsim.asset.importer.urdf.impl.urdf_utils. If a user has an older Isaac Sim version where this module doesn't exist, they'll get an ImportError at module load time. Consider a try/except with a clear error message:

try:
    from isaacsim.asset.importer.urdf.impl.urdf_utils import merge_fixed_joints
except ImportError as e:
    raise ImportError(
        "merge_fixed_joints requires Isaac Sim 4.5+ with the updated URDF importer"
    ) from e

🔵 Improvement: urdf_converter.py:153 — Deprecation warning for NaturalFrequencyGainsCfg should use warnings.warn consistently

The method uses warnings.warn with stacklevel=2, which is correct for showing the user's call site. However, the warning is only triggered inside _convert_asset_warn_unsupported_features, meaning stacklevel=2 points to _convert_asset, not the user's UrdfConverter(cfg) call. Should be stacklevel=4 to trace through _warn_unsupported_features_convert_assetAssetConverterBase.__init__ → user code.

🔵 Improvement: CHANGELOG.rst:4 — Date is in the future (2026-04-24)

The changelog date appears to be a placeholder. Should be updated to the actual release date.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR refactors the URDF and MJCF converters to delegate the full conversion pipeline to the latest Isaac Sim importers (URDFImporter / MJCFImporter), removing ~300 lines of duplicated IsaacLab-side USD post-processing and adding new config fields that mirror the expanded URDFImporterConfig / MJCFImporterConfig options.

  • P1 — silent breakage of per-joint drive configs: JointDriveCfg.drive_type, target_type, gains.stiffness, and gains.damping all accept dict[str, ...] for per-joint regex overrides (a documented, previously-working feature). _unpack_joint_drive passes those dicts verbatim to URDFImporterConfig fields that expect scalar values, with no warning emitted — existing users relying on per-joint patterns will get type errors or silent no-ops.

Confidence Score: 4/5

Mostly safe to merge, but the per-joint JointDriveCfg dict handling is a silent regression for existing users of that feature.

The refactor is well-structured and the MJCF changes are clean. The one P1 finding — per-joint dict values for drive_type/target_type/stiffness/damping being forwarded to scalar-expecting importer fields without a warning — is a real regression for users of that documented API. The fix is low-effort.

source/isaaclab/isaaclab/sim/converters/urdf_converter.py — specifically _unpack_joint_drive and _warn_unsupported_features.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/sim/converters/urdf_converter.py Major refactor: multi-step pipeline replaced with a thin wrapper around URDFImporter.import_urdf(). Per-joint dict support for drive_type/target_type/stiffness/damping is silently broken — those values are forwarded verbatim to scalar-expecting importer fields with no warning.
source/isaaclab/isaaclab/sim/converters/urdf_converter_cfg.py New config fields added (ros_package_paths, robot_type, run_asset_transformer, run_multi_physics_conversion, debug_mode); collision_type Literal extended; deprecated fields annotated. Well-documented.
source/isaaclab/isaaclab/sim/converters/mjcf_converter.py New MJCFImporterConfig fields forwarded cleanly. Shutil import moved to top-level. Straightforward and clean.
source/isaaclab/isaaclab/sim/converters/mjcf_converter_cfg.py Well-documented addition of fix_base, link_density, robot_type, actuator gain override fields, and pipeline control flags mirroring the new MJCFImporterConfig options.
source/isaaclab/isaaclab/sim/converters/urdf_utils.py Replaced with a re-export shim. Top-level import will fail at load time if isaacsim is not present rather than deferring the error as other Isaac Sim imports do.
source/isaaclab/test/sim/test_urdf_converter.py Minor comment update only. No functional test changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant UrdfConverter
    participant _unpack_joint_drive
    participant URDFImporterConfig
    participant URDFImporter

    User->>UrdfConverter: UrdfConverterCfg(fix_base, joint_drive, ...)
    UrdfConverter->>_unpack_joint_drive: JointDriveCfg
    _unpack_joint_drive-->>UrdfConverter: (drive_type, target_type, stiffness, damping)
    UrdfConverter->>URDFImporterConfig: urdf_path, usd_path, merge_fixed_joints, collision_*, fix_base, link_density, joint_drive_type, override_joint_stiffness, run_asset_transformer, debug_mode
    URDFImporterConfig-->>UrdfConverter: config
    UrdfConverter->>URDFImporter: import_urdf()
    URDFImporter-->>UrdfConverter: robot_name/robot_name.usda
Loading

Reviews (1): Last reviewed commit: "update urdf mjcf importer to use the lat..." | Re-trigger Greptile

Comment on lines 155 to +179
@staticmethod
def _apply_fix_base(stage):
"""Add a fixed joint from the world to the root link of the robot.

Args:
stage: The USD stage to modify.
"""
from pxr import UsdPhysics

default_prim = stage.GetDefaultPrim()
if not default_prim or not default_prim.IsValid():
carb.log_warn("UrdfConverter: Cannot apply fix_base - no default prim found.")
return

# find the root link: first child with `RigidBodyAPI` under the prim hierarchy
root_link = None
for prim in stage.Traverse():
if prim.HasAPI(UsdPhysics.RigidBodyAPI):
root_link = prim
break

if root_link is None:
carb.log_warn("UrdfConverter: Cannot apply fix_base - no rigid body link found.")
return

# create a fixed joint connecting the world to the root link
default_prim_path = default_prim.GetPath()
joint_path = default_prim_path.AppendChild("fix_base_joint")

fixed_joint = UsdPhysics.FixedJoint.Define(stage, joint_path)
# `body0` left empty => connected to the world frame
fixed_joint.CreateBody1Rel().SetTargets([root_link.GetPath()])

@staticmethod
def _fix_articulation_root_for_fixed_base(usd_path: str):
"""Move ArticulationRootAPI from the root rigid body to its parent prim.

After the asset transformer, ArticulationRootAPI ends up on the root rigid body.
When combined with a FixedJoint on that same body (``fix_base_joint``), PhysX treats
the articulation as a floating-base + external constraint (maximal coordinate tree)
rather than a proper fixed-base reduced-coordinate articulation.

Moving ArticulationRootAPI to the parent of the root rigid body (a non-rigid Xform /
Scope ancestor) resolves this, matching the pattern used by ``schemas.py``'s
``fix_root_link``.

Changes are authored as **local opinions in the root layer** of the stage, which are
stronger than the variant-payload-sublayer opinions written by the asset transformer.
This means the root layer's ``delete apiSchemas`` overrides the ``prepend apiSchemas``
in the deeper sublayers without modifying those files.

Args:
usd_path: Absolute path to the final ``.usda`` file produced by the asset transformer.
"""
from pxr import Usd, UsdPhysics

stage = Usd.Stage.Open(usd_path)
if not stage:
carb.log_warn(
f"UrdfConverter: Cannot open final stage at '{usd_path}'"
" for fix_base ArticulationRootAPI post-processing."
)
return

# Find the root rigid body that incorrectly has ArticulationRootAPI applied.
root_body_prim = None
for prim in stage.Traverse():
if prim.HasAPI(UsdPhysics.ArticulationRootAPI) and prim.HasAPI(UsdPhysics.RigidBodyAPI):
root_body_prim = prim
break

if root_body_prim is None:
# ArticulationRootAPI is already on a non-rigid ancestor (correct) or not present.
return

parent_prim = root_body_prim.GetParent()
if not parent_prim or not parent_prim.IsValid():
carb.log_warn("UrdfConverter: Root rigid body has no valid parent prim — skipping ArticulationRootAPI fix.")
return

# Collect all articulation-related schema names applied to the root rigid body.
articulation_api_names = [
name
for name in root_body_prim.GetAppliedSchemas()
if "ArticulationRoot" in name or name == "PhysxArticulationAPI"
]

# --- Apply ArticulationRootAPI schemas to the parent prim ---
# (edit target is the root layer by default; writes local opinions)
UsdPhysics.ArticulationRootAPI.Apply(parent_prim)
already_on_parent = set(parent_prim.GetAppliedSchemas())
for name in articulation_api_names:
if name != "PhysicsArticulationRootAPI" and name not in already_on_parent:
parent_prim.AddAppliedSchema(name)

# --- Copy USD articulation attributes to the parent prim ---
usd_art_api = UsdPhysics.ArticulationRootAPI(root_body_prim)
for attr_name in usd_art_api.GetSchemaAttributeNames():
attr = root_body_prim.GetAttribute(attr_name)
val = attr.Get() if attr else None
if val is not None:
parent_attr = parent_prim.GetAttribute(attr_name)
if not parent_attr:
parent_attr = parent_prim.CreateAttribute(attr_name, attr.GetTypeName())
parent_attr.Set(val)

# --- Copy physxArticulation:* attributes to the parent prim ---
for attr in root_body_prim.GetAttributes():
aname = attr.GetName()
if aname.startswith("physxArticulation:"):
val = attr.Get()
if val is not None:
parent_attr = parent_prim.GetAttribute(aname)
if not parent_attr:
parent_attr = parent_prim.CreateAttribute(aname, attr.GetTypeName())
parent_attr.Set(val)

# --- Remove ArticulationRootAPI schemas from the root rigid body ---
# Writing "delete" list-ops in the root layer overrides "prepend" in sublayers.
root_body_prim.RemoveAppliedSchema("PhysxArticulationAPI")
root_body_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI)
for name in articulation_api_names:
if name not in ("PhysicsArticulationRootAPI", "PhysxArticulationAPI"):
root_body_prim.RemoveAppliedSchema(name)

# Save only the root layer (sublayers produced by the asset transformer are untouched).
stage.GetRootLayer().Save()

@staticmethod
def _apply_link_density(stage, density: float):
"""Set default density on rigid body links that have no explicit mass.

Args:
stage: The USD stage to modify.
density: The density value in kg/m^3.
"""
from pxr import UsdPhysics

for prim in stage.Traverse():
if not prim.HasAPI(UsdPhysics.MassAPI):
continue
mass_api = UsdPhysics.MassAPI(prim)
# only set density if mass is not explicitly specified (0.0 means auto-compute)
mass_attr = mass_api.GetMassAttr()
if mass_attr and mass_attr.HasValue() and mass_attr.Get() > 0.0:
continue
density_attr = mass_api.GetDensityAttr()
if not density_attr:
density_attr = mass_api.CreateDensityAttr()
density_attr.Set(density)

def _apply_joint_drives(self, stage, cfg: UrdfConverterCfg):
"""Set joint drive properties (type, target, gains) on USD joints.
def _unpack_joint_drive(joint_drive: UrdfConverterCfg.JointDriveCfg | None) -> tuple:
"""Translate an IsaacLab :class:`UrdfConverterCfg.JointDriveCfg` into flat importer fields.

Args:
stage: The USD stage to modify.
cfg: The URDF converter configuration containing joint drive settings.
"""
from pxr import UsdPhysics

# collect all joints with their metadata
joints: dict[str, tuple] = {}
for prim in stage.Traverse():
if not (prim.IsA(UsdPhysics.RevoluteJoint) or prim.IsA(UsdPhysics.PrismaticJoint)):
continue
joint_name = prim.GetName()
is_revolute = prim.IsA(UsdPhysics.RevoluteJoint)
instance_name = "angular" if is_revolute else "linear"
joints[joint_name] = (prim, is_revolute, instance_name)

if not joints:
return

drive_cfg = cfg.joint_drive

# apply drive type (force / acceleration)
self._set_drive_type_on_joints(joints, drive_cfg)
# apply target type (none / position / velocity)
self._set_target_type_on_joints(joints, drive_cfg)
# apply gains (stiffness / damping)
self._set_drive_gains_on_joints(joints, drive_cfg)

# ------------------------------------------------------------------
# Joint drive helpers
# ------------------------------------------------------------------

@staticmethod
def _set_drive_type_on_joints(joints: dict, drive_cfg: UrdfConverterCfg.JointDriveCfg):
"""Set the drive type (force or acceleration) on joint prims.
joint_drive: The nested IsaacLab joint-drive configuration, or ``None``.

Args:
joints: Mapping of joint name → (prim, is_revolute, instance_name).
drive_cfg: The joint drive configuration.
"""
from pxr import UsdPhysics

def _apply(prim, instance_name: str, drive_type: str):
drive = UsdPhysics.DriveAPI.Get(prim, instance_name)
type_attr = drive.GetTypeAttr()
if not type_attr:
type_attr = drive.CreateTypeAttr()
type_attr.Set(drive_type)

if isinstance(drive_cfg.drive_type, str):
for _name, (prim, _is_rev, inst) in joints.items():
_apply(prim, inst, drive_cfg.drive_type)
elif isinstance(drive_cfg.drive_type, dict):
for pattern, drive_type in drive_cfg.drive_type.items():
matches = [n for n in joints if re.search(pattern, n)]
if not matches:
raise ValueError(
f"Joint name pattern '{pattern}' in drive_type config matched no joints."
f" Available joints: {list(joints.keys())}"
)
for name in matches:
prim, _, inst = joints[name]
_apply(prim, inst, drive_type)

@staticmethod
def _set_target_type_on_joints(joints: dict, drive_cfg: UrdfConverterCfg.JointDriveCfg):
"""Set the target type (none, position, velocity) on joint prims.

For ``"none"``, both stiffness and damping are zeroed out.

Args:
joints: Mapping of joint name → (prim, is_revolute, instance_name).
drive_cfg: The joint drive configuration.
"""
from pxr import UsdPhysics

def _apply(prim, instance_name: str, target_type: str):
drive = UsdPhysics.DriveAPI.Get(prim, instance_name)
if target_type == "none":
drive.GetStiffnessAttr().Set(0.0)
drive.GetDampingAttr().Set(0.0)

if isinstance(drive_cfg.target_type, str):
for _name, (prim, _is_rev, inst) in joints.items():
_apply(prim, inst, drive_cfg.target_type)
elif isinstance(drive_cfg.target_type, dict):
for pattern, target_type in drive_cfg.target_type.items():
matches = [n for n in joints if re.search(pattern, n)]
if not matches:
raise ValueError(
f"Joint name pattern '{pattern}' in target_type config matched no joints."
f" Available joints: {list(joints.keys())}"
)
for name in matches:
prim, _, inst = joints[name]
_apply(prim, inst, target_type)

@staticmethod
def _set_drive_gains_on_joints(joints: dict, drive_cfg: UrdfConverterCfg.JointDriveCfg):
"""Set stiffness and damping on joint drive APIs.

For revolute joints the user-facing values (Nm/rad) are converted to the USD
convention (Nm/deg) by multiplying by ``pi / 180``.

Args:
joints: Mapping of joint name → (prim, is_revolute, instance_name).
drive_cfg: The joint drive configuration.
Returns:
Tuple ``(drive_type, target_type, stiffness, damping)`` suitable for
:class:`~isaacsim.asset.importer.urdf.URDFImporterConfig`. Entries are ``None`` when
the user did not request an override.
"""
from pxr import UsdPhysics

gains = drive_cfg.gains
if not isinstance(gains, UrdfConverterCfg.JointDriveCfg.PDGainsCfg):
return

def _set_stiffness(prim, instance_name: str, is_revolute: bool, value: float):
drive = UsdPhysics.DriveAPI.Get(prim, instance_name)
usd_value = value * math.pi / 180.0 if is_revolute else value
stiffness_attr = drive.GetStiffnessAttr()
if not stiffness_attr:
stiffness_attr = drive.CreateStiffnessAttr()
stiffness_attr.Set(usd_value)

def _set_damping(prim, instance_name: str, is_revolute: bool, value: float):
drive = UsdPhysics.DriveAPI.Get(prim, instance_name)
usd_value = value * math.pi / 180.0 if is_revolute else value
damping_attr = drive.GetDampingAttr()
if not damping_attr:
damping_attr = drive.CreateDampingAttr()
damping_attr.Set(usd_value)

# --- stiffness ---
if isinstance(gains.stiffness, (float, int)):
for _name, (prim, is_rev, inst) in joints.items():
_set_stiffness(prim, inst, is_rev, gains.stiffness)
elif isinstance(gains.stiffness, dict):
for pattern, stiffness in gains.stiffness.items():
matches = [n for n in joints if re.search(pattern, n)]
if not matches:
raise ValueError(
f"Joint name pattern '{pattern}' in stiffness config matched no joints."
f" Available joints: {list(joints.keys())}"
)
for name in matches:
prim, is_rev, inst = joints[name]
_set_stiffness(prim, inst, is_rev, stiffness)

# --- damping ---
if gains.damping is None:
return
if isinstance(gains.damping, (float, int)):
for _name, (prim, is_rev, inst) in joints.items():
_set_damping(prim, inst, is_rev, gains.damping)
elif isinstance(gains.damping, dict):
for pattern, damping in gains.damping.items():
matches = [n for n in joints if re.search(pattern, n)]
if not matches:
raise ValueError(
f"Joint name pattern '{pattern}' in damping config matched no joints."
f" Available joints: {list(joints.keys())}"
)
for name in matches:
prim, is_rev, inst = joints[name]
_set_damping(prim, inst, is_rev, damping)
if joint_drive is None:
return None, None, None, None

gains = joint_drive.gains
if isinstance(gains, UrdfConverterCfg.JointDriveCfg.PDGainsCfg):
stiffness = gains.stiffness
damping = gains.damping
else:
# `NaturalFrequencyGainsCfg` is deprecated; leave gains unchanged.
stiffness = None
damping = None

return joint_drive.drive_type, joint_drive.target_type, stiffness, damping
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Per-joint dict configs silently passed to scalar-expecting importer fields

JointDriveCfg.drive_type, target_type, gains.stiffness, and gains.damping all accept dict[str, ...] for per-joint regex-pattern overrides. The old code explicitly handled this with re.search pattern matching. _unpack_joint_drive now extracts these values verbatim and forwards them to URDFImporterConfig(joint_drive_type=..., joint_target_type=..., override_joint_stiffness=..., override_joint_damping=...), which almost certainly expects scalar/string values — not dicts. Any user who relied on per-joint configs will either hit a runtime type error or have their settings silently ignored, with no warning emitted.

_warn_unsupported_features should detect dict-valued drive_type, target_type, stiffness, and damping and emit a deprecation warning, the same way it does for NaturalFrequencyGainsCfg.

the canonical implementation to preserve the public import path
``isaaclab.sim.converters.urdf_utils``.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Re-export shim has no fallback for missing upstream module

The entire urdf_utils.py module is now a single import from isaacsim.asset.importer.urdf.impl.urdf_utils. If that internal path changes or the isaacsim package is not installed (e.g., when the module is imported in a documentation/test environment without Isaac Sim), any import of isaaclab.sim.converters.urdf_utils will raise an ImportError at module load time rather than at conversion time. Consider wrapping the import in a try/except and raising a clearer error only when merge_fixed_joints is actually called, similar to how other Isaac Sim-specific imports are deferred inside methods across the codebase.

Comment on lines +113 to +121
run_asset_transformer: bool = True
"""Whether to run the asset transformer profile after conversion. Defaults to True.

When enabled, the importer restructures the intermediate USD into a layered,
payload-based package. Disable for a single flat USD output.
"""

run_multi_physics_conversion: bool = True
"""Whether to run the MJCF-to-PhysX physics conversion pass. Defaults to True."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 run_asset_transformer and run_multi_physics_conversion default to True — worth documenting the behavioral implications

Defaulting them to True preserves existing behaviour, which is correct. However, if a user accidentally sets run_multi_physics_conversion=False, the resulting USD will lack PhysX joint attributes and the articulation will be unusable in simulation. Consider adding a .. warning:: block to both docstrings noting that disabling these flags produces a non-simulated asset intended only for inspection/debugging.

Comment on lines +49 to +51
# ---------------------------------------------------------------
# Physics / articulation options
# ---------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: We can remove these dunders here as they are redundant and not done at other places in the code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thanks, fixed

Comment on lines +29 to +31
# ---------------------------------------------------------------
# Geometry / mesh options
# ---------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT: We can remove these dunders here as they are redundant and not done at other places in the code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thanks, fixed

# Actuator gain overrides
# ---------------------------------------------------------------

override_gain_type: str | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this called override here and not apply? I suppose a one-to-one naming makes more sense unless justified.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm calling it override because it will apply this overrides the imported gains settings on the mujoco actuators, not just the empty ones. Would you prefer if i use "apply" instead?

"""

override_gain_prm: list[float] | None = None
"""MuJoCo actuator gain parameter array (10 floats) override. Defaults to ``None``.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you explain here what the 10 floats correspond to?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yup, I gave a more general explanation F = gain * control + bias, the param array doesn't need to be 10 elements I think

"""

override_bias_prm: list[float] | None = None
"""MuJoCo actuator bias parameter array (10 floats) override. Defaults to ``None``.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here too. Could you explain what do the floats mean?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yup, I gave a more general explanation F = gain * control + bias, the param array doesn't need to be 10 elements I think

# ---------------------------------------------------------------

run_asset_transformer: bool = True
"""Whether to run the asset transformer profile after conversion. Defaults to True.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This too needs more information. We should avoid docstrings that just describe what the name says. Ideally, they should add more information on what the attribute/function is doing internally, so that the developer is more informed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I agree, I listed out the file structure that the asset transformer generates for extra clarity

payload-based package. Disable for a single flat USD output.
"""

run_multi_physics_conversion: bool = True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does this mean? Is it only for PhysX or also for Newton?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added explanation: yes, so this will convert mujoco: attributes to physx: attributes. I can imagine this will slowly fade away as newton bridges the gap between mujoco and physx.

If None, the root link will be set by PhysX.

.. deprecated::
This option is no longer supported by the URDF importer 3.0. A warning is logged if set.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

An explanation on why is needed. Otherwise it stays unclear.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am not sure about the purpose of this config, so I kept the arg there purely for backwards compatibility

to :class:`~isaacsim.asset.importer.urdf.URDFImporterConfig`.
"""

robot_type: str = "Default"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does this mean? Could you explain what other types are there? Otherwise, it is unclear.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added robot types

"""

run_asset_transformer: bool = True
"""Whether to run the asset transformer profile after conversion. Defaults to True.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same docstring note here as the mjcf importer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I agree, I listed out the file structure that the asset transformer generates for extra clarity

payload-based package. Disable for a single flat USD output.
"""

run_multi_physics_conversion: bool = True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same docstring note here as the mjcf importer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

agree, explained further. this will convert urdf: tags to newton, mujoco, and physx

R: A 3x3 rotation matrix.
Historically, IsaacLab shipped its own copy of ``merge_fixed_joints`` for the URDF
pipeline. That logic has moved to the Isaac Sim URDF importer at
:mod:`isaacsim.asset.importer.urdf.impl.urdf_utils`, so this module now simply re-exports
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this mean one needs to have Isaac Sim installed? If so, then this isn't ideal IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this module is still in develop, I suggest just removing it altogether and directly call isaacsim import where used in the codebase. Following that please update the changelog to say this module is deleted and check versioning accordingly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

hopefully Isaac Sim is not a hard requirement, we are looking into shipping the importer as a separate isaac sim independent pip package. and yes sounds good, I will update the doc string

@Mayankm96 Mayankm96 changed the title update urdf mjcf importer to use the latest isaac sim importer Updates URDF/MJCF importer to use the latest Isaac Sim importer May 11, 2026
Copy link
Copy Markdown

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR represents a significant architectural improvement by delegating URDF/MJCF conversion logic to the Isaac Sim importer, reducing code duplication and maintenance burden. The refactoring is well-executed overall.

✅ Strengths

  1. Excellent Code Simplification: Removing ~400 lines of duplicated conversion logic from urdf_converter.py in favor of the upstream Isaac Sim implementation is the right architectural decision.

  2. Good Backwards Compatibility: The urdf_utils.py re-export shim preserves the public import path for merge_fixed_joints, preventing breaking changes for downstream users.

  3. Comprehensive Test Coverage: The 202 new lines of MJCF tests cover the new configuration options (self-collision, collision types, actuator gains, etc.).

  4. Clear Documentation: The changelog entries thoroughly document all added/changed configuration fields.

🔍 Findings

1. Potential Issue: Dictionary-based joint drive gains not supported

File: urdf_converter.py (lines 145-171)
Severity: Medium

The _unpack_joint_drive() method only extracts scalar stiffness and damping values. The previous implementation supported regex-based per-joint dictionaries (dict[str, float]) for fine-grained control. If the Isaac Sim importer does not support per-joint overrides via override_joint_stiffness/override_joint_damping, this represents a feature regression.

Recommendation: Verify whether the Isaac Sim importer accepts dictionary-based gains, or document this as a breaking change for users who relied on per-joint gain specifications.

2. Missing validation for JointDriveCfg dictionary types

File: urdf_converter.py (line 159-163)

if isinstance(gains, UrdfConverterCfg.JointDriveCfg.PDGainsCfg):
    stiffness = gains.stiffness
    damping = gains.damping

If gains.stiffness or gains.damping is a dict (for per-joint values), passing it directly to the importer may cause unexpected behavior. Consider adding type validation or conversion.

3. Type annotation missing for ros_package_paths

File: urdf_converter_cfg.py (line 174-178)

ros_package_paths: list[dict[str, str]] = []

The docstring states each entry should have keys name and path, but there is no TypedDict or validation to enforce this structure. Consider using a @dataclass or TypedDict for better type safety.

📋 Previous Concerns Status

  • ⏳ Per-joint dictionary gains not supported — not addressed (original inline comment still stands)
  • ⏳ Re-export shim fallback — not addressed (original inline comment still stands)
  • ⏳ Documentation for disabled flags — not addressed (original inline comment still stands)

🆕 Update (085e3f8): OVPhysX RigidObject Implementation

This is a substantial update adding a complete OVPhysX backend implementation for RigidObject, mirroring the PhysX API. Key additions:

New Files

  • rigid_object.py (1173 lines): Full OVPhysX-backed RigidObject implementation with pose/velocity writers, mass/COM/inertia setters, and wrench composers
  • rigid_object_data.py (1198 lines): Data container with lazy timestamped buffers and ProxyArray wrappers
  • test_rigid_object.py (1134 lines): Comprehensive test suite covering initialization, state setting, external forces, and material properties (some xfail due to missing RIGID_BODY_MATERIAL TensorType)

Architecture Observations

✅ Well-designed:

  • Device-lock mechanism at process level (_locked_device) correctly handles ovphysx<=0.3.7's C++ constraint
  • Lazy binding creation via _get_binding() avoids unnecessary allocations
  • CPU staging buffers for CPU-only bindings (mass/coms/inertia) handle device mismatch properly
  • Clear documentation of HACK notes for Carbonite dual-instance workarounds

⚠️ Potential concerns:

  1. Process-global PhysX instance reuse (ovphysx_manager.py lines 90-97, 158-178)
    The PhysX instance is now intentionally kept alive across SimulationContext instances. The _release_physx() method only calls physx.reset() rather than dropping the reference. This is documented well but users may be surprised if they expect fresh state between contexts.

  2. _configure_physx_scene_prim() device branching (lines 330-425)
    GPU-specific attributes are only applied when device == "gpu". Verify this doesn't cause issues when switching device modes within the same process (though the device lock should prevent this).

  3. _read_binding_into() assertion vs exception (rigid_object_data.py line 875)
    Uses assert for shape checking which is stripped in -O mode. Consider raise ValueError for production safety.

Minor Issues

  1. Changelog entries removed prematurely (lines removed from changelog.d/)
    The feature entries for heterogeneous Dexsuite and Newton extra were removed but the version bump is to 0.6.4. Verify these changes were already released in 0.6.3.

  2. Test device parametrization (test_rigid_object.py)
    Tests parametrize over ["cuda:0", "cpu"] but the autouse fixture _ovphysx_skip_other_device may skip half the tests based on session lock. This is documented but could be confusing for CI.

Overall Assessment

The new OVPhysX RigidObject implementation is well-architected and follows the established patterns from the Articulation implementation. The device-lock mechanism and Carbonite workarounds are properly documented. The main previous concerns about URDF converter remain unaddressed but the incremental changes are solid.

Recommendation: Merge after addressing or acknowledging the per-joint dictionary gains concern from the original review.


Update (a8aa860): Reviewed 13 new commits (mostly merge commits from develop). Key upstream changes include:

  • Backend-agnostic task-space accessors for IK/OSC (#5400)
  • RSL-RL config updates for rsl_rl >= 5.0 (#5551)
  • New Scene Data Provider API (#5128)
  • Newton dependency simplification using newton[sim] (#5566)
  • Shadow-Hand-Over MAPPO Newton backend (#5437)

The upstream merges introduce no new issues related to the URDF/MJCF importer changes in this PR. Previous concerns about per-joint dictionary gains and documentation remain unaddressed but are not blocking.

✅ No new issues found in the incremental changes.


Update (a3a3d0a): Reviewed incremental changes.

Changes Summary

  • Documentation: Extensive improvements to URDF/MJCF importer docs (configuration parameter organization, notes about auto-deduplication)
  • LEAPP Install Fix: Changed pip install leapp./isaaclab.sh -p -m pip install leapp for proper Isaac Lab environment installation
  • Changelog Consolidation: Released entries from changelog.d/ merged into main CHANGELOGs; version bumps to isaaclab 5.2.0/5.3.0
  • Test Fix: Added sim._disable_app_control_on_stop_handle = True in MJCF converter test to prevent timeout

Previous Concerns Status

  1. urdf_utils.py re-export shim — ✅ Resolved (removed): The file was completely deleted rather than being a shim. Import path is now directly from isaacsim.asset.importer.urdf.impl.urdf_utils. This is a clean break from the old API. Tests updated to import from the new location.

  2. Per-joint dictionary gains — ⏳ Still unaddressed: The inline comment about dict-valued drive_type/target_type/gains being silently passed to scalar-expecting importer fields remains relevant.

  3. Documentation for disabled flags — ⏳ Still unaddressed: The inline comment about adding warnings for run_asset_transformer=False / run_multi_physics_conversion=False remains relevant.

✅ No new issues found in the incremental changes. The documentation improvements are excellent.


Update (f1addb0): Reviewed incremental changes.

Changes Summary

  • Changelog housekeeping: Added changelog fragment file stevfeng-fix-converter-usd-path.minor.rst containing the properly formatted changelog entries
  • Main CHANGELOG.rst cleanup: Removed pre-generated 5.3.0 entries from the main changelog (will be auto-generated from fragments during release)

This follows the correct scriv/towncrier changelog fragment pattern - entries are maintained in changelog.d/ and compiled into the main CHANGELOG during releases.

✅ No new issues. This is correct changelog management - no code changes, just documentation housekeeping.


Update (1a665c5): Reviewed incremental changes since f1addb0.

Changes Summary

  • Documentation RST fixes: Corrected section heading underlines in import_new_asset.rst (changed from """ to ~~~ for proper heading hierarchy)
  • CHANGELOG.rst formatting: Fixed orphan paragraph indentation, added proper bullet prefix and blank lines between versions
  • Changelog validation tooling: Added orphan paragraph detection to tools/changelog/cli.py to catch malformed fragments at CI time
  • Test fixtures: Added test case for the new orphan paragraph validation

Previous Concerns Status

  1. Per-joint dictionary gains — ⏳ Still unaddressed (not modified in this update)
  2. Documentation for disabled flags — ⏳ Still unaddressed (not modified in this update)

✅ No new issues found. These are purely documentation and CI tooling improvements with no impact on the URDF/MJCF converter functionality.


Update (178634f): Reviewed incremental changes since 1a665c5.

Changes Summary

This update contains a collection of bug fixes and improvements unrelated to the URDF/MJCF importer changes:

  • AppLauncher CUDA fix: Deferred torch.cuda.set_device() call until after SimulationApp starts to prevent OpenBLAS/NumPy fork crashes during Kit startup
  • Math utility edge case fixes: create_rotation_matrix_from_view() now returns valid orthonormal frames when look-at direction is parallel to up axis (previously returned singular matrix); quat_from_matrix() now returns NaN for non-rotation inputs (singular, reflection, scale-error matrices)
  • Camera pose fixes: Camera.set_world_poses_from_view() and RayCasterCamera.set_world_poses_from_view() now handle degenerate cases (eye==target) with warnings instead of silently applying garbage poses
  • PVA sensor debug visualizer fixes: Both newton and physx backends now skip bodies with zero acceleration when drawing direction arrows (previously drew undefined directions)
  • LEAPP export script refactoring: Restructured as importable library with deferred runtime dependencies for testability
  • CI test improvements: Added LEAPP package debug output, unset HUB__ARGS__DETECT_ONLY flag
  • Import path fixes: Several files changed from from isaaclab.utils import configclass to direct import
  • Test cleanup: Removed unnecessary Isaac Sim launcher imports from pure-Python tests
  • Version bumps: isaaclab 5.2.1, isaaclab_newton 0.9.1, isaaclab_physx 0.7.1

Previous Concerns Status

  1. Per-joint dictionary gains — ⏳ Still unaddressed (not modified in this update)
  2. Documentation for disabled flags — ⏳ Still unaddressed (not modified in this update)

✅ No new issues found. These are solid bug fixes with comprehensive test coverage for the math edge cases.

@stevfeng stevfeng force-pushed the dev/stevfeng/update_importers branch from c638330 to 0ba9b59 Compare May 12, 2026 00:41
@stevfeng stevfeng force-pushed the dev/stevfeng/update_importers branch 3 times, most recently from 64447a7 to 17e01a9 Compare May 13, 2026 16:49
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label May 13, 2026
@stevfeng stevfeng force-pushed the dev/stevfeng/update_importers branch from a8aa860 to a3a3d0a Compare May 14, 2026 16:22
@stevfeng stevfeng force-pushed the dev/stevfeng/update_importers branch from 127f48d to d7d45ba Compare May 14, 2026 17:35
@kellyguo11 kellyguo11 moved this to In review in Isaac Lab May 15, 2026
@kellyguo11 kellyguo11 moved this from In review to Ready to merge in Isaac Lab May 15, 2026
@kellyguo11 kellyguo11 merged commit 94c0967 into isaac-sim:develop May 15, 2026
33 of 34 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to merge to Done in Isaac Lab May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants