diff --git a/source/isaaclab/changelog.d/jichuanh-kitless-newton-tests.rst b/source/isaaclab/changelog.d/jichuanh-kitless-newton-tests.rst new file mode 100644 index 000000000000..a90832ac0fb4 --- /dev/null +++ b/source/isaaclab/changelog.d/jichuanh-kitless-newton-tests.rst @@ -0,0 +1,8 @@ +Fixed +^^^^^ + +* Removed the unconditional ``from omni.physx.scripts import utils`` in + :func:`isaaclab.sim.schemas.modify_articulation_root_properties` by inlining + the single-selection ``Fixed`` joint creation via :mod:`pxr.UsdPhysics` + directly. The previous code path broke any kitless newton run that needed + to anchor a fixed-base articulation to the world. diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index 23f53b105cb3..05e30747be89 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -13,7 +13,7 @@ import numpy as np import warp as wp -from pxr import Sdf, Usd, UsdGeom, UsdPhysics +from pxr import Gf, Sdf, Usd, UsdGeom, UsdPhysics from isaaclab.sim.utils.stage import get_current_stage from isaaclab.utils.string import to_camel_case @@ -134,6 +134,63 @@ def _get_field_declaring_class(cfg_class: type, field_name: str) -> type | None: return None +def _create_fixed_joint_to_world(stage: Usd.Stage, articulation_prim: Usd.Prim) -> UsdPhysics.FixedJoint: + """Create a ``UsdPhysics.FixedJoint`` pinning ``articulation_prim`` to the world. + + Kitless equivalent of ``omni.physx.scripts.utils.createJoint(stage, "Fixed", + from_prim=None, to_prim=articulation_prim)``: the omni helper is just a + thin wrapper around :mod:`pxr.UsdPhysics` ops, so inlining the + single-selection Fixed case lets this code path work both with and without + Kit loaded (newton backend in kitless mode hits this too). + + Args: + stage: USD stage to author on. + articulation_prim: The articulation root prim to anchor to the world. + + Returns: + The created :class:`pxr.UsdPhysics.FixedJoint`. + """ + to_path = articulation_prim.GetPath().pathString + # Mirror omni.physx createJoint's "find first writable ancestor" walk: + # instanced / prototype prims can't host the joint, so climb to a writable + # parent before authoring. + base_prim = articulation_prim + pseudo_root = stage.GetPseudoRoot() + while base_prim != pseudo_root: + if base_prim.IsInPrototype() or base_prim.IsInstanceProxy() or base_prim.IsInstanceable(): + base_prim = base_prim.GetParent() + else: + break + joint_base_path = str(base_prim.GetPrimPath()) + if joint_base_path == "/": + joint_base_path = "" + + # Pick a unique sibling name "FixedJoint", "FixedJoint_01", etc. + joint_name = "FixedJoint" + idx = 1 + while stage.GetPrimAtPath(f"{joint_base_path}/{joint_name}").IsValid(): + joint_name = f"FixedJoint_{idx:02d}" + idx += 1 + joint_path = f"{joint_base_path}/{joint_name}" + + component = UsdPhysics.FixedJoint.Define(stage, joint_path) + + # Single-selection placement: joint sits at to_prim's world pose; body0 + # rel is left empty (world), body1 rel points to to_prim. Matches the + # omni single-selection branch. + xf_cache = UsdGeom.XformCache() + to_pose = xf_cache.GetLocalToWorldTransform(articulation_prim).RemoveScaleShear() + pos = Gf.Vec3f(to_pose.ExtractTranslation()) + rot = Gf.Quatf(to_pose.ExtractRotationQuat()) + + component.CreateBody1Rel().SetTargets([Sdf.Path(to_path)]) + component.CreateLocalPos0Attr().Set(pos) + component.CreateLocalRot0Attr().Set(rot) + component.CreateLocalPos1Attr().Set(Gf.Vec3f(0.0)) + component.CreateLocalRot1Attr().Set(Gf.Quatf(1.0)) + return component + + def _apply_namespaced_schemas(prim, cfg, cfg_dict: dict) -> None: """Route every cfg field to its declaring class's namespace and apply schemas. @@ -339,10 +396,11 @@ def modify_articulation_root_properties( " the articulation tree. However, this is not implemented yet." ) - # create a fixed joint between the root link and the world frame - from omni.physx.scripts import utils as physx_utils - - physx_utils.createJoint(stage=stage, joint_type="Fixed", from_prim=None, to_prim=articulation_prim) + # Create a fixed joint between the root link and the world frame. + # Use pxr.UsdPhysics directly so this code path also works in + # kitless mode (newton backend without Kit). The equivalent + # ``omni.physx.scripts.utils.createJoint`` is a thin pxr.Usd wrapper. + _create_fixed_joint_to_world(stage, articulation_prim) # Having a fixed joint on a rigid body is not treated as "fixed base articulation". # instead, it is treated as a part of the maximal coordinate tree. diff --git a/source/isaaclab_contrib/changelog.d/jichuanh-kitless-newton-tests.skip b/source/isaaclab_contrib/changelog.d/jichuanh-kitless-newton-tests.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_newton/changelog.d/jichuanh-kitless-newton-tests.skip b/source/isaaclab_newton/changelog.d/jichuanh-kitless-newton-tests.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_newton/test/assets/test_articulation.py b/source/isaaclab_newton/test/assets/test_articulation.py index 4b27988f65ed..284218a5dc86 100644 --- a/source/isaaclab_newton/test/assets/test_articulation.py +++ b/source/isaaclab_newton/test/assets/test_articulation.py @@ -6,16 +6,16 @@ # ignore private usage of variables warning # pyright: reportPrivateUsage=none -"""Launch Isaac Sim Simulator first.""" +"""Kitless newton tests: run the newton physics backend without booting Kit. -from isaaclab.app import AppLauncher - -HEADLESS = True - -# launch omniverse app -simulation_app = AppLauncher(headless=True).app - -"""Rest everything follows.""" +``SimulationContext`` and :func:`~isaaclab.sim.build_simulation_context` gate +all Kit-specific paths on :func:`~isaaclab.utils.version.has_kit`, so omitting +the module-level ``AppLauncher(headless=True).app`` boot is sufficient — newton +tests run in pure-python + warp without Isaac Sim's Kit runtime. This avoids +the Kit/Isaac-Sim concurrency lifecycle bug (SIGHUP / shutdown-hang at >=3 +concurrent Kit instances on test_articulation under multi-GPU CI) and shaves +~30s off per-file boot. +""" import sys from copy import deepcopy @@ -354,9 +354,12 @@ def generate_articulation( # Fix reversed joints for known-broken USD assets (body0/body1 swapped) usd_path = getattr(articulation_cfg.spawn, "usd_path", "") if any(name in usd_path for name in _REVERSED_JOINT_USD_FILES): - import omni.usd + # Kitless: use IsaacLab's stage helper instead of ``omni.usd.get_context()``. + # ``get_current_stage`` falls back to the in-memory pxr.Usd stage when Kit + # isn't loaded. + from isaaclab.sim.utils.stage import get_current_stage - fix_reversed_joints(omni.usd.get_context().get_stage()) + fix_reversed_joints(get_current_stage()) return articulation, translations diff --git a/source/isaaclab_newton/test/assets/test_rigid_object_collection.py b/source/isaaclab_newton/test/assets/test_rigid_object_collection.py index 18928bedb5d8..c200311a88f3 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object_collection.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object_collection.py @@ -7,14 +7,8 @@ # pyright: reportPrivateUsage=none -"""Launch Isaac Sim Simulator first.""" - -from isaaclab.app import AppLauncher - -# launch omniverse app -simulation_app = AppLauncher(headless=True).app - -"""Rest everything follows.""" +"""Kitless newton-only test: no AppLauncher boot. See test_articulation.py +for rationale.""" import sys