Skip to content

Add Newton deformable object API (modular, contrib-based)#5383

Open
xiangdonglai wants to merge 19 commits intoisaac-sim:developfrom
xiangdonglai:donglaix/deformable_experimental
Open

Add Newton deformable object API (modular, contrib-based)#5383
xiangdonglai wants to merge 19 commits intoisaac-sim:developfrom
xiangdonglai:donglaix/deformable_experimental

Conversation

@xiangdonglai
Copy link
Copy Markdown

@xiangdonglai xiangdonglai commented Apr 23, 2026

Summary

This PR adds Newton deformable object support (cloth and volumetric soft bodies with coupled rigid-deformable simulation) using a modular, layered architecture. It is a restructured version of #5287 that splits the monolithic implementation into clean layers with the bulk of the Newton-specific logic in isaaclab_contrib.

Fixes #5285

Motivation

The original PR (#5287) placed all deformable Newton logic inside isaaclab_newton. This restructured version follows a plugin architecture: core abstractions live in isaaclab, minimal hooks in isaaclab_newton, and all Newton deformable implementation in isaaclab_contrib. This keeps isaaclab_newton free of deformable-specific code and makes the feature opt-in.

Architecture

```
isaaclab (core abstractions)

isaaclab_newton (minimal hooks, no deformable logic)

isaaclab_contrib/deformable (all Newton deformable implementation)
```

No circular dependencies. Each layer only imports from layers below it.

What stays in `isaaclab` core

Backend-agnostic infrastructure needed by both PhysX and Newton deformable:

  • New base classes: `BaseDeformableObject`, `BaseDeformableObjectData`, `DeformableObjectCfg`, plus factory stubs (`DeformableObject`, `DeformableObjectData`) for backend dispatch
  • Material configs: `DeformableBodyMaterialCfg`, `SurfaceDeformableBodyMaterialCfg`, `NewtonDeformableMaterialCfg`, `OmniPhysicsDeformableMaterialCfg` and `spawn_deformable_body_material()`
  • Spawner support: `DeformableObjectSpawnerCfg`, `MeshSquareCfg`, deformable props in mesh spawners
  • Schema updates: `DeformableBodyPropertiesCfg`, `define_deformable_body_properties()`
  • Bug fix: `add_usd_reference()` defaultPrim fallback in `sim/utils/prims.py`

What stays in `isaaclab_newton` (minimal hooks only)

Small, additive changes to `NewtonManager` and `newton_replicate.py` that let `isaaclab_contrib` plug in without modifying the manager later. No deformable logic, no behavioral change for rigid-only scenes.

  • Deformable registry list, solver factory dict, dirty flags
  • Hook points: `_particle_sync_fn`, `_post_finalize_model_fn`, `_post_start_simulation_fn`
  • `register_solver_factory()` classmethod
  • `_per_world_builder_hooks` / `_post_replicate_hooks` lists
  • Hook loops in `newton_replicate.py` replacing hardcoded deformable calls
  • `ignore_paths` support for deformable prims during USD import

What lives in `isaaclab_contrib/deformable` (new)

All Newton-specific deformable implementation, registered as plugins via hooks:

  • `DeformableObject` / `DeformableObjectData`: Newton backend implementation with particle state read/write, kinematic targets, debug visualization
  • `CoupledSolver`: alternates rigid (Featherstone/MuJoCo) and VBD solvers per substep, supports one-way and two-way coupling with normal + Coulomb friction
  • `VBDSolverCfg` / `CoupledSolverCfg` / `NewtonModelCfg`: solver and model configuration
  • Warp kernels: gather/scatter particles, kinematic target enforcement, mean position computation
  • `sync_particles_to_usd()`: writes Newton particle positions to Fabric for Kit viewport rendering
  • `setup_fabric_particle_sync()`: sets up Fabric attributes for deformable particle sync after simulation starts (registered via `_post_start_simulation_fn` hook)
  • Cloner hooks: `per_world_deformable_hook`, `post_replicate_deformable_hook`
  • `register_hooks()`: explicit activation function called by task/tutorial modules before pxr-dependent code runs (deferred to avoid pxr-before-Kit crashes)
  • Backend auto-dispatch: registers `"newton"` backend for `DeformableObject` factory

Tasks (`isaaclab_tasks`)

  • `Isaac-Pick-Cloth-Direct-v0`: Franka robot interacting with cloth using coupled solver
  • `Isaac-Pick-VBD-Cube-Direct-v0`: Franka robot interacting with a deformable cube

Tests

  • `test_deformable_object.py` — 17 tests covering initialization, nodal state read/write, kinematic targets, transforms
  • `test_rigid_deformable_coupling.py` — 3 tests covering one-way coupling, two-way coupling, and contact deflection

Type of change

  • New feature (non-breaking change which adds functionality)

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

…article sync

Create isaaclab_experimental/deformable/ module containing:
- DeformableObject and DeformableObjectData (Newton backend)
- Warp kernels for particle gather/scatter operations
- CoupledSolver for rigid-body + VBD cloth interaction
- VBDSolverCfg, CoupledSolverCfg, NewtonModelCfg configs
- Solver factory functions registered with NewtonManager
- Particle sync for USD/Fabric viewport rendering
- Cloner hooks for deformable body replication
- Model cfg hook for post-finalize parameter application

Bug fixes included:
- init_pos/init_rot zeroed after Xform bake to prevent double-application
- vis_mesh_prim fallback for empty vis_candidates (surface cloth)
- No weakref in data class (direct references for particle_q/qd)
- model_cfg always applied (not gated behind contact attributes)
Add test_deformable_object.py and test_rigid_deformable_coupling.py
under isaaclab_experimental/test/deformable/ with imports adapted for
the experimental module layout.  Introduce register_hooks() in the
deformable __init__ so hooks survive NewtonManager.clear() across
test fixtures.  Include pre-commit formatting fixes.
Add two deformable object manipulation tasks to isaaclab_tasks_experimental:
- Isaac-Pick-Cloth-Direct-v0: Franka robot + cloth (shirt) with coupled
  MJWarp + VBD solver
- Isaac-Pick-VBD-Cube-Direct-v0: Franka robot + deformable cube with
  coupled MJWarp + VBD solver

Both tasks use DeformableNewtonCfg (NewtonCfg subclass with model_cfg
field) and import isaaclab_experimental.deformable to trigger hook
registration. Also add isaaclab_tasks_experimental import to
zero_agent.py for task discovery.
Update the deformable tutorial to support both PhysX and Newton
backends via --backend argument. Use core isaaclab.assets imports
for backend-agnostic DeformableObject, and import VBDSolverCfg
from isaaclab_experimental for the Newton backend path.
Add _get_deformable_ignore_paths() to skip deformable sim/visual mesh
prims in builder.add_usd() calls, preventing Newton from creating
redundant static mesh colliders for deformable bodies. Also add
rebuild_bvh() call in _simulate_physics_only() for solvers that
require BVH rebuilds (e.g. VBD cloth).
Gracefully handle particle-only scenes (no rigid bodies) by logging a
warning instead of raising RuntimeError when body_label/body_key is
empty. Add Fabric particle sync setup for deformable bodies: resize
visual mesh topology to match sim mesh, and create per-instance
particle offset attributes so the sync kernel can write points.
Defer heavy imports in isaaclab_experimental.deformable behind
__getattr__ so pxr is not loaded before Kit starts.  Ensure
register_hooks() always re-registers after NewtonManager.clear()
by resetting the _hooks_registered guard on each explicit call.

Fix SurfaceDeformableBodyMaterialCfg class-identity mismatch in
from_files.py that caused isinstance() to always fail, making PhysX
create volume hierarchies for surface cloth.

In deformable_object.py, prefer the original authored Mesh over the
PhysX sim_mesh proxy (which has no points at construction time) when
a surface deformable material is configured.

In newton_replicate.py, pass deformable prim paths as ignore_paths to
add_usd so Newton's USD importer skips PhysX proxy meshes.
Replace bare `import isaaclab_experimental.deformable` with explicit
`register_hooks()` calls since deferred imports no longer trigger
hook registration on import.
The Newton deformable implementation (solvers, kernels, particle sync,
cloner hooks) and its tests now live under isaaclab_contrib.  All import
references in tutorials, task envs, and the isaaclab_newton re-export
stub are updated accordingly.
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 adds a modular Newton deformable object API (cloth and volumetric soft bodies) with a plugin architecture: core abstractions in isaaclab, minimal hooks in isaaclab_newton, and all Newton-specific implementation in isaaclab_contrib. The architecture is sound, but there are several correctness issues in state management, potential data races, and missing error handling that need attention before merging.

Architecture Impact

Cross-module impact is significant:

  • isaaclab.assets.deformable_object introduces new base classes and factory pattern for backend dispatch
  • isaaclab.sim.schemas adds define_deformable_body_properties() with pytetwild tetrahedralization
  • isaaclab.sim.spawners.meshes adds TetMesh class and tet mesh spawners
  • isaaclab.sim.spawners.materials adds deformable body material configs and spawner
  • isaaclab_newton.physics.NewtonManager modified with hooks, registries, and solver factories
  • isaaclab_contrib.deformable contains all Newton-specific deformable logic

The factory pattern via FactoryBase enables backend dispatch but requires careful initialization ordering.

Implementation Verdict

Significant concerns — Several correctness issues need addressing before ship.

Test Coverage

The PR includes 20 tests across two files:

  • test_deformable_object.py: 17 tests covering initialization, state read/write, kinematic targets, transforms
  • test_rigid_deformable_coupling.py: 3 tests for one-way/two-way coupling and contact deflection

Missing coverage:

  • No tests for the new define_deformable_body_properties() schema function
  • No tests for TetMesh class construction (cuboid, from_file)
  • No tests for spawn_deformable_body_material()
  • No tests for the MeshFromFileCfg spawner
  • No regression test for the add_usd_reference() defaultPrim fallback fix

CI Status

No CI checks available yet.

Findings

🔴 Critical: source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py:413-414 — Missing self.sim.forward() sync after state reset can cause stale state

In pick_cloth_env.py:398-430, after writing robot state to sim, self.sim.forward() is called, but the deformable state sync to state_1 (lines 403-430) happens after this. The cloth write_nodal_state_to_sim_index at line 428 writes to both state_0 and state_1, but the robot state sync to state_1 at lines 407-423 reads from state_0 which may not reflect the forward() result correctly. The ordering should be: write robot state → forward() → sync robot to state_1 → write cloth state.

# pick_cloth_env.py:398-428 - forward() is called before state_1 sync
self.sim.forward()  # line 398

# Then state_1 is synced from state_0 (lines 407-423)
# But state_0 was just modified by forward()

🔴 Critical: source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object_data.py:88-99 — Race condition in lazy property evaluation

The nodal_pos_w property writes to self._nodal_pos_w.data via a Warp kernel, but the timestamp check and update is not atomic. If update() is called from another thread while a kernel is in-flight, the timestamp could be updated before the kernel completes:

# Line 92-99
if self._nodal_pos_w.timestamp < self._sim_timestamp:
    wp.launch(...)  # Async kernel launch
    self._nodal_pos_w.timestamp = self._sim_timestamp  # Updated before kernel completes

This needs wp.synchronize() before the timestamp update or use of a completion flag.

🔴 Critical: source/isaaclab/isaaclab/sim/schemas/schemas.py:1123-1128pytetwild tetrahedralization can produce invalid meshes

The define_deformable_body_properties() calls pytetwild.tetrahedralize() with hardcoded parameters that may fail for certain mesh geometries. There's no error handling if tetrahedralization fails or produces degenerate tets:

tet_mesh_points, tet_mesh_indices = tetrahedralize(
    vertices,
    faces.reshape(-1, 3),
    edge_length_fac=0.2,  # May be too coarse for small meshes
    # No try/except for tetrahedralization failures
)

🟡 Warning: source/isaaclab_contrib/isaaclab_contrib/deformable/__init__.py:61-67 — Hook registration in _do_deferred_imports() modifies global state unsafely

The deferred import mechanism modifies NewtonManager class attributes (_particle_sync_fn, _per_world_builder_hooks, etc.) without synchronization. If multiple threads import simultaneously, hooks could be registered multiple times or incompletely:

# Lines 61-67 in _register_hooks_impl
NewtonManager._particle_sync_fn = sync_particles_to_usd
if per_world_deformable_hook not in NewtonManager._per_world_builder_hooks:
    NewtonManager._per_world_builder_hooks.append(per_world_deformable_hook)

🟡 Warning: source/isaaclab_contrib/isaaclab_contrib/deformable/coupled_solver.py:296-303_MAX_REACTION_CONTACTS=2048 may overflow

The reaction kernel is launched with a fixed dimension of 2048, but if soft_contact_count[0] exceeds this, contacts will be silently ignored. The kernel early-exits for tid >= contact_count[0], but there's no warning or dynamic resizing:

wp.launch(
    _kernel_body_particle_reaction,
    dim=_MAX_REACTION_CONTACTS,  # Fixed at 2048
    ...
)

🟡 Warning: source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py:543-567spawn_mesh_from_file uses naive fan triangulation

The triangulation logic at lines 560-567 assumes convex quads for fan triangulation, which produces incorrect geometry for non-convex polygons:

# Manual fan triangulation - incorrect for non-convex faces
for i in range(1, count - 1):
    tris.append([face_indices[idx], face_indices[idx + i], face_indices[idx + i + 1]])

🔵 Improvement: source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py:285-295 — Redundant _deformable_type check in _register_deformable

The _is_surface_material check at line 285 and the subsequent tet_prims/mesh_prims logic is complex and has overlapping conditions. Consider consolidating the deformable type determination:

# Lines 285-295 have complex nested conditions that could be simplified
_is_surface_material = (
    self.cfg.spawn is not None
    and getattr(self.cfg.spawn, "physics_material", None) is not None
    and "Surface" in type(self.cfg.spawn.physics_material).__name__
)

🔵 Improvement: source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/direct/pick_cloth/pick_cloth_env_cfg.py:14-17 — Hardcoded path to Newton assets may fail in distribution

The shirt USD path is constructed from the Newton package location, which may not be portable:

_newton_spec = importlib.util.find_spec("newton")
_SHIRT_USD = os.path.join(
    os.path.dirname(_newton_spec.origin),
    "examples", "assets", "unisex_shirt.usd",
)

This will fail if Newton is installed as a zip or if the examples directory is not distributed.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR adds Newton deformable object support (cloth and volumetric soft bodies) using a layered plugin architecture: core abstractions in isaaclab, minimal hooks in isaaclab_newton, and all Newton-specific implementation in isaaclab_contrib/deformable. Two P1 bugs were found in the new isaaclab_contrib code.

  • Two-way friction is silently incorrect: _apply_reactions in coupled_solver.py passes state.particle_q for both the particle_q and particle_q_prev kernel arguments, making the relative-slip term always zero and friction forces purely body-velocity-driven instead of properly computing particle-body relative motion.
  • Multi-deformable particle sync broken: sync_particles_to_usd uses _deformable_registry[0].particles_per_body as num_points for all Fabric prims — scenes with two or more deformable objects of differing particle counts will sync incorrect particle ranges for every object after the first.

Confidence Score: 4/5

Safe to merge for rigid-only scenes; two P1 bugs affect two-way friction correctness and multi-deformable particle sync that should be fixed before production use of the new deformable feature.

Two P1 findings: incorrect particle_q_prev aliasing silently breaks Coulomb friction in two-way coupling mode, and num_points hardcoded from the first registry entry breaks Fabric sync for mixed-particle-count scenes. Both are confined to isaaclab_contrib and do not affect existing rigid-body functionality. The broader architecture is solid.

source/isaaclab_contrib/isaaclab_contrib/deformable/coupled_solver.py (friction bug) and source/isaaclab_contrib/isaaclab_contrib/deformable/particle_sync.py (multi-deformable num_points bug)

Important Files Changed

Filename Overview
source/isaaclab_contrib/isaaclab_contrib/deformable/coupled_solver.py New coupled rigid+VBD solver; _apply_reactions passes state.particle_q for both particle_q and particle_q_prev, making friction always zero-slip (P1 bug)
source/isaaclab_contrib/isaaclab_contrib/deformable/particle_sync.py New Fabric particle sync; num_points hardcoded from first deformable registry entry — incorrect for scenes with multiple deformables of differing particle counts (P1 bug)
source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py Newton DeformableObject implementation with registry, initialization, and write methods; kinematic target loop is slow (P2) but logic is otherwise sound
source/isaaclab_contrib/isaaclab_contrib/deformable/init.py Deferred-import module with hook registration; correctly avoids pxr-before-Kit crashes; register_hooks re-registration pattern is clear
source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py Adds hook lists, solver factory registry, _particles_dirty flag, and Fabric particle setup; changes are additive and non-breaking for rigid-only scenes
source/isaaclab/isaaclab/assets/deformable_object/base_deformable_object.py New abstract base class with index/mask write variants and deprecation wrappers; well-structured and consistent with existing AssetBase pattern
source/isaaclab/isaaclab/sim/utils/prims.py Bug fix: add_usd_reference now falls back to first pseudo-root child when USD file has no defaultPrim, preventing silent empty references
source/isaaclab_contrib/isaaclab_contrib/deformable/kernels.py Warp kernels for gather/scatter/kinematic enforcement; logic is sound and dimensions are well-documented
source/isaaclab_contrib/isaaclab_contrib/deformable/cloner_hooks.py Per-world and post-replicate hooks for deformable builder registration; correctly records particle offsets per environment
source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object_data.py Newton DeformableObjectData with lazy-evaluated warp buffers for nodal position, velocity, and state; timestamp invalidation is correctly handled

Sequence Diagram

sequenceDiagram
    participant Task as Task/Env
    participant DO as DeformableObject
    participant NM as NewtonManager
    participant CS as CoupledSolver
    participant FS as Fabric/USD

    Task->>DO: __init__(cfg)
    DO->>NM: _deformable_registry.append(entry)

    Task->>NM: finalize_model()
    NM->>NM: post_finalize_model_fn() [apply_model_cfg]
    NM->>CS: create solver via _solver_factories
    NM->>FS: setup Fabric particle attrs (newton:particleOffset)

    loop Each simulation step
        Task->>NM: step()
        NM->>CS: rebuild_bvh(state_0)
        NM->>CS: solver.step(state_in, state_out, ...)
        Note over CS: one_way: rigid → collide → VBD<br/>two_way: collide → reactions → rigid → VBD
        NM->>NM: _mark_state_dirty()
    end

    Task->>NM: pre_render()
    NM->>NM: sync_transforms_to_usd() [rigid bodies]
    NM->>FS: sync_particles_to_usd() [deformables via Warp kernel]

    Task->>DO: write_nodal_kinematic_target_to_sim_index(targets)
    DO->>DO: update nodal_kinematic_target buffer
    Task->>NM: step() [write_data_to_sim enforces targets]
    DO->>NM: enforce_kinematic_targets kernel → state_0, state_1
Loading

Reviews (1): Last reviewed commit: "Move deformable module from isaaclab_exp..." | Re-trigger Greptile

Comment on lines +284 to +310

# 5. Clear spurious particle forces from rigid step
state_in.particle_f.zero_()

# 6. VBD step -- uses same contacts detected in step 2
self.vbd.step(state_in, state_out, control, self.contacts, dt)

def _rigid_step(self, state_in: State, state_out: State, control: Control, dt: float) -> None:
"""Advance rigid bodies with the configured sub-solver."""
model = self._model

saved_particle_count = model.particle_count
model.particle_count = 0

self.rigid_solver.step(state_in, state_out, control, None, dt)

model.particle_count = saved_particle_count

def _apply_reactions(self, state: State, dt: float) -> None:
"""Launch the reaction kernel to inject normal + friction forces into body_f."""
model = self._model
contacts = self.contacts

wp.launch(
_kernel_body_particle_reaction,
dim=_MAX_REACTION_CONTACTS,
inputs=[
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 particle_q_prev aliased to particle_q, zeroing friction slip

_apply_reactions passes state.particle_q for both the particle_q and particle_q_prev kernel arguments. Inside _kernel_body_particle_reaction, friction is computed as:

dx = particle_q[p_idx] - particle_q_prev[p_idx]   # always 0 when same array
relative_translation = dx - bv * dt                 # reduces to -bv * dt

Because both arguments point to the same array, dx is always zero, so the relative slip between particle and body surface is never measured — friction becomes purely a function of the body velocity, not the actual particle-body relative motion. The docstring says this kernel "mirrors the complete contact model from evaluate_body_particle_contact()", which uses a dedicated previous-position buffer. A separate particle_q_prev snapshot must be maintained (e.g. clone state.particle_q before the rigid step and pass it here) for the friction term to be physically correct.

Comment thread source/isaaclab_contrib/isaaclab_contrib/deformable/particle_sync.py Outdated
Comment thread source/isaaclab_contrib/isaaclab_contrib/deformable/deformable_object.py Outdated
Moves the two deformable task environments from isaaclab_tasks_experimental
to isaaclab_tasks so they are discovered by zero_agent.py without needing
an extra import of the experimental package.
… code

Restore dt=0.01 in SimulationCfg, update env prim comment to explain
Newton's env_\d+ naming requirement, restore original reset() ordering,
and remove stale commented-out loop.
Remove TetMesh class, spawn_tet_mesh_cuboid, spawn_tet_mesh_from_file,
spawn_mesh_from_file, _spawn_tet_mesh_geom_from_tet_mesh and their
config counterparts. None are used by any task, test, or tutorial.
Also restore Newton docstring in modify_deformable_body_properties.
The isaaclab_newton/assets/deformable_object/__init__.py stub re-exported
DeformableObject from isaaclab_contrib, creating a reverse dependency.
This is unnecessary since register_hooks() already registers the Newton
backend with the DeformableObject factory directly.
Extract the 50-line inline block that sets up Fabric attributes for
deformable particle sync from NewtonManager.start_simulation() into
setup_fabric_particle_sync() in isaaclab_contrib/deformable/particle_sync.
Registered via a new _post_start_simulation_fn hook, keeping
isaaclab_newton free of deformable-specific logic.
Cherry-pick Mike's fixes from #4:

- Move visual mesh topology overwrite to define_deformable_body_properties
  in schemas.py so it happens at USD authoring time, not simulation start.
- Add per-instance newton:particleCount Fabric attribute so the sync kernel
  supports heterogeneous deformable bodies instead of assuming a single
  particles_per_body for all instances.
- Simplify setup_fabric_particle_sync() to only create Fabric attributes.
…e logging

- Replace CPU-bound Python for-loop in write_nodal_kinematic_target_to_sim_index
  with vectorized torch advanced indexing (eliminates per-env GPU-CPU sync).
- Log sync_particles_to_usd failures at warning level on first occurrence,
  then throttle to debug for subsequent failures.
- Cache usdrt module import to avoid re-importing every render frame.
- Add clarifying comment on particle_q_prev aliasing in two-way coupling
  friction (known limitation, acceptable for one-way primary use case).
- Document _MAX_REACTION_CONTACTS fixed upper bound in coupled_solver.
@xiangdonglai xiangdonglai force-pushed the donglaix/deformable_experimental branch from 717693e to 28f4b25 Compare April 24, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant