@@ -656,16 +656,42 @@ def randomize_rigid_body_com(
656656class 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
761854class randomize_physics_scene_gravity (ManagerTermBase ):
0 commit comments