隐式 MPM#

Newton 的隐式物质点法(MPM)求解器用于模拟沙、雪等颗粒材料。Isaac Lab 为其提供 Newton 求解器配置、粒子资产类型,以及与刚体求解器的可选耦合。MPM 支持和刚体–MPM 耦合目前均处于实验阶段。

本页介绍集成方式。可运行的场景见 示例与任务;分辨率、收敛性、材质参数研究及近刚性 MPM 极限见 MPM 求解器 。

MPM 如何接入 Newton 后端#

将 NewtonCfg 的 solver_cfg 设为 MPMSolverCfg ,会启用 NewtonMPMManager 。这是 Newton 管理器的专用实现,负责构建 Newton 的 SolverImplicitMPM。求解器将材料表示为粒子,并在背景体素网格上隐式求解其流变行为。MPM 管理器会对 Newton 管理器作如下配置:

  • 在同一个 Newton 状态对象中原地更新粒子,而不对状态使用双缓冲。

  • 在隐式求解过程中处理碰撞体接触,不使用 Newton 的碰撞处理管线。

  • 将刚体几何体视为碰撞体。它不是刚体或关节系统的动力学求解器,也不接受外部刚体力。模型构建完成前,运动学刚体的质量会被设为零,凸网格则以三角网格形式传入求解器。

与刚体耦合#

单独使用时,MPM 求解器只推进粒子。刚体充当碰撞体,其运动由其他机制提供,例如写入运动学位姿;材料不会对这些刚体施加反作用力。

双向交互使用 isaaclab_contrib.coupling 中的实验性耦合器。CouplerProxyCfg 将同一个 Newton 模型划分为若干命名条目:

  • 刚体条目,例如 MJWarp,负责机器人或物体的刚体;

  • MPM 条目负责粒子,必须设置 in_place=True;

  • 代理映射将选定的刚体作为碰撞体提供给 MPM 条目,并将产生的冲量反馈给刚体条目。

mpm-two-way-coupling 示例、snowball-smash 演示和 Franka Pour 任务均使用这一方式。代理耦合、ADMM 耦合、数据交换模式及调优见 耦合求解器 。

数据模型#

配置与 USD#

MPM 材料是一种场景资产。MPMObjectCfg 扩展了可变形对象配置,并使用粒子生成器:

  • MPMGridCfg 在局部坐标系中轴对齐的盒体内填充粒子点阵,分辨率由 voxel_size 和 particles_per_cell 决定。

  • MPMPointsCfg 将粒子放置在明确指定的局部位置,可选配速度、质量和半径。

两种生成器都会写入符合 schema 的 UsdGeom.Points 仿真几何体。网格配置仍先在 Isaac Lab 中生成点阵,再写入各点的位置、宽度、速度和质量。逐粒子的材料参数来自 MPMParticleMaterialCfg ,通过 NewtonMPMMaterialAPI 写入。阻尼参数的单位为 [s],写入值为 damping * young_modulus,单位为 [Pa·s]。critical_fraction 等求解器全局设置位于 MPMSolverCfg 中;其中可用 schema 表达的设置通过 NewtonMPMSceneAPI 写入物理场景 prim。

MPM 对象通过 Newton 的复制机制克隆。网格随机扰动在资产局部坐标系中使用固定种子生成一次,因此 USD 克隆共享相同的局部粒子分布。每个环境需要独立分布时,应使用重置事件或域随机化。

求解器网格与粒子生成器应使用相同的体素尺寸。和其他声明式资产一样,将生成的对象加入 InteractiveSceneCfg :

import isaaclab.sim as sim_utils
from isaaclab_newton.assets import MPMObjectCfg
from isaaclab_newton.physics import MPMSolverCfg, NewtonCfg
from isaaclab_newton.sim.spawners.mpm import MPMGridCfg

voxel_size = 0.02

sim_cfg = sim_utils.SimulationCfg(
    dt=1.0 / 100.0,
    physics=NewtonCfg(
        solver_cfg=MPMSolverCfg(
            voxel_size=voxel_size,
            max_iterations=100,
            tolerance=1.0e-4,
        ),
        num_substeps=2,
    ),
)

media = MPMObjectCfg(
    prim_path="{ENV_REGEX_NS}/Media",
    spawn=MPMGridCfg(
        lower=(-0.1, -0.1, 0.0),
        upper=(0.1, 0.1, 0.2),
        voxel_size=voxel_size,
        particles_per_cell=2.0,
        particle_placement="cell_center",
    ),
)

运行时资产#

MPMObject 实现 Isaac Lab 的可变形对象接口,因此场景重置、更新和状态操作可以像处理其他可变形体一样处理它。每个环境实例的粒子数相同,可通过 particles_per_object 获取。MPMObjectData 通过 particle_pos_w、particle_vel_w 和 particle_state_w 提供仿真世界坐标系中的粒子位置、速度和组合状态,可变形体的 nodal_* 属性是这些属性的别名。root_pos_w 和 root_vel_w 分别是每个实例内所有粒子位置和速度的平均值。位置、速度和状态的写入会立即生效,不支持可变形体运动学目标。

配置参考#

字段及默认值以自动生成的 MPMSolverCfg 参考为准。各字段可分为以下几组:

类别

字段

作用

分辨率

voxel_size,以及生成器中的 particles_per_cell

背景网格和粒子的分辨率,决定精度、活跃网格单元数量和内存占用。

网格存储

grid_type、grid_padding、max_active_cell_count、max_*_node_count 容量限制,以及 separate_worlds

稀疏、稠密或固定网格的分配方式、预留容量,以及是否为每个仿真世界分配独立的局部网格。

流变求解

max_iterations、tolerance、solver 和 warmstart_mode

迭代上限、收敛容差、求解器或热启动流程,以及热启动数据来源。

离散化

transfer_scheme、integration_scheme、velocity_basis、strain_basis 和 collider_basis

粒子与网格之间的数据传递、形函数支撑域,以及各场使用的基函数。

材料与背景

critical_fraction 和 air_drag

求解器全局的屈服面坍缩参数,以及空区域的数值阻力。

碰撞体

collider_velocity_mode、collider_normal_from_sdf_gradient 和 project_outside_colliders

碰撞体速度和法线的计算方式,以及每个子步结束后将粒子投影到碰撞体外部的可选操作。

隐式求解已负责处理碰撞体接触。project_outside_colliders 额外在步进后强制校正仍处于碰撞体内部的粒子。仅在确实需要这种几何校正时使用它,不能用它替代正确的初始状态、碰撞几何体或稳定的时间步长。

CUDA 图捕获#

仅当网格存储满足图捕获的稳定性要求时,MPM 步进才支持 CUDA 图捕获:

  • 使用 "fixed" 网格,且 max_active_cell_count 为正值;或者

  • 使用 "sparse" 网格,max_active_cell_count 为正值,设置 grid_padding=0,并使用 "Q1" 速度基函数。应变基函数名称必须以 "pic" 开头,或为 "P0"、"P1d"、"Q1d"、"Q1" 之一。碰撞体基函数名称必须以 "pic" 开头,或为 "Q1"、"S2"、"S3" 之一。

稠密网格和未设置容量上限的网格不使用图捕获。

重置#

单独使用 MPM 求解器时,资产自动重置不会清除求解器历史,因为共享同一网格的仿真世界无法分别重置各自的历史。使用耦合器时,自动重置会调用耦合求解器的重置方法,将逐世界的重置掩码传递给包括 MPM 在内的所有条目。Franka Pour 等需要完整重置历史的任务,会使用独立的 MPM 仿真世界(separate_worlds=True),在写入完整状态后调用 reset_solver_state() 。在耦合运行中,这也会重置耦合求解器的历史。

渲染粒子表面#

MPM 仿真状态始终是一组粒子。表面重建是可选的可视化步骤,不会改变粒子运动、碰撞或材质行为。运行茶壶演示可对比可用模式:

# Reconstructed surface
uv run isaaclab demo teapot-fill \
  --visualizer newton_gl --fluid_render_mode surface
# Surface and source particles together
uv run isaaclab demo teapot-fill \
  --visualizer newton_gl --fluid_render_mode both
# Path-traced translucent surface
uv run --extra ovrtx isaaclab demo teapot-fill \
  --visualizer newton_rtx --fluid_render_mode surface

茶壶演示默认显示重建表面。选择 particles 或 both ,可在 Newton GL 或 Newton RTX 中查看原始 MPM 状态。

Newton GL 和 Newton RTX 可视化器支持表面渲染,Kit 可视化器仍直接渲染 MPM 粒子。

这段录制展示的是渲染外观,并非另一种本构模型。

茶壶演示在 sim.reset() 后配置 newton.geometry.ParticleSurface ,并在 sim.render() 前更新网格。启用 CUDA 捕获时,其渲染器会捕获表面提取操作,并在查看器的帧生命周期内暂存动态网格。

重建参数应独立于仿真调节:

  • voxel_size 控制表面细节和内存用量,可以小于 MPM 求解器的体素大小。

  • kernel_radius 控制每个粒子对表面的影响范围。建议从粒子间距的三倍左右开始。

  • max_grid_cells 为 CUDA 图捕获提供固定容量的存储。如果重建区域超出预留网格,请增大该值。

  • 各向异性核能更好地保留薄片和拉长的流体特征,但开销高于各向同性核。

茶壶演示处理了 CUDA 图捕获、空表面、未激活粒子和动态拓扑:

茶壶表面渲染器的实现
class FluidSurfaceRenderer:
    """Extract and display a dynamic water surface in Newton visualizers."""

    def __init__(self, sim) -> None:
        import warp as wp
        from isaaclab_newton.physics import NewtonManager
        from isaaclab_visualizers.newton import NewtonGLVisualizer, NewtonRTXVisualizer
        from newton.geometry import ParticleSurface

        self._wp = wp
        self._visualizers = tuple(
            visualizer
            for visualizer in sim.visualizers
            if isinstance(visualizer, (NewtonGLVisualizer, NewtonRTXVisualizer))
        )
        if not self._visualizers:
            raise RuntimeError("Particle surface rendering requires a Newton GL or RTX visualizer.")

        self._model = NewtonManager.get_model()
        self._state = NewtonManager.get_state_0()
        self._surface = ParticleSurface(
            voxel_size=SURFACE_VOXEL_SIZE,
            max_grid_cells=SURFACE_MAX_GRID_CELLS,
            world_count=max(self._model.world_count, 1),
            kernel_radius=SURFACE_KERNEL_RADIUS,
            threshold=0.4,
            smooth_lambda=0.0,
            anisotropic=True,
            kernel_scale=0.5,
            anisotropy_ratio=16.0,
            anisotropy_scale=1.0,
            anisotropy_min_neighbors=4,
            anisotropy_binning=True,
            anisotropy_strength=0.95,
            field_smooth_iterations=0,
            mesh_smooth_iterations=1,
            device=self._model.device,
        )
        self._empty_points = wp.empty(0, dtype=wp.vec3, device=self._model.device)
        self._empty_indices = wp.empty(0, dtype=wp.int32, device=self._model.device)
        self._empty_normals = wp.empty(0, dtype=wp.vec3, device=self._model.device)
        self._surface_mesh = None
        self._surface_graph = None
        self._capture_surface_extraction()

    def _extract_surface(self):
        """Extract the water surface from the current Newton particle state."""
        return self._surface.extract(
            self._state.particle_q,
            self._model.particle_radius,
            particle_flags=self._model.particle_flags,
            particle_world=self._model.particle_world if self._surface.world_count > 1 else None,
        )

    def _capture_surface_extraction(self) -> None:
        """Capture reconstruction separately from the MPM physics graph."""
        if not self._model.device.is_cuda or args_cli.disable_cuda_graph:
            return
        self._surface_mesh = self._extract_surface()
        with self._wp.ScopedCapture(device=self._model.device) as capture:
            self._surface_mesh = self._extract_surface()
        self._surface_graph = capture.graph

    def update(self) -> int:
        """Reconstruct and publish the current water surface, returning its triangle count."""
        if self._surface_graph is None:
            self._surface_mesh = self._extract_surface()
        else:
            self._wp.capture_launch(self._surface_graph)

        vertices, indices, normals = self._surface_mesh.to_arrays()
        if vertices is None:
            vertices = self._empty_points
            indices = self._empty_indices
            normals = self._empty_normals
            hidden = True
            triangle_count = 0
        else:
            hidden = False
            triangle_count = indices.shape[0] // 3

        for visualizer in self._visualizers:
            visualizer.log_mesh(
                SURFACE_PATH,
                vertices,
                indices,
                normals=normals,
                hidden=hidden,
                backface_culling=False,
                color=WATER_COLOR,
                roughness=0.1,
                metallic=0.0,
                dynamic=True,
                opacity=WATER_OPACITY,
            )
        return triangle_count

示例与任务#

这些示例和演示介绍 MPM 的构建与交互,默认使用 Newton GL 进行交互式查看。启动器默认使用 cuda:0;可通过 --device cuda:N 选择其他 GPU。

颗粒下落
uv run isaaclab example mpm-granular
双向耦合球坑
uv run isaaclab example mpm-two-way-coupling
雪球撞击
uv run isaaclab demo snowball-smash
填充茶壶
uv run isaaclab demo teapot-fill --fluid_render_mode particles

Franka 倒料任务#

IsaacContrib-Franka-Pour 任务从重置数据文件恢复回合,该文件包含相互衔接的 14 阶段重置分布。首次使用时,会从标准 Isaac Lab 资产根目录下载含 20,000 行的标准数据文件,因此训练无需另行准备该文件:

uv run isaaclab train --rl_library rsl_rl --task IsaacContrib-Franka-Pour \
  --num_envs 2048

仓库中的生成器是复现或自定义该分布的可执行参考。在 L40S 级别的 GPU 上约需两分钟,它会写入一个可显式选择的本地数据文件:

uv run python scripts/tools/generate_franka_pour_reset_dataset.py
uv run isaaclab train --rl_library rsl_rl --task IsaacContrib-Franka-Pour \
  --num_envs 2048 \
  env.reset_dataset_path=datasets/franka_pour/reset_dataset.pt

任务会自动验证数据中保存的内容摘要。设置 ISAACSIM_ASSET_ROOT 可将标准数据文件的查找位置改为兼容的本地或自托管资产目录树。自定义的可复现实验仍可固定摘要。

源容器使用不参与碰撞的解析填充体积,其高度由 env.source_fill_level 控制,取值范围为 (0, 1] 。默认 0.70 表示填充至约 70% 的杯高;粒子数量随指定填充比例变化,而体素大小和粒子间距保持固定。 env.pour_target_frac 独立控制当前粒子总量中必须到达接收容器的比例。

使用标准任务配置,在 Kit 中运行检查点;无需外部回调或粒子覆盖配置:

uv run --extra isaacsim isaaclab play --rl_library rsl_rl --task IsaacContrib-Franka-Pour \
  --checkpoint /path/to/model.pt --num_envs 1 --visualizer kit

限制#

  • 耦合的 MPM 条目必须原地步进,不支持 project_outside_colliders。