Source code for isaaclab_newton.envs.mdp.events

# 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

"""Backend implementations of MDP event terms for Newton."""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal

import torch
import warp as wp
from newton import ModelFlags
from newton.solvers import SolverKamino

from isaaclab.envs.mdp.events import _GravityRandomization, _randomize_prop_by_op
from isaaclab.envs.mdp.visual_events import _compile_distribution
from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg
from isaaclab.utils import math as math_utils

from ... import assets
from ...physics.newton_manager import NewtonManager

if TYPE_CHECKING:
    from isaaclab.assets import Articulation, RigidObject
    from isaaclab.envs import ManagerBasedEnv


[docs] class randomize_rigid_body_material(ManagerTermBase): """Sample friction and restitution per shape. Newton uses one friction coefficient, so ``dynamic_friction_range``, ``num_buckets``, and ``make_consistent`` are ignored. Kamino shares materials across environments. It samples one value per original ``(mu, restitution)`` group and applies it to every environment, ignoring ``env_ids``. """
[docs] def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv) -> None: """Initialize the asset bindings and sampling state. Args: cfg: Event configuration. env: Environment owning this term. """ super().__init__(cfg, env) asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] asset: RigidObject | Articulation = env.scene[asset_cfg.name] self.asset = asset self.asset_cfg = asset_cfg self._newton_manager = env.sim.physics_manager # Capture material groups on the first call, before any randomized writes. self._kamino_group_inverse: torch.Tensor | None = None self._kamino_num_groups = 0 self._static_friction_range = cfg.params.get("static_friction_range", (1.0, 1.0)) self._restitution_range = cfg.params.get("restitution_range", (0.0, 0.0)) model = self._newton_manager.get_model() self._friction_binding = asset._root_view.get_attribute("shape_material_mu", model)[:, 0] # type: ignore self._restitution_binding = asset._root_view.get_attribute("shape_material_restitution", model)[:, 0] # type: ignore if isinstance(asset, assets.Articulation) and asset_cfg.body_ids != slice(None): # Shape counts use backend body order. num_shapes_per_body = asset.backend_num_shapes_per_body shape_indices_list = [] backend_body_ids = asset.map_body_ids_to_backend(asset_cfg.body_ids) for body_id in backend_body_ids: start_idx = sum(num_shapes_per_body[:body_id]) end_idx = start_idx + num_shapes_per_body[body_id] shape_indices_list.extend(range(start_idx, end_idx)) self._shape_indices = torch.tensor(shape_indices_list, dtype=torch.long) else: self._shape_indices = torch.arange(self._friction_binding.shape[1], dtype=torch.long)
def __call__( self, env: ManagerBasedEnv, env_ids: torch.Tensor | slice | None, static_friction_range: tuple[float, float], dynamic_friction_range: tuple[float, float], restitution_range: tuple[float, float], num_buckets: int, asset_cfg: SceneEntityCfg, make_consistent: bool = False, ) -> None: """Sample friction and restitution for the selected shapes. Args: env: Environment owning this term. env_ids: Environment selection; ignored by Kamino's shared material groups. None selects all environments. static_friction_range: Friction bounds captured at construction. dynamic_friction_range: Unused; Newton has a single friction coefficient. restitution_range: Restitution bounds captured at construction. num_buckets: Unused; Newton samples continuous values. asset_cfg: Asset and body selection resolved at construction. make_consistent: Unused; Newton has a single friction coefficient. """ device = env.device if env_ids is None: env_ids = slice(None) env_rows = env_ids if isinstance(env_ids, slice) else env_ids[:, None] num_shapes = len(self._shape_indices) shape_idx = self._shape_indices.to(device) friction_range = torch.tensor(self._static_friction_range, device=device) restitution_range_t = torch.tensor(self._restitution_range, device=device) friction_view = wp.to_torch(self._friction_binding) restitution_view = wp.to_torch(self._restitution_binding) num_envs = len(range(env.num_envs)[env_ids]) if isinstance(env_ids, slice) else len(env_ids) if isinstance(self._newton_manager._solver, SolverKamino): # Kamino shares each material group across all environments. if self._kamino_group_inverse is None: build_keys = torch.stack((friction_view[0, shape_idx], restitution_view[0, shape_idx]), dim=-1) _, inverse = torch.unique(build_keys, dim=0, return_inverse=True) self._kamino_group_inverse = inverse self._kamino_num_groups = int(inverse.max().item()) + 1 if inverse.numel() else 0 inverse = self._kamino_group_inverse friction_groups = math_utils.sample_uniform( friction_range[0], friction_range[1], (self._kamino_num_groups,), device=device ) restitution_groups = math_utils.sample_uniform( restitution_range_t[0], restitution_range_t[1], (self._kamino_num_groups,), device=device ) friction_view[:, shape_idx] = friction_groups[inverse] restitution_view[:, shape_idx] = restitution_groups[inverse] else: friction_samples = math_utils.sample_uniform( friction_range[0], friction_range[1], (num_envs, num_shapes), device=device ) restitution_samples = math_utils.sample_uniform( restitution_range_t[0], restitution_range_t[1], (num_envs, num_shapes), device=device ) friction_view[env_rows, shape_idx] = friction_samples restitution_view[env_rows, shape_idx] = restitution_samples self._newton_manager.add_model_change(ModelFlags.SHAPE_PROPERTIES)
[docs] class randomize_rigid_body_collider_offsets(ManagerTermBase): """Newton backend implementation for collider offset randomization. Maps PhysX concepts to Newton's geometry properties: - ``rest_offset`` -> ``shape_margin`` (Newton margin) - ``contact_offset`` -> ``shape_gap`` (Newton gap = contact_offset - margin) See the `Newton collision schema`_ for details. .. _Newton collision schema: https://newton-physics.github.io/newton/latest/concepts/collisions.html """
[docs] def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv) -> None: """Initialize the asset bindings and sampling state. Args: cfg: Event configuration. env: Environment owning this term. """ super().__init__(cfg, env) asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] asset: RigidObject | Articulation = env.scene[asset_cfg.name] self.asset = asset self._newton_manager = env.sim.physics_manager model = self._newton_manager.get_model() self._sim_bind_shape_margin = asset._root_view.get_attribute("shape_margin", model)[:, 0] # type: ignore self._sim_bind_shape_gap = asset._root_view.get_attribute("shape_gap", model)[:, 0] # type: ignore self.default_margin = wp.to_torch(self._sim_bind_shape_margin).clone() self.default_gap = wp.to_torch(self._sim_bind_shape_gap).clone()
def __call__( self, env: ManagerBasedEnv, env_ids: torch.Tensor | slice | None, asset_cfg: SceneEntityCfg, rest_offset_distribution_params: tuple[float, float] | None = None, contact_offset_distribution_params: tuple[float, float] | None = None, distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", ) -> None: """Sample offsets and translate them to Newton margins and gaps. Args: env: Environment owning this term. env_ids: Environment selection; None selects all environments. asset_cfg: Asset selection; collider randomization operates on every body. rest_offset_distribution_params: Rest offset distribution parameters [m]. contact_offset_distribution_params: Contact offset distribution parameters [m]. distribution: Sampling distribution for the offsets. """ if env_ids is None: env_ids = slice(None) margin_view = wp.to_torch(self._sim_bind_shape_margin) if rest_offset_distribution_params is not None: margin = self.default_margin.clone() margin = _randomize_prop_by_op( margin, rest_offset_distribution_params, None, slice(None), operation="abs", distribution=distribution, ) self.default_margin[env_ids] = margin[env_ids] margin_view[env_ids] = margin[env_ids] if contact_offset_distribution_params is not None: current_margin = self.default_margin contact_offset = torch.zeros_like(self.default_gap) contact_offset = _randomize_prop_by_op( contact_offset, contact_offset_distribution_params, None, slice(None), operation="abs", distribution=distribution, ) gap = torch.clamp(contact_offset - current_margin, min=0.0) self.default_gap[env_ids] = gap[env_ids] gap_view = wp.to_torch(self._sim_bind_shape_gap) gap_view[env_ids] = gap[env_ids] if rest_offset_distribution_params is not None or contact_offset_distribution_params is not None: self._newton_manager.add_model_change(ModelFlags.SHAPE_PROPERTIES)
[docs] class randomize_physics_scene_gravity(_GravityRandomization): """Randomize selected Newton worlds, leaving the global world unchanged. Add and scale operate on current gravity; repeated calls accumulate. Distribution is fixed at construction; distribution parameters [m/s^2] may change at runtime. """
[docs] def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv) -> None: """Initialize gravity sampling for the active simulation. Args: cfg: Event configuration. env: Environment owning this term. """ super().__init__(cfg, env, device=env.device) self._manager = env.sim.physics_manager
def __call__( self, env: ManagerBasedEnv, env_ids: torch.Tensor | slice | None, gravity_distribution_params: tuple[list[float], list[float]], operation: Literal["add", "scale", "abs"], distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", ) -> None: """Sample and set gravity. Args: env: Environment owning this term. env_ids: Environment selection; None selects all environments. gravity_distribution_params: Distribution parameters [m/s^2] for add/abs, dimensionless for scale. operation: Apply absolute values, add to current gravity, or scale current gravity. distribution: Sampling distribution; gravity terms cache this at construction. """ model = self._manager.get_model() if model is None or model.gravity is None: raise RuntimeError("Newton model is not initialized. Cannot randomize gravity.") # The trailing global-world row is not an environment. gravity = wp.to_torch(model.gravity)[: env.num_envs] if env_ids is None: env_ids = slice(None) selected = gravity[env_ids] if selected.shape[0] == 0: return gravity[env_ids] = self._sample_gravity(selected, gravity_distribution_params, operation) self._manager.add_model_change(ModelFlags.MODEL_PROPERTIES)
[docs] class randomize_visual_shape(ManagerTermBase): """Sample one color per selected link and write Newton shape storage on device."""
[docs] def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) channels = cfg.params["channels"] if tuple(channels) != ("color",): raise NotImplementedError("Newton per-shape randomization currently supports only the 'color' channel.") asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] asset = env.scene[asset_cfg.name] if isinstance(asset_cfg.body_ids, slice): ids = range(asset.num_bodies)[asset_cfg.body_ids] else: ids = asset_cfg.body_ids body_names = tuple(asset.body_names[index] for index in ids) self._writer = NewtonManager.create_visual_shape_color_writer(asset, body_names) self._sample = _compile_distribution(channels["color"], env.device) self._all_env_ids = torch.arange(env.num_envs, dtype=torch.int32, device=self._writer.device)
def __call__( self, env: ManagerBasedEnv, env_ids: torch.Tensor | slice | None, asset_cfg: SceneEntityCfg, channels: dict[str, tuple | dict], ) -> None: del asset_cfg, channels if env_ids is None: env_ids = slice(None) selected = self._all_env_ids[env_ids] if isinstance(env_ids, slice) else env_ids.to(dtype=torch.int32) model = NewtonManager.get_model() if self._writer.model is not model: self._writer.rebind(model) self._writer(self._sample((len(selected), self._writer.body_count)), selected)