Skip to content

Commit 556e431

Browse files
Fix fragment writers to modify existing rigid-body, collision, and mass prims (#6501)
# Description The `apply_rigid_body_properties`, `apply_collision_properties`, and `apply_mass_properties` fragment family writers force-applied their defining USD API (`RigidBodyAPI` / `CollisionAPI` / `MassAPI`) directly on the input prim. The legacy writers they replace are `@apply_nested`: they modify the prims that *already carry* the API and never author a fresh one. For bare-prim spawns (shapes, meshes) the define-fresh behavior is correct, which is why all existing tests passed. The failure needs a real USD asset: robot assets author `RigidBodyAPI` on their **link** prims and (commonly) `ArticulationRootAPI` on the **spawn** prim. Configuring `rigid_props` as a fragment list on such an asset force-anchored a new rigid body on the spawn prim, turning every link into a nested rigid body — the PhysX parser then drops the articulation's joint graph (`Articulation` initialization fails with an empty joint registry). Reproduced A/B on the cartpole asset. ## Fix Resolve targets the way the legacy nested writers do, mirroring the articulation-root writer's existing pattern: collect subtree prims already carrying the defining API (breadth-first, without descending past a match — nested applications of the same physics schema are not allowed), dispatch every fragment to each; apply a fresh API on the input prim **only** when the subtree carries none (presence-gated), preserving the bare-prim spawner behavior. Instanced prims are skipped with a warning. The shared resolution lives in one helper used by all three writers. ## Tests New `test_schema_writer_nested_targets.py` (all fail before the fix, pass after): - fragment `rigid_props`/`collision_props`/`mass_props` through the production USD spawn path on an articulated asset: attributes land on the links/colliders, the spawn prim gains no anchor; - fragment vs legacy path on the same asset: identical per-prim applied-schema sets and authored attributes; - bare-prim define-fresh preserved; traversal does not descend past an existing body. Existing suites re-run green: schema/collision/mass/mesh-collision fragments, spawn meshes, schemas (101 tests total). ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source/<pkg>/changelog.d/` - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Octi Zhang <zhengyuz@nvidia.com>
1 parent 7257bc9 commit 556e431

3 files changed

Lines changed: 398 additions & 47 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed :func:`~isaaclab.sim.schemas.apply_rigid_body_properties`,
5+
:func:`~isaaclab.sim.schemas.apply_collision_properties`, and
6+
:func:`~isaaclab.sim.schemas.apply_mass_properties` force-applying their defining USD API on
7+
the input prim. They now modify the prims in the subtree that already carry the API, matching
8+
the legacy nested writers, and only apply a fresh API on the input prim when the subtree
9+
carries none. Previously, passing a fragment list for ``rigid_props`` on a USD asset whose
10+
spawn prim carries the articulation root turned the asset's links into nested rigid bodies,
11+
and the PhysX parser dropped the articulation's joints.
12+
* Fixed :func:`~isaaclab.sim.schemas.apply_collision_properties` unconditionally returning
13+
``True``. It now raises ``ValueError`` on an invalid prim path and reports fragment failures
14+
and skipped instanced prims in its return value, matching
15+
:func:`~isaaclab.sim.schemas.apply_rigid_body_properties` and
16+
:func:`~isaaclab.sim.schemas.apply_mass_properties`.

source/isaaclab/isaaclab/sim/schemas/schemas.py

Lines changed: 138 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,77 @@ def modify_articulation_root_properties(
542542
return True
543543

544544

545+
"""
546+
Fragment-writer helpers.
547+
"""
548+
549+
550+
def _resolve_fragment_targets(
551+
prim_path: str,
552+
api_type,
553+
stage: Usd.Stage,
554+
apply_fresh: bool,
555+
) -> tuple[list, bool]:
556+
"""Resolve the prims that a fragment family writer should author on.
557+
558+
This mirrors the legacy nested writers: prims under ``prim_path`` that already carry
559+
``api_type`` are modified in place, and the search does not descend past a match, since
560+
nested applications of the same physics schema are not allowed. Only when the subtree
561+
carries no such prim is the defining API applied freshly to ``prim_path`` itself -- the
562+
bare-prim case used by the shape and mesh spawners. Instanced prims cannot be edited, so
563+
they are skipped with a warning.
564+
565+
Args:
566+
prim_path: The prim path whose subtree is searched.
567+
api_type: The defining USD API schema type (e.g. ``UsdPhysics.RigidBodyAPI``).
568+
stage: The stage containing the prim.
569+
apply_fresh: Whether to apply ``api_type`` to ``prim_path`` when the subtree contains
570+
no carrier of it.
571+
572+
Returns:
573+
A tuple ``(targets, any_skipped)`` holding the writable target prims and whether any
574+
instanced prim was skipped.
575+
576+
Raises:
577+
ValueError: When the prim path is not valid.
578+
"""
579+
prim = stage.GetPrimAtPath(prim_path)
580+
if not prim.IsValid():
581+
raise ValueError(f"Prim path '{prim_path}' is not valid.")
582+
# breadth-first search that stops descending at the first carrier on each branch, like the
583+
# legacy nested writers: nested applications of the same physics schema are not allowed, so
584+
# nothing below a carrier is ever a target. This keeps the search linear in the subtree size
585+
# instead of collecting every carrier and pruning afterwards. Carriers on instanced prims are
586+
# read-only and collected separately so they can be reported.
587+
targets = []
588+
skipped = []
589+
prims_queue = [prim]
590+
index = 0
591+
while index < len(prims_queue):
592+
candidate = prims_queue[index]
593+
index += 1
594+
if candidate.HasAPI(api_type):
595+
if candidate.IsInstance() or candidate.IsInstanceProxy():
596+
skipped.append(candidate)
597+
else:
598+
targets.append(candidate)
599+
continue
600+
prims_queue += candidate.GetFilteredChildren(Usd.TraverseInstanceProxies())
601+
if not targets and not skipped and apply_fresh:
602+
if prim.IsInstance() or prim.IsInstanceProxy():
603+
skipped.append(prim)
604+
else:
605+
api_type.Apply(prim)
606+
targets.append(prim)
607+
if skipped:
608+
logger.warning(
609+
"Skipping %s updates on instanced prims: %s.",
610+
api_type.__name__,
611+
[p.GetPath().pathString for p in skipped],
612+
)
613+
return targets, bool(skipped)
614+
615+
545616
"""
546617
Rigid body properties.
547618
"""
@@ -550,34 +621,41 @@ def modify_articulation_root_properties(
550621
def apply_rigid_body_properties(
551622
prim_path: str, fragments: Iterable[schemas_cfg.RigidBodyFragment], stage: Usd.Stage | None = None
552623
) -> bool:
553-
"""Apply a list of rigid-body fragments to a prim.
624+
"""Apply a list of rigid-body fragments to the rigid bodies under a prim.
554625
555-
Applies ``UsdPhysics.RigidBodyAPI`` as the implicit anchor (the defining schema for a rigid
556-
body), then dispatches each fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
557-
Backend fragments carry backend-specific funcs, so core never imports a backend.
626+
Existing rigid bodies in the subtree are modified in place, matching the legacy nested
627+
writer. Real assets author ``UsdPhysics.RigidBodyAPI`` on their link prims, so anchoring a
628+
fresh rigid body on the spawn prim instead would nest those links under a new body and the
629+
PhysX parser would drop the articulation's joint graph. Only when the subtree carries no
630+
rigid body is ``UsdPhysics.RigidBodyAPI`` applied to ``prim_path`` itself -- the bare-prim
631+
case used by the shape and mesh spawners. Each fragment is dispatched to every resolved
632+
target via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend fragments carry
633+
backend-specific funcs, so core never imports a backend.
558634
559635
Args:
560-
prim_path: The prim path to apply the rigid-body schemas on.
636+
prim_path: The prim path whose subtree is searched for rigid bodies.
561637
fragments: An iterable of :class:`~isaaclab.sim.schemas.RigidBodyFragment` instances.
562638
stage: The stage where to find the prim. Defaults to None, in which case the current
563639
stage is used.
564640
565641
Returns:
566-
True if the properties were successfully set.
642+
True if every target and fragment succeeded and no instanced prim was skipped.
643+
644+
Raises:
645+
ValueError: When the prim path is not valid.
567646
"""
647+
fragments = list(fragments)
568648
if stage is None:
569649
stage = get_current_stage()
570-
prim = stage.GetPrimAtPath(prim_path)
571-
# fail loudly on an invalid path (matches the legacy define_rigid_body_properties writer)
572-
if not prim.IsValid():
573-
raise ValueError(f"Prim path '{prim_path}' is not valid.")
574-
if not UsdPhysics.RigidBodyAPI(prim):
575-
UsdPhysics.RigidBodyAPI.Apply(prim)
576-
# aggregate per-fragment results so a reported failure is not masked by the always-applied anchor
577-
success = True
578-
for cfg in fragments:
579-
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
580-
success = bool(func(cfg, prim_path, stage)) and success
650+
targets, any_skipped = _resolve_fragment_targets(
651+
prim_path, UsdPhysics.RigidBodyAPI, stage, apply_fresh=bool(fragments)
652+
)
653+
# aggregate per-target, per-fragment results so a reported failure is not masked
654+
success = not any_skipped
655+
for target in targets:
656+
target_path = target.GetPath().pathString
657+
for cfg in fragments:
658+
success = bool(cfg.func(cfg, target_path, stage)) and success
581659
return success
582660

583661

@@ -778,31 +856,41 @@ def modify_rigid_body_properties(
778856
def apply_collision_properties(
779857
prim_path: str, fragments: Iterable[schemas_cfg.CollisionFragment], stage: Usd.Stage | None = None
780858
) -> bool:
781-
"""Apply a list of collision fragments to a prim.
859+
"""Apply a list of collision fragments to the colliders under a prim.
782860
783-
Applies ``UsdPhysics.CollisionAPI`` as the implicit anchor (the defining schema for a
784-
collider), then dispatches each fragment via its
785-
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend fragments carry backend-specific
786-
funcs, so core never imports a backend.
861+
Existing colliders in the subtree are modified in place, matching the legacy nested writer.
862+
Real assets author ``UsdPhysics.CollisionAPI`` on their collider prims, so anchoring a fresh
863+
collider on the spawn prim would change the scene's collision geometry. Only when the
864+
subtree carries no collider is ``UsdPhysics.CollisionAPI`` applied to ``prim_path`` itself
865+
-- the bare-prim case used by the shape and mesh spawners. Each fragment is dispatched to
866+
every resolved target via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend
867+
fragments carry backend-specific funcs, so core never imports a backend.
787868
788869
Args:
789-
prim_path: The prim path to apply the collision schemas on.
870+
prim_path: The prim path whose subtree is searched for colliders.
790871
fragments: An iterable of :class:`~isaaclab.sim.schemas.CollisionFragment` instances.
791872
stage: The stage where to find the prim. Defaults to None, in which case the current
792873
stage is used.
793874
794875
Returns:
795-
True if the properties were successfully set.
876+
True if every target and fragment succeeded and no instanced prim was skipped.
877+
878+
Raises:
879+
ValueError: When the prim path is not valid.
796880
"""
881+
fragments = list(fragments)
797882
if stage is None:
798883
stage = get_current_stage()
799-
prim = stage.GetPrimAtPath(prim_path)
800-
if not UsdPhysics.CollisionAPI(prim):
801-
UsdPhysics.CollisionAPI.Apply(prim)
802-
for cfg in fragments:
803-
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
804-
func(cfg, prim_path, stage)
805-
return True
884+
targets, any_skipped = _resolve_fragment_targets(
885+
prim_path, UsdPhysics.CollisionAPI, stage, apply_fresh=bool(fragments)
886+
)
887+
# aggregate per-target, per-fragment results so a reported failure is not masked
888+
success = not any_skipped
889+
for target in targets:
890+
target_path = target.GetPath().pathString
891+
for cfg in fragments:
892+
success = bool(cfg.func(cfg, target_path, stage)) and success
893+
return success
806894

807895

808896
def define_collision_properties(
@@ -902,33 +990,36 @@ def modify_collision_properties(
902990
def apply_mass_properties(
903991
prim_path: str, fragments: Iterable[schemas_cfg.MassFragment], stage: Usd.Stage | None = None
904992
) -> bool:
905-
"""Apply a list of mass fragments to a prim.
993+
"""Apply a list of mass fragments to the mass-bearing prims under a prim.
906994
907-
Applies ``UsdPhysics.MassAPI`` as the implicit anchor (the defining schema for mass properties),
908-
then dispatches each fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
995+
Existing mass-bearing prims in the subtree are modified in place, matching the legacy nested
996+
writer, since real assets author ``UsdPhysics.MassAPI`` on their link prims. Only when the
997+
subtree carries none is ``UsdPhysics.MassAPI`` applied to ``prim_path`` itself -- the
998+
bare-prim case used by the shape and mesh spawners. Each fragment is dispatched to every
999+
resolved target via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
9091000
9101001
Args:
911-
prim_path: The prim path to apply the mass schemas on.
1002+
prim_path: The prim path whose subtree is searched for mass-bearing prims.
9121003
fragments: An iterable of :class:`~isaaclab.sim.schemas.MassFragment` instances.
9131004
stage: The stage where to find the prim. Defaults to None, in which case the current
9141005
stage is used.
9151006
9161007
Returns:
917-
True if the properties were successfully set.
1008+
True if every target and fragment succeeded and no instanced prim was skipped.
1009+
1010+
Raises:
1011+
ValueError: When the prim path is not valid.
9181012
"""
1013+
fragments = list(fragments)
9191014
if stage is None:
9201015
stage = get_current_stage()
921-
prim = stage.GetPrimAtPath(prim_path)
922-
# fail loudly on an invalid path (matches the legacy define_mass_properties writer)
923-
if not prim.IsValid():
924-
raise ValueError(f"Prim path '{prim_path}' is not valid.")
925-
if not UsdPhysics.MassAPI(prim):
926-
UsdPhysics.MassAPI.Apply(prim)
927-
# aggregate per-fragment results so a reported failure is not masked by the always-applied anchor
928-
success = True
929-
for cfg in fragments:
930-
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
931-
success = bool(func(cfg, prim_path, stage)) and success
1016+
targets, any_skipped = _resolve_fragment_targets(prim_path, UsdPhysics.MassAPI, stage, apply_fresh=bool(fragments))
1017+
# aggregate per-target, per-fragment results so a reported failure is not masked
1018+
success = not any_skipped
1019+
for target in targets:
1020+
target_path = target.GetPath().pathString
1021+
for cfg in fragments:
1022+
success = bool(cfg.func(cfg, target_path, stage)) and success
9321023
return success
9331024

9341025

0 commit comments

Comments
 (0)