Source code for isaaclab_newton.physics.mpm_manager

# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Implicit MPM Newton manager."""

from __future__ import annotations

import warp as wp
from newton import BodyFlags, Contacts, Control, Model, ModelBuilder, State
from newton.solvers import SolverImplicitMPM
from warp.fem import TemporaryStore

from .mpm_manager_cfg import MPMSolverCfg
from .newton_manager import NewtonManager


def _make_solver_config(solver_cfg: MPMSolverCfg) -> SolverImplicitMPM.Config:
    """Build Newton's implicit MPM solver config from Isaac Lab's cfg."""
    return SolverImplicitMPM.Config(
        max_iterations=solver_cfg.max_iterations,
        tolerance=solver_cfg.tolerance,
        solver=solver_cfg.solver,
        warmstart_mode=solver_cfg.warmstart_mode,
        collider_velocity_mode=solver_cfg.collider_velocity_mode,
        voxel_size=solver_cfg.voxel_size,
        grid_type=solver_cfg.grid_type,
        grid_padding=solver_cfg.grid_padding,
        max_active_cell_count=solver_cfg.max_active_cell_count,
        transfer_scheme=solver_cfg.transfer_scheme,
        integration_scheme=solver_cfg.integration_scheme,
        critical_fraction=solver_cfg.critical_fraction,
        air_drag=solver_cfg.air_drag,
        collider_normal_from_sdf_gradient=solver_cfg.collider_normal_from_sdf_gradient,
        collider_basis=solver_cfg.collider_basis,
        strain_basis=solver_cfg.strain_basis,
        velocity_basis=solver_cfg.velocity_basis,
    )


[docs] class NewtonMPMManager(NewtonManager): """:class:`NewtonManager` specialization for Newton's implicit MPM solver. MPM advances particle materials in-place and treats rigid geometry as colliders, so it does not consume Newton's rigid-body collision pipeline and steps with a single :class:`State`. """ _project_outside_colliders: bool = False """Whether :meth:`_step_solver` projects particles out of colliders each substep. Set from :attr:`MPMSolverCfg.project_outside_colliders` in :meth:`_build_solver` and read in :meth:`_step_solver`. """ @classmethod def _register_builder_attributes(cls, builder: ModelBuilder) -> None: """Register the particle custom attributes required by :class:`SolverImplicitMPM`. Implicit MPM materials are configured per-particle through Newton custom attributes (``mpm:young_modulus``, ``mpm:viscosity``, ...). These must be present on the builder *before* particles are added so that ``add_particles(custom_attributes=...)`` succeeds and so that ``builder.finalize()`` allocates the matching model arrays. Idempotent: ``has_custom_attribute`` guards against re-registration when the hook is invoked multiple times (e.g. once via :meth:`create_builder` and again via :meth:`start_simulation`). """ if not builder.has_custom_attribute("mpm:young_modulus"): SolverImplicitMPM.register_custom_attributes(builder) @classmethod def _prepare_builder_for_finalize(cls, builder: ModelBuilder) -> None: """Normalize kinematic rigid bodies before MPM solver construction. Newton's implicit MPM solver treats positive-mass body colliders as finite-mass colliders. Isaac Lab kinematic assets can import with a computed mass, so clear mass and inertia for kinematic bodies to match Newton's direct-builder MPM examples. """ kinematic_flag = int(BodyFlags.KINEMATIC) for body_id, flags in enumerate(builder.body_flags): if int(flags) & kinematic_flag: builder.body_mass[body_id] = 0.0 builder.body_inv_mass[body_id] = 0.0 builder.body_inertia[body_id] = wp.mat33() builder.body_inv_inertia[body_id] = wp.mat33() @classmethod def _build_solver(cls, model: Model, solver_cfg: MPMSolverCfg) -> None: """Construct :class:`SolverImplicitMPM` and populate the base-class slots. MPM steps in-place on a single :class:`State` and runs collision handling internally, so it neither double-buffers state nor drives Newton's :class:`CollisionPipeline`. Args: model: Finalized Newton model the solver should run on. solver_cfg: Implicit MPM solver configuration. """ NewtonManager._solver = SolverImplicitMPM( model, _make_solver_config(solver_cfg), temporary_store=TemporaryStore(), ) NewtonManager._use_single_state = True NewtonManager._needs_collision_pipeline = False NewtonManager._needs_fk_before_step = True cls._project_outside_colliders = solver_cfg.project_outside_colliders @classmethod def _supports_cuda_graph_capture(cls) -> bool: """Return ``True`` only for fixed-grid MPM. Sparse and dense grids reallocate as particles move, which is not capturable in a CUDA graph; the fixed grid keeps a static topology. """ return cls._solver.grid_type == "fixed" @classmethod def _step_solver( cls, state_0: State, state_1: State, control: Control, contacts: Contacts | None, substep_dt: float ) -> None: """Run one implicit MPM substep, optionally projecting particles out of colliders. The implicit solve already resolves colliders at the grid level. When :attr:`MPMSolverCfg.project_outside_colliders` is set, the manager also runs ``project_outside`` after the step (as in Newton's MPM examples) to hard-project particles out of collider interiors. The flag is evaluated when the step is first run, so the chosen branch is baked into any captured CUDA graph. """ cls._solver.step(state_0, state_1, control, contacts, substep_dt) if cls._project_outside_colliders: cls._solver.project_outside(state_1, state_1, substep_dt) @classmethod def _solver_specific_clear(cls) -> None: """Reset MPM-specific class state on teardown. :meth:`_build_solver` sets :attr:`_project_outside_colliders` from the active config. Resetting it here keeps a teardown-only :meth:`clear` (without a follow-up rebuild) from leaving a stale value on the class, mirroring how :meth:`NewtonManager.clear` resets the base-class flags. """ cls._project_outside_colliders = False