Skip to content

Commit 2d0d975

Browse files
ooctipusAntoineRichard
authored andcommitted
support newton randomize_rigid_body_collider_offsets
1 parent 5e0379d commit 2d0d975

1 file changed

Lines changed: 142 additions & 49 deletions

File tree

source/isaaclab/isaaclab/envs/mdp/events.py

Lines changed: 142 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -656,16 +656,42 @@ def randomize_rigid_body_com(
656656
class randomize_rigid_body_collider_offsets(ManagerTermBase):
657657
"""Randomize the collider parameters of rigid bodies by setting random values.
658658
659-
Supports both PhysX (rest/contact offset) and Newton (shape_margin/shape_gap).
660-
For Newton, ``rest_offset`` maps to ``shape_margin`` and ``contact_offset`` is
661-
converted to ``shape_gap`` via ``gap = contact_offset - margin``.
659+
This function allows randomizing the collider parameters of the asset, such as rest and contact offsets.
660+
These correspond to the physics engine collider properties that affect collision checking.
661+
662+
Automatically detects the active physics backend (PhysX or Newton) and applies the appropriate
663+
collider offset randomization strategy:
664+
665+
- **PhysX**: Uses rest offset and contact offset directly via the PhysX tensor API
666+
(``root_view.set_rest_offsets`` / ``root_view.set_contact_offsets``).
667+
- **Newton**: Maps PhysX concepts to Newton's geometry properties. PhysX ``rest_offset``
668+
maps to Newton ``shape_margin``, and PhysX ``contact_offset`` is converted to Newton
669+
``shape_gap`` via ``gap = contact_offset - margin``.
670+
See the `Newton collision schema`_ for details on this mapping.
671+
672+
The function samples random values from the given distribution parameters and applies them
673+
as absolute values to the collider properties. If the distribution parameters are not
674+
provided for a particular property, the function does not modify it.
675+
676+
.. _Newton collision schema: https://newton-physics.github.io/newton/latest/concepts/collisions.html
662677
663678
.. tip::
664-
It is recommended to use this function only during environment initialization.
679+
This function uses CPU tensors (PhysX) or GPU tensors (Newton) to assign the collision
680+
properties. It is recommended to use this function only during the initialization of
681+
the environment.
665682
"""
666683

667684
def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
668-
from isaaclab.assets import BaseArticulation, BaseRigidObject
685+
"""Initialize the term.
686+
687+
Args:
688+
cfg: The configuration of the event term.
689+
env: The environment instance.
690+
691+
Raises:
692+
ValueError: If the asset is not a RigidObject or an Articulation.
693+
"""
694+
from isaaclab.assets import BaseArticulation, BaseRigidObject # noqa: PLC0415
669695

670696
super().__init__(cfg, env)
671697

@@ -678,48 +704,40 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
678704
f" '{self.asset_cfg.name}' with type: '{type(self.asset)}'."
679705
)
680706

681-
if "newton" in env.sim.physics_manager.__name__.lower():
707+
manager_name = env.sim.physics_manager.__name__.lower()
708+
self._backend = "newton" if "newton" in manager_name else "physx"
709+
710+
if self._backend == "newton":
682711
self._init_newton()
683712
else:
684713
self._init_physx()
685714

686715
def _init_physx(self) -> None:
687-
asset = self.asset
688-
self._default_rest = wp.to_torch(asset.root_view.get_rest_offsets()).clone() # type: ignore[union-attr]
689-
self._default_contact = wp.to_torch(asset.root_view.get_contact_offsets()).clone() # type: ignore[union-attr]
690-
self._env_ids_device = "cpu"
691-
self._write_rest = lambda data, ids: asset.root_view.set_rest_offsets(data, ids) # type: ignore[union-attr]
692-
self._write_contact = lambda data, ids: asset.root_view.set_contact_offsets(data, ids) # type: ignore[union-attr]
716+
"""Initialize PhysX-specific state: cache default offsets."""
717+
asset: PhysXArticulation | PhysXRigidObject | PhysXRigidObjectCollection = self.asset # type: ignore[assignment]
718+
self.default_rest_offsets = wp.to_torch(asset.root_view.get_rest_offsets()).clone()
719+
self.default_contact_offsets = wp.to_torch(asset.root_view.get_contact_offsets()).clone()
693720

694721
def _init_newton(self) -> None:
695-
import isaaclab_newton.physics.newton_manager as nm
696-
from newton.solvers import SolverNotifyFlags
697-
698-
notify = lambda: nm.NewtonManager.add_model_change(SolverNotifyFlags.SHAPE_PROPERTIES) # noqa: E731
699-
700-
model = nm.NewtonManager.get_model()
701-
margin_buf = self.asset._root_view.get_attribute("shape_margin", model)[:, 0] # type: ignore[union-attr]
702-
gap_buf = self.asset._root_view.get_attribute("shape_gap", model)[:, 0] # type: ignore[union-attr]
703-
704-
self._default_rest = wp.to_torch(margin_buf).clone()
705-
self._default_contact = wp.to_torch(gap_buf).clone()
706-
self._env_ids_device = self._default_rest.device
707-
margin_view = wp.to_torch(margin_buf)
708-
gap_view = wp.to_torch(gap_buf)
722+
"""Initialize Newton-specific state: bind to shape_margin and shape_gap."""
723+
import isaaclab_newton.physics.newton_manager as newton_manager_module # noqa: PLC0415
724+
from newton.solvers import SolverNotifyFlags # noqa: PLC0415
709725

710-
def _write_margin(data: torch.Tensor, ids: torch.Tensor) -> None:
711-
self._default_rest[ids] = data[ids]
712-
margin_view[ids] = data[ids]
713-
notify()
726+
self._newton_manager = newton_manager_module.NewtonManager
727+
self._notify_shape_properties = SolverNotifyFlags.SHAPE_PROPERTIES
714728

715-
def _write_gap(data: torch.Tensor, ids: torch.Tensor) -> None:
716-
gap = torch.clamp(data - self._default_rest, min=0.0)
717-
self._default_contact[ids] = gap[ids]
718-
gap_view[ids] = gap[ids]
719-
notify()
729+
asset: NewtonArticulation | NewtonRigidObject | NewtonRigidObjectCollection = self.asset # type: ignore[assignment]
730+
model = self._newton_manager.get_model()
731+
# TODO(newton-collider-api): We access root_view.get_attribute directly to create
732+
# bindings for shape_margin and shape_gap because Newton doesn't yet expose dedicated
733+
# APIs for these properties on its asset classes. This mirrors the approach used for
734+
# material properties in randomize_rigid_body_material. When Newton adds proper
735+
# getter/setter APIs for collision geometry properties, update this code.
736+
self._sim_bind_shape_margin = asset._root_view.get_attribute("shape_margin", model)[:, 0]
737+
self._sim_bind_shape_gap = asset._root_view.get_attribute("shape_gap", model)[:, 0]
720738

721-
self._write_rest = _write_margin
722-
self._write_contact = _write_gap
739+
self.default_margin = wp.to_torch(self._sim_bind_shape_margin).clone()
740+
self.default_gap = wp.to_torch(self._sim_bind_shape_gap).clone()
723741

724742
def __call__(
725743
self,
@@ -730,32 +748,107 @@ def __call__(
730748
contact_offset_distribution_params: tuple[float, float] | None = None,
731749
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
732750
):
751+
if self._backend == "newton":
752+
self._call_newton(
753+
env, env_ids, rest_offset_distribution_params, contact_offset_distribution_params, distribution
754+
)
755+
else:
756+
self._call_physx(
757+
env, env_ids, rest_offset_distribution_params, contact_offset_distribution_params, distribution
758+
)
759+
760+
def _call_physx(
761+
self,
762+
env: ManagerBasedEnv,
763+
env_ids: torch.Tensor | None,
764+
rest_offset_params: tuple[float, float] | None,
765+
contact_offset_params: tuple[float, float] | None,
766+
distribution: Literal["uniform", "log_uniform", "gaussian"],
767+
) -> None:
768+
"""Apply collider offset randomization via PhysX's tensor API."""
769+
asset: PhysXRigidObject | PhysXArticulation | PhysXRigidObjectCollection = self.asset # type: ignore[assignment]
733770
if env_ids is None:
734-
env_ids = torch.arange(env.scene.num_envs, device=self._env_ids_device)
771+
env_ids = torch.arange(env.scene.num_envs, device="cpu")
735772
else:
736-
env_ids = env_ids.to(self._env_ids_device)
773+
env_ids = env_ids.cpu()
737774

738-
if rest_offset_distribution_params is not None:
739-
data = _randomize_prop_by_op(
740-
self._default_rest.clone(),
741-
rest_offset_distribution_params,
775+
if rest_offset_params is not None:
776+
rest_offset = self.default_rest_offsets.clone()
777+
rest_offset = _randomize_prop_by_op(
778+
rest_offset,
779+
rest_offset_params,
742780
None,
743781
slice(None),
744782
operation="abs",
745783
distribution=distribution,
746784
)
747-
self._write_rest(data, env_ids)
785+
asset.root_view.set_rest_offsets(rest_offset, env_ids)
748786

749-
if contact_offset_distribution_params is not None:
750-
data = _randomize_prop_by_op(
751-
self._default_contact.clone(),
752-
contact_offset_distribution_params,
787+
if contact_offset_params is not None:
788+
contact_offset = self.default_contact_offsets.clone()
789+
contact_offset = _randomize_prop_by_op(
790+
contact_offset,
791+
contact_offset_params,
753792
None,
754793
slice(None),
755794
operation="abs",
756795
distribution=distribution,
757796
)
758-
self._write_contact(data, env_ids)
797+
asset.root_view.set_contact_offsets(contact_offset, env_ids)
798+
799+
def _call_newton(
800+
self,
801+
env: ManagerBasedEnv,
802+
env_ids: torch.Tensor | None,
803+
rest_offset_params: tuple[float, float] | None,
804+
contact_offset_params: tuple[float, float] | None,
805+
distribution: Literal["uniform", "log_uniform", "gaussian"],
806+
) -> None:
807+
"""Apply collider offset randomization via Newton's shape geometry properties.
808+
809+
Maps PhysX concepts to Newton (see ``SchemaResolverPhysx`` in newton):
810+
811+
- ``rest_offset`` → ``shape_margin`` (Newton margin)
812+
- ``contact_offset`` → ``shape_gap`` (Newton gap = contact_offset - rest_offset)
813+
"""
814+
device = env.device
815+
if env_ids is None:
816+
env_ids = torch.arange(env.scene.num_envs, device=device, dtype=torch.int32)
817+
else:
818+
env_ids = env_ids.to(device)
819+
820+
margin_view = wp.to_torch(self._sim_bind_shape_margin)
821+
822+
if rest_offset_params is not None:
823+
margin = self.default_margin.clone()
824+
margin = _randomize_prop_by_op(
825+
margin,
826+
rest_offset_params,
827+
None,
828+
slice(None),
829+
operation="abs",
830+
distribution=distribution,
831+
)
832+
self.default_margin[env_ids] = margin[env_ids]
833+
margin_view[env_ids] = margin[env_ids]
834+
835+
if contact_offset_params is not None:
836+
current_margin = self.default_margin
837+
contact_offset = torch.zeros_like(self.default_gap)
838+
contact_offset = _randomize_prop_by_op(
839+
contact_offset,
840+
contact_offset_params,
841+
None,
842+
slice(None),
843+
operation="abs",
844+
distribution=distribution,
845+
)
846+
gap = torch.clamp(contact_offset - current_margin, min=0.0)
847+
self.default_gap[env_ids] = gap[env_ids]
848+
gap_view = wp.to_torch(self._sim_bind_shape_gap)
849+
gap_view[env_ids] = gap[env_ids]
850+
851+
self._newton_manager.add_model_change(self._notify_shape_properties)
759852

760853

761854
class randomize_physics_scene_gravity(ManagerTermBase):

0 commit comments

Comments
 (0)