diff --git a/source/isaaclab_ovphysx/changelog.d/antoiner-feat-ovphysx_articulation_view.major.rst b/source/isaaclab_ovphysx/changelog.d/antoiner-feat-ovphysx_articulation_view.major.rst new file mode 100644 index 000000000000..789881fe48f4 --- /dev/null +++ b/source/isaaclab_ovphysx/changelog.d/antoiner-feat-ovphysx_articulation_view.major.rst @@ -0,0 +1,13 @@ +Changed +^^^^^^^ + +* **Breaking:** :attr:`~isaaclab_ovphysx.assets.Articulation.root_view` now returns an + :class:`~isaaclab_ovphysx.sim.views.OvPhysxView` binding manager instead of a raw + ``dict`` mapping ``TensorType`` to ``TensorBinding``. The view owns all tensor-binding + creation, caching, reads, and writes for the articulation. Address bindings by attribute + name or ``TensorType`` member through + :meth:`~isaaclab_ovphysx.sim.views.OvPhysxView.try_binding_for` / + :meth:`~isaaclab_ovphysx.sim.views.OvPhysxView.get_attribute` rather than indexing the + dict, e.g. replace ``root_view[tensor_type]`` with + ``root_view.try_binding_for(tensor_type)`` and ``tensor_type in root_view`` with + ``root_view.try_binding_for(tensor_type) is not None``. diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation.py index e61bec9b7381..0ee81535a5b9 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation.py @@ -32,6 +32,7 @@ from isaaclab_ovphysx.assets.kernels import _body_wrench_to_world from isaaclab_ovphysx.cloner import queue_ovphysx_replication from isaaclab_ovphysx.physics import OvPhysxManager +from isaaclab_ovphysx.sim.views.ovphysx_view import OvPhysxView from .articulation_data import ArticulationData from .kernels import ( @@ -86,9 +87,9 @@ def __init__(self, cfg: ArticulationCfg): """ super().__init__(cfg) queue_ovphysx_replication(cfg) - # bindings are populated eagerly in ``_initialize_impl``; the dict - # also caches any tensor type the user explicitly queries later - self._bindings: dict[int, Any] = {} + # the binding manager is created in ``_initialize_impl``; it owns all + # TensorBinding creation, caching, and the CPU/GPU device policy. + self._root_view: OvPhysxView | None = None """ Properties @@ -148,20 +149,23 @@ def body_names(self) -> list[str]: return self._body_names @property - def root_view(self) -> dict[int, Any]: + def root_view(self) -> OvPhysxView: """Root view for the asset. - OVPhysX exposes per-tensor-type bindings rather than a single opaque view object - as used by the PhysX and Newton backends. Callers that need low-level binding - access should call :meth:`_get_binding` rather than iterating this dict directly. - For high-level state access (instance counts, prim paths, transforms), use the - :attr:`num_instances`, :attr:`body_names`, and :attr:`~ArticulationData.root_link_pose_w` - accessors instead. + On OVPhysX this is an :class:`~isaaclab_ovphysx.sim.views.OvPhysxView`: a + string-keyed binding manager over the per-tensor-type ``TensorBinding`` handles, + rather than the single opaque view object used by the PhysX and Newton backends. + Address attributes by their lowercased ``TensorType`` name (e.g. + ``root_view.get_attribute("articulation_dof_stiffness")``) or by the + :class:`~isaaclab_ovphysx.tensor_types.TensorType` member itself. For high-level + state access (instance counts, prim paths, transforms), prefer the + :attr:`num_instances`, :attr:`body_names`, and + :attr:`~ArticulationData.root_link_pose_w` accessors instead. .. note:: Use this view with caution. It requires handling of tensors in a specific way. """ - return self._bindings + return self._root_view @property def instantaneous_wrench_composer(self) -> WrenchComposer: @@ -238,22 +242,21 @@ def write_data_to_sim(self) -> None: outputs=[self._wrench_buf], device=self._device, ) - binding = self._get_binding(TT.LINK_WRENCH) - if binding is not None: - binding.write(self._wrench_buf) + if self._get_binding(TT.LINK_WRENCH) is not None: + self._root_view.set_attribute(TT.LINK_WRENCH, self._wrench_buf) inst.reset() # apply actuator models self._apply_actuator_model() # write actions into simulation (zeros are safe when no actuators are active) - if self._effort_binding is not None: - self._effort_binding.write(self._effort_write_view) + if self._effort_write_view is not None: + self._root_view.set_attribute(TT.DOF_ACTUATION_FORCE, self._effort_write_view) # position and velocity targets only for implicit actuators if self._has_implicit_actuators: - if self._pos_target_binding is not None: - self._pos_target_binding.write(self._pos_target_write_view) - if self._vel_target_binding is not None: - self._vel_target_binding.write(self._vel_target_write_view) + if self._pos_target_write_view is not None: + self._root_view.set_attribute(TT.DOF_POSITION_TARGET, self._pos_target_write_view) + if self._vel_target_write_view is not None: + self._root_view.set_attribute(TT.DOF_VELOCITY_TARGET, self._vel_target_write_view) def update(self, dt: float) -> None: """Updates the simulation data. @@ -452,8 +455,7 @@ def write_root_link_pose_to_sim_index( # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose() - binding = self._get_binding(TT.ROOT_POSE) - binding.write(self.data._root_link_pose_w.data.view(wp.float32), indices=env_ids) + self._root_view.set_attribute(TT.ROOT_POSE, self.data._root_link_pose_w.data.view(wp.float32), indices=env_ids) def write_root_link_pose_to_sim_mask( self, @@ -492,8 +494,7 @@ def write_root_link_pose_to_sim_mask( # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose() - binding = self._get_binding(TT.ROOT_POSE) - binding.write(self.data._root_link_pose_w.data.view(wp.float32), mask=env_mask_wp) + self._root_view.set_attribute(TT.ROOT_POSE, self.data._root_link_pose_w.data.view(wp.float32), mask=env_mask_wp) def write_root_com_pose_to_sim_index( self, @@ -533,8 +534,7 @@ def write_root_com_pose_to_sim_index( # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose(from_link=False) - binding = self._get_binding(TT.ROOT_POSE) - binding.write(self.data._root_link_pose_w.data.view(wp.float32), indices=env_ids) + self._root_view.set_attribute(TT.ROOT_POSE, self.data._root_link_pose_w.data.view(wp.float32), indices=env_ids) def write_root_com_pose_to_sim_mask( self, @@ -574,8 +574,7 @@ def write_root_com_pose_to_sim_mask( # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose(from_link=False) - binding = self._get_binding(TT.ROOT_POSE) - binding.write(self.data._root_link_pose_w.data.view(wp.float32), mask=env_mask_wp) + self._root_view.set_attribute(TT.ROOT_POSE, self.data._root_link_pose_w.data.view(wp.float32), mask=env_mask_wp) def write_root_velocity_to_sim_index( self, @@ -676,8 +675,9 @@ def write_root_com_velocity_to_sim_index( # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity() - binding = self._get_binding(TT.ROOT_VELOCITY) - binding.write(self.data._root_com_vel_w.data.view(wp.float32), indices=env_ids) + self._root_view.set_attribute( + TT.ROOT_VELOCITY, self.data._root_com_vel_w.data.view(wp.float32), indices=env_ids + ) def write_root_com_velocity_to_sim_mask( self, @@ -719,8 +719,9 @@ def write_root_com_velocity_to_sim_mask( # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity() - binding = self._get_binding(TT.ROOT_VELOCITY) - binding.write(self.data._root_com_vel_w.data.view(wp.float32), mask=env_mask_wp) + self._root_view.set_attribute( + TT.ROOT_VELOCITY, self.data._root_com_vel_w.data.view(wp.float32), mask=env_mask_wp + ) def write_root_link_velocity_to_sim_index( self, @@ -768,8 +769,9 @@ def write_root_link_velocity_to_sim_index( # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity(from_com=False) - binding = self._get_binding(TT.ROOT_VELOCITY) - binding.write(self.data._root_com_vel_w.data.view(wp.float32), indices=env_ids) + self._root_view.set_attribute( + TT.ROOT_VELOCITY, self.data._root_com_vel_w.data.view(wp.float32), indices=env_ids + ) def write_root_link_velocity_to_sim_mask( self, @@ -817,8 +819,9 @@ def write_root_link_velocity_to_sim_mask( # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity(from_com=False) - binding = self._get_binding(TT.ROOT_VELOCITY) - binding.write(self.data._root_com_vel_w.data.view(wp.float32), mask=env_mask_wp) + self._root_view.set_attribute( + TT.ROOT_VELOCITY, self.data._root_com_vel_w.data.view(wp.float32), mask=env_mask_wp + ) def write_joint_position_to_sim_index( self, @@ -861,8 +864,7 @@ def write_joint_position_to_sim_index( if not skip_forward: self._data._reset_pose() self._data._reset_velocity() - binding = self._get_binding(TT.DOF_POSITION) - binding.write(self._data._joint_pos_buf.data, indices=env_ids) + self._root_view.set_attribute(TT.DOF_POSITION, self._data._joint_pos_buf.data, indices=env_ids) def write_joint_position_to_sim_mask( self, @@ -907,8 +909,7 @@ def write_joint_position_to_sim_mask( if not skip_forward: self._data._reset_pose() self._data._reset_velocity() - binding = self._get_binding(TT.DOF_POSITION) - binding.write(self._data._joint_pos_buf.data, mask=env_mask_wp) + self._root_view.set_attribute(TT.DOF_POSITION, self._data._joint_pos_buf.data, mask=env_mask_wp) def write_joint_velocity_to_sim_index( self, @@ -967,8 +968,7 @@ def write_joint_velocity_to_sim_index( self._data._joint_acc.timestamp = self._data._sim_timestamp if not skip_forward: self._data._reset_velocity() - binding = self._get_binding(TT.DOF_VELOCITY) - binding.write(self._data._joint_vel_buf.data, indices=env_ids) + self._root_view.set_attribute(TT.DOF_VELOCITY, self._data._joint_vel_buf.data, indices=env_ids) def write_joint_velocity_to_sim_mask( self, @@ -1029,8 +1029,7 @@ def write_joint_velocity_to_sim_mask( self._data._joint_acc.timestamp = self._data._sim_timestamp if not skip_forward: self._data._reset_velocity() - binding = self._get_binding(TT.DOF_VELOCITY) - binding.write(self._data._joint_vel_buf.data, mask=env_mask_wp) + self._root_view.set_attribute(TT.DOF_VELOCITY, self._data._joint_vel_buf.data, mask=env_mask_wp) def write_joint_state_to_sim_mask( self, @@ -1120,8 +1119,7 @@ def write_joint_stiffness_to_sim_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_joint_stiffness, self._data._joint_stiffness.data) - binding = self._get_binding(TT.DOF_STIFFNESS) - binding.write(self.data._cpu_joint_stiffness, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_STIFFNESS, self.data._cpu_joint_stiffness, indices=cpu_env_ids) def write_joint_stiffness_to_sim_mask( self, @@ -1164,8 +1162,9 @@ def write_joint_stiffness_to_sim_mask( device=self._device, ) wp.copy(self.data._cpu_joint_stiffness, self._data._joint_stiffness.data) - binding = self._get_binding(TT.DOF_STIFFNESS) - binding.write(self.data._cpu_joint_stiffness, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_STIFFNESS, self.data._cpu_joint_stiffness, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_damping_to_sim_index( self, @@ -1208,8 +1207,7 @@ def write_joint_damping_to_sim_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_joint_damping, self._data._joint_damping.data) - binding = self._get_binding(TT.DOF_DAMPING) - binding.write(self.data._cpu_joint_damping, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_DAMPING, self.data._cpu_joint_damping, indices=cpu_env_ids) def write_joint_damping_to_sim_mask( self, @@ -1252,8 +1250,9 @@ def write_joint_damping_to_sim_mask( device=self._device, ) wp.copy(self.data._cpu_joint_damping, self._data._joint_damping.data) - binding = self._get_binding(TT.DOF_DAMPING) - binding.write(self.data._cpu_joint_damping, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_DAMPING, self.data._cpu_joint_damping, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_position_limit_to_sim_index( self, @@ -1352,8 +1351,7 @@ def write_joint_position_limit_to_sim_index( copy=False, ) wp.copy(self.data._cpu_joint_position_limit, flat_src) - binding = self._get_binding(TT.DOF_LIMIT) - binding.write(self.data._cpu_joint_position_limit, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_LIMIT, self.data._cpu_joint_position_limit, indices=cpu_env_ids) def write_joint_position_limit_to_sim_mask( self, @@ -1449,8 +1447,9 @@ def write_joint_position_limit_to_sim_mask( copy=False, ) wp.copy(self.data._cpu_joint_position_limit, flat_src) - binding = self._get_binding(TT.DOF_LIMIT) - binding.write(self.data._cpu_joint_position_limit, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_LIMIT, self.data._cpu_joint_position_limit, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_velocity_limit_to_sim_index( self, @@ -1493,8 +1492,7 @@ def write_joint_velocity_limit_to_sim_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_joint_velocity_limit, self._data._joint_vel_limits.data) - binding = self._get_binding(TT.DOF_MAX_VELOCITY) - binding.write(self.data._cpu_joint_velocity_limit, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_MAX_VELOCITY, self.data._cpu_joint_velocity_limit, indices=cpu_env_ids) def write_joint_velocity_limit_to_sim_mask( self, @@ -1537,8 +1535,9 @@ def write_joint_velocity_limit_to_sim_mask( device=self._device, ) wp.copy(self.data._cpu_joint_velocity_limit, self._data._joint_vel_limits.data) - binding = self._get_binding(TT.DOF_MAX_VELOCITY) - binding.write(self.data._cpu_joint_velocity_limit, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_MAX_VELOCITY, self.data._cpu_joint_velocity_limit, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_effort_limit_to_sim_index( self, @@ -1581,8 +1580,7 @@ def write_joint_effort_limit_to_sim_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_joint_effort_limit, self._data._joint_effort_limits.data) - binding = self._get_binding(TT.DOF_MAX_FORCE) - binding.write(self.data._cpu_joint_effort_limit, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_MAX_FORCE, self.data._cpu_joint_effort_limit, indices=cpu_env_ids) def write_joint_effort_limit_to_sim_mask( self, @@ -1625,8 +1623,9 @@ def write_joint_effort_limit_to_sim_mask( device=self._device, ) wp.copy(self.data._cpu_joint_effort_limit, self._data._joint_effort_limits.data) - binding = self._get_binding(TT.DOF_MAX_FORCE) - binding.write(self.data._cpu_joint_effort_limit, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_MAX_FORCE, self.data._cpu_joint_effort_limit, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_armature_to_sim_index( self, @@ -1669,8 +1668,7 @@ def write_joint_armature_to_sim_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_joint_armature, self._data._joint_armature.data) - binding = self._get_binding(TT.DOF_ARMATURE) - binding.write(self.data._cpu_joint_armature, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_ARMATURE, self.data._cpu_joint_armature, indices=cpu_env_ids) def write_joint_armature_to_sim_mask( self, @@ -1713,8 +1711,9 @@ def write_joint_armature_to_sim_mask( device=self._device, ) wp.copy(self.data._cpu_joint_armature, self._data._joint_armature.data) - binding = self._get_binding(TT.DOF_ARMATURE) - binding.write(self.data._cpu_joint_armature, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_ARMATURE, self.data._cpu_joint_armature, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_friction_coefficient_to_sim_index( self, @@ -1788,8 +1787,7 @@ def write_joint_friction_coefficient_to_sim_index( cpu_friction = self._data._stage_to_pinned_cpu( TT.DOF_FRICTION_PROPERTIES, "write", self._data._joint_friction_props_buf.data ) - binding = self._get_binding(TT.DOF_FRICTION_PROPERTIES) - binding.write(cpu_friction, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_FRICTION_PROPERTIES, cpu_friction, indices=cpu_env_ids) def write_joint_friction_coefficient_to_sim_mask( self, @@ -1841,8 +1839,9 @@ def write_joint_friction_coefficient_to_sim_mask( cpu_friction = self._data._stage_to_pinned_cpu( TT.DOF_FRICTION_PROPERTIES, "write", self._data._joint_friction_props_buf.data ) - binding = self._get_binding(TT.DOF_FRICTION_PROPERTIES) - binding.write(cpu_friction, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_FRICTION_PROPERTIES, cpu_friction, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_dynamic_friction_coefficient_to_sim_index( self, @@ -1902,8 +1901,7 @@ def write_joint_dynamic_friction_coefficient_to_sim_index( cpu_friction = self._data._stage_to_pinned_cpu( TT.DOF_FRICTION_PROPERTIES, "write", self._data._joint_friction_props_buf.data ) - binding = self._get_binding(TT.DOF_FRICTION_PROPERTIES) - binding.write(cpu_friction, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_FRICTION_PROPERTIES, cpu_friction, indices=cpu_env_ids) def write_joint_dynamic_friction_coefficient_to_sim_mask( self, @@ -1947,8 +1945,9 @@ def write_joint_dynamic_friction_coefficient_to_sim_mask( cpu_friction = self._data._stage_to_pinned_cpu( TT.DOF_FRICTION_PROPERTIES, "write", self._data._joint_friction_props_buf.data ) - binding = self._get_binding(TT.DOF_FRICTION_PROPERTIES) - binding.write(cpu_friction, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_FRICTION_PROPERTIES, cpu_friction, mask=self._get_cpu_env_mask(env_mask_wp) + ) def write_joint_viscous_friction_coefficient_to_sim_index( self, @@ -2009,8 +2008,7 @@ def write_joint_viscous_friction_coefficient_to_sim_index( cpu_friction = self._data._stage_to_pinned_cpu( TT.DOF_FRICTION_PROPERTIES, "write", self._data._joint_friction_props_buf.data ) - binding = self._get_binding(TT.DOF_FRICTION_PROPERTIES) - binding.write(cpu_friction, indices=cpu_env_ids) + self._root_view.set_attribute(TT.DOF_FRICTION_PROPERTIES, cpu_friction, indices=cpu_env_ids) def write_joint_viscous_friction_coefficient_to_sim_mask( self, @@ -2055,8 +2053,9 @@ def write_joint_viscous_friction_coefficient_to_sim_mask( cpu_friction = self._data._stage_to_pinned_cpu( TT.DOF_FRICTION_PROPERTIES, "write", self._data._joint_friction_props_buf.data ) - binding = self._get_binding(TT.DOF_FRICTION_PROPERTIES) - binding.write(cpu_friction, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.DOF_FRICTION_PROPERTIES, cpu_friction, mask=self._get_cpu_env_mask(env_mask_wp) + ) """ Operations - Setters. @@ -2100,8 +2099,7 @@ def set_masses_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_body_mass, self._data._body_mass.data) - binding = self._get_binding(TT.BODY_MASS) - binding.write(self.data._cpu_body_mass, indices=cpu_env_ids) + self._root_view.set_attribute(TT.BODY_MASS, self.data._cpu_body_mass, indices=cpu_env_ids) def set_masses_mask( self, @@ -2142,8 +2140,7 @@ def set_masses_mask( device=self._device, ) wp.copy(self.data._cpu_body_mass, self._data._body_mass.data) - binding = self._get_binding(TT.BODY_MASS) - binding.write(self.data._cpu_body_mass, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute(TT.BODY_MASS, self.data._cpu_body_mass, mask=self._get_cpu_env_mask(env_mask_wp)) def set_coms_index( self, @@ -2186,8 +2183,7 @@ def set_coms_index( self.data._body_com_pose_w.timestamp = -1.0 cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_body_coms, self._data._body_com_pose_b.data) - binding = self._get_binding(TT.BODY_COM_POSE) - binding.write(self.data._cpu_body_coms, indices=cpu_env_ids) + self._root_view.set_attribute(TT.BODY_COM_POSE, self.data._cpu_body_coms, indices=cpu_env_ids) def set_coms_mask( self, @@ -2231,8 +2227,9 @@ def set_coms_mask( self.data._root_com_pose_w.timestamp = -1.0 self.data._body_com_pose_w.timestamp = -1.0 wp.copy(self.data._cpu_body_coms, self._data._body_com_pose_b.data) - binding = self._get_binding(TT.BODY_COM_POSE) - binding.write(self.data._cpu_body_coms, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.BODY_COM_POSE, self.data._cpu_body_coms, mask=self._get_cpu_env_mask(env_mask_wp) + ) def set_inertias_index( self, @@ -2272,8 +2269,7 @@ def set_inertias_index( ) cpu_env_ids = self._get_cpu_env_ids(env_ids) wp.copy(self.data._cpu_body_inertia, self._data._body_inertia.data) - binding = self._get_binding(TT.BODY_INERTIA) - binding.write(self.data._cpu_body_inertia, indices=cpu_env_ids) + self._root_view.set_attribute(TT.BODY_INERTIA, self.data._cpu_body_inertia, indices=cpu_env_ids) def set_inertias_mask( self, @@ -2314,8 +2310,9 @@ def set_inertias_mask( device=self._device, ) wp.copy(self.data._cpu_body_inertia, self._data._body_inertia.data) - binding = self._get_binding(TT.BODY_INERTIA) - binding.write(self.data._cpu_body_inertia, mask=self._get_cpu_env_mask(env_mask_wp)) + self._root_view.set_attribute( + TT.BODY_INERTIA, self.data._cpu_body_inertia, mask=self._get_cpu_env_mask(env_mask_wp) + ) def set_joint_position_target_index( self, @@ -2354,8 +2351,7 @@ def set_joint_position_target_index( outputs=[self._data._joint_pos_target], device=self._device, ) - binding = self._get_binding(TT.DOF_POSITION_TARGET) - binding.write(self._data._joint_pos_target, indices=env_ids) + self._root_view.set_attribute(TT.DOF_POSITION_TARGET, self._data._joint_pos_target, indices=env_ids) def set_joint_position_target_mask( self, @@ -2391,8 +2387,7 @@ def set_joint_position_target_mask( outputs=[self._data._joint_pos_target], device=self._device, ) - binding = self._get_binding(TT.DOF_POSITION_TARGET) - binding.write(self._data._joint_pos_target, mask=env_mask_wp) + self._root_view.set_attribute(TT.DOF_POSITION_TARGET, self._data._joint_pos_target, mask=env_mask_wp) def set_joint_velocity_target_index( self, @@ -2431,8 +2426,7 @@ def set_joint_velocity_target_index( outputs=[self._data._joint_vel_target], device=self._device, ) - binding = self._get_binding(TT.DOF_VELOCITY_TARGET) - binding.write(self._data._joint_vel_target, indices=env_ids) + self._root_view.set_attribute(TT.DOF_VELOCITY_TARGET, self._data._joint_vel_target, indices=env_ids) def set_joint_velocity_target_mask( self, @@ -2468,8 +2462,7 @@ def set_joint_velocity_target_mask( outputs=[self._data._joint_vel_target], device=self._device, ) - binding = self._get_binding(TT.DOF_VELOCITY_TARGET) - binding.write(self._data._joint_vel_target, mask=env_mask_wp) + self._root_view.set_attribute(TT.DOF_VELOCITY_TARGET, self._data._joint_vel_target, mask=env_mask_wp) def set_joint_effort_target_index( self, @@ -2508,8 +2501,7 @@ def set_joint_effort_target_index( outputs=[self._data._joint_effort_target], device=self._device, ) - binding = self._get_binding(TT.DOF_ACTUATION_FORCE) - binding.write(self._data._joint_effort_target, indices=env_ids) + self._root_view.set_attribute(TT.DOF_ACTUATION_FORCE, self._data._joint_effort_target, indices=env_ids) def set_joint_effort_target_mask( self, @@ -2545,8 +2537,7 @@ def set_joint_effort_target_mask( outputs=[self._data._joint_effort_target], device=self._device, ) - binding = self._get_binding(TT.DOF_ACTUATION_FORCE) - binding.write(self._data._joint_effort_target, mask=env_mask_wp) + self._root_view.set_attribute(TT.DOF_ACTUATION_FORCE, self._data._joint_effort_target, mask=env_mask_wp) """ Operations - Tendons. @@ -2591,8 +2582,9 @@ def set_fixed_tendon_stiffness_index( outputs=[self._data._fixed_tendon_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_STIFFNESS) - binding.write(self._data._fixed_tendon_stiffness.data, indices=env_ids) + self._root_view.set_attribute( + TT.FIXED_TENDON_STIFFNESS, self._data._fixed_tendon_stiffness.data, indices=env_ids + ) def set_fixed_tendon_stiffness_mask( self, @@ -2635,8 +2627,9 @@ def set_fixed_tendon_stiffness_mask( outputs=[self._data._fixed_tendon_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_STIFFNESS) - binding.write(self._data._fixed_tendon_stiffness.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.FIXED_TENDON_STIFFNESS, self._data._fixed_tendon_stiffness.data, mask=env_mask_wp + ) def set_fixed_tendon_damping_index( self, @@ -2677,8 +2670,7 @@ def set_fixed_tendon_damping_index( outputs=[self._data._fixed_tendon_damping.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_DAMPING) - binding.write(self._data._fixed_tendon_damping.data, indices=env_ids) + self._root_view.set_attribute(TT.FIXED_TENDON_DAMPING, self._data._fixed_tendon_damping.data, indices=env_ids) def set_fixed_tendon_damping_mask( self, @@ -2721,8 +2713,7 @@ def set_fixed_tendon_damping_mask( outputs=[self._data._fixed_tendon_damping.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_DAMPING) - binding.write(self._data._fixed_tendon_damping.data, mask=env_mask_wp) + self._root_view.set_attribute(TT.FIXED_TENDON_DAMPING, self._data._fixed_tendon_damping.data, mask=env_mask_wp) def set_fixed_tendon_limit_stiffness_index( self, @@ -2763,8 +2754,9 @@ def set_fixed_tendon_limit_stiffness_index( outputs=[self._data._fixed_tendon_limit_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_LIMIT_STIFFNESS) - binding.write(self._data._fixed_tendon_limit_stiffness.data, indices=env_ids) + self._root_view.set_attribute( + TT.FIXED_TENDON_LIMIT_STIFFNESS, self._data._fixed_tendon_limit_stiffness.data, indices=env_ids + ) def set_fixed_tendon_limit_stiffness_mask( self, @@ -2807,8 +2799,9 @@ def set_fixed_tendon_limit_stiffness_mask( outputs=[self._data._fixed_tendon_limit_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_LIMIT_STIFFNESS) - binding.write(self._data._fixed_tendon_limit_stiffness.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.FIXED_TENDON_LIMIT_STIFFNESS, self._data._fixed_tendon_limit_stiffness.data, mask=env_mask_wp + ) def set_fixed_tendon_position_limit_index( self, @@ -2855,8 +2848,7 @@ def set_fixed_tendon_position_limit_index( device=self._device, copy=False, ) - binding = self._get_binding(TT.FIXED_TENDON_LIMIT) - binding.write(flat_src, indices=env_ids) + self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, indices=env_ids) def set_fixed_tendon_position_limit_mask( self, @@ -2903,8 +2895,7 @@ def set_fixed_tendon_position_limit_mask( device=self._device, copy=False, ) - binding = self._get_binding(TT.FIXED_TENDON_LIMIT) - binding.write(flat_src, mask=env_mask_wp) + self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, mask=env_mask_wp) def set_fixed_tendon_rest_length_index( self, @@ -2945,8 +2936,9 @@ def set_fixed_tendon_rest_length_index( outputs=[self._data._fixed_tendon_rest_length.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_REST_LENGTH) - binding.write(self._data._fixed_tendon_rest_length.data, indices=env_ids) + self._root_view.set_attribute( + TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length.data, indices=env_ids + ) def set_fixed_tendon_rest_length_mask( self, @@ -2989,8 +2981,9 @@ def set_fixed_tendon_rest_length_mask( outputs=[self._data._fixed_tendon_rest_length.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_REST_LENGTH) - binding.write(self._data._fixed_tendon_rest_length.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length.data, mask=env_mask_wp + ) def set_fixed_tendon_offset_index( self, @@ -3031,8 +3024,7 @@ def set_fixed_tendon_offset_index( outputs=[self._data._fixed_tendon_offset.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_OFFSET) - binding.write(self._data._fixed_tendon_offset.data, indices=env_ids) + self._root_view.set_attribute(TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset.data, indices=env_ids) def set_fixed_tendon_offset_mask( self, @@ -3075,8 +3067,7 @@ def set_fixed_tendon_offset_mask( outputs=[self._data._fixed_tendon_offset.data], device=self._device, ) - binding = self._get_binding(TT.FIXED_TENDON_OFFSET) - binding.write(self._data._fixed_tendon_offset.data, mask=env_mask_wp) + self._root_view.set_attribute(TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset.data, mask=env_mask_wp) def write_fixed_tendon_properties_to_sim_index( self, @@ -3108,9 +3099,8 @@ def write_fixed_tendon_properties_to_sim_index( (TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length), (TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset), ): - binding = self._get_binding(tt) - if binding is not None: - binding.write(buf.data, indices=env_ids) + if self._get_binding(tt) is not None: + self._root_view.set_attribute(tt, buf.data, indices=env_ids) # Position-limit binding consumes a flat (N, T, 2) float32 view. binding = self._get_binding(TT.FIXED_TENDON_LIMIT) if binding is not None: @@ -3121,7 +3111,7 @@ def write_fixed_tendon_properties_to_sim_index( device=self._device, copy=False, ) - binding.write(flat_src, indices=env_ids) + self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, indices=env_ids) def write_fixed_tendon_properties_to_sim_mask( self, @@ -3143,9 +3133,8 @@ def write_fixed_tendon_properties_to_sim_mask( (TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length), (TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset), ): - binding = self._get_binding(tt) - if binding is not None: - binding.write(buf.data, mask=env_mask_wp) + if self._get_binding(tt) is not None: + self._root_view.set_attribute(tt, buf.data, mask=env_mask_wp) binding = self._get_binding(TT.FIXED_TENDON_LIMIT) if binding is not None: flat_src = wp.array( @@ -3155,7 +3144,7 @@ def write_fixed_tendon_properties_to_sim_mask( device=self._device, copy=False, ) - binding.write(flat_src, mask=env_mask_wp) + self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, mask=env_mask_wp) def set_spatial_tendon_stiffness_index( self, @@ -3197,8 +3186,9 @@ def set_spatial_tendon_stiffness_index( outputs=[self._data._spatial_tendon_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_STIFFNESS) - binding.write(self._data._spatial_tendon_stiffness.data, indices=env_ids) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_STIFFNESS, self._data._spatial_tendon_stiffness.data, indices=env_ids + ) def set_spatial_tendon_stiffness_mask( self, @@ -3241,8 +3231,9 @@ def set_spatial_tendon_stiffness_mask( outputs=[self._data._spatial_tendon_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_STIFFNESS) - binding.write(self._data._spatial_tendon_stiffness.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_STIFFNESS, self._data._spatial_tendon_stiffness.data, mask=env_mask_wp + ) def set_spatial_tendon_damping_index( self, @@ -3281,8 +3272,9 @@ def set_spatial_tendon_damping_index( outputs=[self._data._spatial_tendon_damping.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_DAMPING) - binding.write(self._data._spatial_tendon_damping.data, indices=env_ids) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_DAMPING, self._data._spatial_tendon_damping.data, indices=env_ids + ) def set_spatial_tendon_damping_mask( self, @@ -3325,8 +3317,9 @@ def set_spatial_tendon_damping_mask( outputs=[self._data._spatial_tendon_damping.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_DAMPING) - binding.write(self._data._spatial_tendon_damping.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_DAMPING, self._data._spatial_tendon_damping.data, mask=env_mask_wp + ) def set_spatial_tendon_limit_stiffness_index( self, @@ -3369,8 +3362,9 @@ def set_spatial_tendon_limit_stiffness_index( outputs=[self._data._spatial_tendon_limit_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_LIMIT_STIFFNESS) - binding.write(self._data._spatial_tendon_limit_stiffness.data, indices=env_ids) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness.data, indices=env_ids + ) def set_spatial_tendon_limit_stiffness_mask( self, @@ -3413,8 +3407,9 @@ def set_spatial_tendon_limit_stiffness_mask( outputs=[self._data._spatial_tendon_limit_stiffness.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_LIMIT_STIFFNESS) - binding.write(self._data._spatial_tendon_limit_stiffness.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness.data, mask=env_mask_wp + ) def set_spatial_tendon_offset_index( self, @@ -3455,8 +3450,7 @@ def set_spatial_tendon_offset_index( outputs=[self._data._spatial_tendon_offset.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_OFFSET) - binding.write(self._data._spatial_tendon_offset.data, indices=env_ids) + self._root_view.set_attribute(TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset.data, indices=env_ids) def set_spatial_tendon_offset_mask( self, @@ -3499,8 +3493,9 @@ def set_spatial_tendon_offset_mask( outputs=[self._data._spatial_tendon_offset.data], device=self._device, ) - binding = self._get_binding(TT.SPATIAL_TENDON_OFFSET) - binding.write(self._data._spatial_tendon_offset.data, mask=env_mask_wp) + self._root_view.set_attribute( + TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset.data, mask=env_mask_wp + ) def write_spatial_tendon_properties_to_sim_index( self, @@ -3527,9 +3522,8 @@ def write_spatial_tendon_properties_to_sim_index( (TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness), (TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset), ): - binding = self._get_binding(tt) - if binding is not None: - binding.write(buf.data, indices=env_ids) + if self._get_binding(tt) is not None: + self._root_view.set_attribute(tt, buf.data, indices=env_ids) def write_spatial_tendon_properties_to_sim_mask( self, @@ -3545,9 +3539,8 @@ def write_spatial_tendon_properties_to_sim_mask( (TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness), (TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset), ): - binding = self._get_binding(tt) - if binding is not None: - binding.write(buf.data, mask=env_mask_wp) + if self._get_binding(tt) is not None: + self._root_view.set_attribute(tt, buf.data, mask=env_mask_wp) """ Internal helper. @@ -3610,33 +3603,33 @@ def has_articulation_root_api(prim) -> bool: TT.BODY_COM_POSE, TT.BODY_INERTIA, ] + self._root_view = OvPhysxView(self._ovphysx, pattern=pattern, device=self._device) + # ``try_binding_for`` creates and caches each binding, returning ``None`` for tensor + # types that do not apply to these prims (so a minimal articulation that lacks some + # of these types is skipped rather than failing the whole init). for tt in eager_types: - try: - self._bindings[tt] = physx_instance.create_tensor_binding(pattern=pattern, tensor_type=tt) - except Exception: - logger.debug("Could not create tensor binding for type %s on pattern %s", tt, pattern) + self._root_view.try_binding_for(tt) - if not self._bindings: + if not self._root_view.available_attributes: raise RuntimeError( f"OVPhysX could not create any articulation bindings for pattern {pattern!r}. " f"Check that prim_path={self.cfg.prim_path!r} matches at least one " "UsdPhysics.ArticulationRootAPI prim." ) - # read metadata from the first available binding - sample = next(iter(self._bindings.values())) - self._num_instances = sample.count - self._num_joints = sample.dof_count - self._num_bodies = sample.body_count - self._is_fixed_base = sample.is_fixed_base - self._joint_names = list(sample.dof_names) - self._body_names = list(sample.body_names) + # read metadata from the view (any instantiated binding carries it) + self._num_instances = self._root_view.count + self._num_joints = self._root_view.dof_count + self._num_bodies = self._root_view.body_count + self._is_fixed_base = self._root_view.is_fixed_base + self._joint_names = list(self._root_view.dof_names) + self._body_names = list(self._root_view.body_names) # tendon counts/names must be resolved before buffer allocation self._process_tendons() - # eagerly create tendon bindings now that the counts are known; this keeps - # ArticulationData's _get_binding a simple dict lookup (no lazy callback). + # eagerly create tendon bindings now that the counts are known, through the same + # view, so ArticulationData reads them via a cached lookup (no lazy callback). if self._num_fixed_tendons > 0: for tt in ( TT.FIXED_TENDON_STIFFNESS, @@ -3646,10 +3639,7 @@ def has_articulation_root_api(prim) -> bool: TT.FIXED_TENDON_REST_LENGTH, TT.FIXED_TENDON_OFFSET, ): - try: - self._bindings[tt] = physx_instance.create_tensor_binding(pattern=pattern, tensor_type=tt) - except Exception: - logger.debug("Could not create tensor binding for type %s on pattern %s", tt, pattern) + self._root_view.try_binding_for(tt) if self._num_spatial_tendons > 0: for tt in ( TT.SPATIAL_TENDON_STIFFNESS, @@ -3657,13 +3647,10 @@ def has_articulation_root_api(prim) -> bool: TT.SPATIAL_TENDON_LIMIT_STIFFNESS, TT.SPATIAL_TENDON_OFFSET, ): - try: - self._bindings[tt] = physx_instance.create_tensor_binding(pattern=pattern, tensor_type=tt) - except Exception: - logger.debug("Could not create tensor binding for type %s on pattern %s", tt, pattern) + self._root_view.try_binding_for(tt) - # construct the data container; counts come from the bindings - self._data = ArticulationData(self._bindings, self._device) + # construct the data container; counts come from the view's bindings + self._data = ArticulationData(self._root_view, self._device) self._data.body_names = self._body_names self._data.joint_names = self._joint_names self._data.fixed_tendon_names = self._fixed_tendon_names @@ -3803,9 +3790,8 @@ def _process_tendons(self) -> None: self._fixed_tendon_names = [] self._spatial_tendon_names = [] - sample = next(iter(self._bindings.values())) - self._num_fixed_tendons = getattr(sample, "fixed_tendon_count", 0) - self._num_spatial_tendons = getattr(sample, "spatial_tendon_count", 0) + self._num_fixed_tendons = self._root_view.fixed_tendon_count + self._num_spatial_tendons = self._root_view.spatial_tendon_count if self._num_fixed_tendons > 0 or self._num_spatial_tendons > 0: stage_path = OvPhysxManager._stage_path @@ -3864,16 +3850,7 @@ def _get_binding(self, tensor_type: int): Returns: A TensorBinding object, or ``None`` if the binding could not be created. """ - binding = self._bindings.get(tensor_type) - if binding is not None: - return binding - try: - binding = self._ovphysx.create_tensor_binding(pattern=self._binding_pattern, tensor_type=tensor_type) - self._bindings[tensor_type] = binding - return binding - except Exception: - logger.debug("Could not create tensor binding for type %s", tensor_type) - return None + return self._root_view.try_binding_for(tensor_type) def _resolve_joint_values(self, pattern_dict: dict[str, float], buffer: wp.array) -> None: """Resolve a ``{pattern: value}`` dict into a per-joint buffer. @@ -3920,6 +3897,9 @@ def _nst(self) -> int: def _invalidate_initialize_callback(self, event) -> None: """Invalidate the asset on simulation reset.""" super()._invalidate_initialize_callback(event) + # Drop the view (and the bindings it caches) on stop so a destroyed/stale binding is + # not held across the reset; ``_initialize_impl`` rebuilds a fresh view on the next play. + self._root_view = None """ Internal helpers -- Actuators. @@ -4185,9 +4165,10 @@ def _broadcast_scalar_to_2d( def _resolve_env_mask(self, env_mask: wp.array | None) -> wp.array: """Resolve an environment mask to a ``wp.bool`` array on ``self._device``. - OVPhysX (like Newton) uses the binding's native ``binding.write(mask=...)`` path, - so the mask is preserved end-to-end; no ``torch.nonzero`` conversion is needed. - ``None`` returns the pre-allocated all-true mask. + OVPhysX (like Newton) writes through the view's ``set_attribute(mask=...)``, which + forwards to the binding's native masked write, so the mask is preserved end-to-end; + no ``torch.nonzero`` conversion is needed. ``None`` returns the pre-allocated + all-true mask. """ if env_mask is None: return self._ALL_TRUE_ENV_MASK @@ -4240,9 +4221,9 @@ def _resolve_spatial_tendon_mask(self, tendon_mask: wp.array | None) -> wp.array def _get_cpu_env_mask(self, env_mask: wp.array) -> wp.array: """Return a pinned-host CPU copy of :paramref:`env_mask` for a CPU-only binding write. - :paramref:`env_mask` is normally on ``self._device``; ``binding.write(mask=...)`` - requires the mask on the binding's device, which is CPU for mass / CoMs / inertia. - Reuses the pre-allocated ``_cpu_env_mask`` pinned buffer. + :paramref:`env_mask` is normally on ``self._device``; ``set_attribute(mask=...)`` + requires the mask on the binding's native device, which is CPU for mass / CoMs / + inertia. Reuses the pre-allocated ``_cpu_env_mask`` pinned buffer. """ wp.copy(self._cpu_env_mask, env_mask) return self._cpu_env_mask diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py index 922c12edfaa3..c0a660cd0ea1 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/assets/articulation/articulation_data.py @@ -32,6 +32,7 @@ vec13f, ) from isaaclab_ovphysx.physics import OvPhysxManager +from isaaclab_ovphysx.sim.views.ovphysx_view import OvPhysxView from .kernels import _fd_joint_acc @@ -72,30 +73,28 @@ class ArticulationData(BaseArticulationData): __backend_name__: str = "ovphysx" """The name of the backend for the articulation data.""" - def __init__(self, bindings: dict[int, Any], device: str) -> None: + def __init__(self, view: OvPhysxView, device: str) -> None: """Initialize the articulation data container. Args: - bindings: Dictionary of OVPhysX :class:`TensorBinding` objects keyed - by :class:`isaaclab_ovphysx.tensor_types.TensorType`. All counts - (instances, bodies, DOFs, fixed/spatial tendons) are derived - from the binding metadata. Name lists are assigned by - :meth:`~isaaclab_ovphysx.assets.Articulation._initialize_impl` - after construction. + view: The :class:`~isaaclab_ovphysx.sim.views.OvPhysxView` binding manager + for this articulation. All counts (instances, bodies, DOFs, + fixed/spatial tendons) are derived from the view metadata. Name lists + are assigned by + :meth:`~isaaclab_ovphysx.assets.Articulation._initialize_impl` after + construction. device: Simulation device string (e.g., ``"cuda:0"`` or ``"cpu"``). """ super().__init__(root_view=None, device=device) - self._bindings = bindings - - # Every OVPhysX TensorBinding carries the articulation metadata - # (instance count, dof_count, body_count, fixed/spatial tendon counts); - # any binding will do for the read. - sample = next(iter(bindings.values())) - self.num_instances = sample.count - self.num_bodies = sample.body_count - self.num_joints = sample.dof_count - self.num_fixed_tendons = getattr(sample, "fixed_tendon_count", 0) - self.num_spatial_tendons = getattr(sample, "spatial_tendon_count", 0) + self._view = view + + # The view exposes the articulation metadata (instance count, dof_count, + # body_count, fixed/spatial tendon counts) read from any instantiated binding. + self.num_instances = view.count + self.num_bodies = view.body_count + self.num_joints = view.dof_count + self.num_fixed_tendons = view.fixed_tendon_count + self.num_spatial_tendons = view.spatial_tendon_count # private aliases used throughout _create_buffers and property bodies self._num_instances = self.num_instances self._num_bodies = self.num_bodies @@ -109,8 +108,6 @@ def __init__(self, bindings: dict[int, Any], device: str) -> None: self._is_primed: bool = False # pinned-host staging buffers for CPU-only bindings (keyed by tensor_type) self._cpu_staging_buffers: dict[int, wp.array] = {} - # scratch buffers for _get_read_view cache (keyed by (tensor_type, ptr)) - self._read_scratch: dict = {} # obtain gravity from the simulation configuration (fall back to standard # gravity when the simulation has not been configured yet, e.g. in unit tests) @@ -1553,35 +1550,37 @@ def _create_buffers(self) -> None: # noqa: C901 # Initialize ProxyArray wrappers (lazily created on first property access). self._pin_proxy_arrays() - def _binding_read(self, tensor_type: int, binding: Any, dst: wp.array) -> None: - """Read *binding* into *dst*, staging through a pinned-host buffer for CPU-only bindings. + def _binding_read(self, tensor_type: int, dst: wp.array) -> None: + """Refresh *dst* from the binding via the view, staging for CPU-only bindings. - For GPU-resident state bindings (pose, velocity, etc.) the read goes directly - into the destination array. For CPU-only property bindings (mass, COM, limits, - stiffness, …) the wheel writes into a pinned-host staging buffer first, then - :func:`wp.copy` moves the data to the simulation device asynchronously. + GPU-resident state bindings (pose, velocity, …) fill *dst* directly through + :meth:`~isaaclab_ovphysx.sim.views.OvPhysxView.read_into`, which reinterprets a + structured *dst* and reuses that reinterpret across calls so the wheel's read cache + stays warm. CPU-only property bindings (mass, COM, limits, stiffness, …) are read + into a pinned-host staging buffer first (the view does not stage across devices), + then :func:`wp.copy` moves the data to the simulation device. Args: tensor_type: TensorType key identifying the binding. - binding: OVPhysX TensorBinding whose ``read`` method is called. dst: Destination :class:`wp.array` on the simulation device. """ if tensor_type not in TT._CPU_ONLY_TYPES or self.device == "cpu": - binding.read(dst) + self._view.read_into(tensor_type, dst) return - # Route through a lazily-allocated pinned-host staging buffer. + # Route through a lazily-allocated pinned-host staging buffer (read_into refuses to + # cross devices), then copy to the simulation device. staging = self._cpu_staging_buffers.get(tensor_type) if staging is None: - staging = wp.zeros(binding.shape, dtype=wp.float32, device="cpu", pinned=True) + staging = wp.zeros(self._view.binding_for(tensor_type).shape, dtype=wp.float32, device="cpu", pinned=True) self._cpu_staging_buffers[tensor_type] = staging - binding.read(staging) - # Build a flat float32 view of dst matching the binding's flat shape. + self._view.read_into(tensor_type, staging) + # Build a flat float32 view of dst matching the staging's flat shape. if dst.dtype == wp.float32: view = dst else: view = wp.array( ptr=dst.ptr, - shape=binding.shape, + shape=staging.shape, dtype=wp.float32, device=str(dst.device), copy=False, @@ -1741,11 +1740,11 @@ def _read_cpu(tensor_type): ]: binding = self._get_binding(tt) if binding is not None: - self._binding_read(tt, binding, buf.data) + self._binding_read(tt, buf.data) buf.timestamp = self._sim_timestamp binding = self._get_binding(TT.FIXED_TENDON_LIMIT) if binding is not None: - self._binding_read(TT.FIXED_TENDON_LIMIT, binding, self._fixed_tendon_pos_limits.data) + self._binding_read(TT.FIXED_TENDON_LIMIT, self._fixed_tendon_pos_limits.data) self._fixed_tendon_pos_limits.timestamp = self._sim_timestamp # Spatial tendon properties (sim-device, see fixed-tendon comment above). @@ -1759,7 +1758,7 @@ def _read_cpu(tensor_type): ]: binding = self._get_binding(tt) if binding is not None: - self._binding_read(tt, binding, buf.data) + self._binding_read(tt, buf.data) buf.timestamp = self._sim_timestamp def _pin_proxy_arrays(self) -> None: @@ -1920,135 +1919,47 @@ def _invalidate_initialize_callback(self, event) -> None: val.timestamp = -1.0 def _get_binding(self, tensor_type: int): - """Return the cached binding for :paramref:`tensor_type`, or ``None`` if absent. + """Return the binding for :paramref:`tensor_type`, or ``None`` if unavailable. - Args: - tensor_type: TensorType key. - - Returns: - The TensorBinding, or ``None`` if not present in the binding dict. - """ - return self._bindings.get(tensor_type) - - def _get_read_view(self, tensor_type: int, wp_array: wp.array, floats_per_elem: int = 0) -> wp.array | None: - """Return a stable float32 view of a warp buffer for reading from a binding. - - For structured-dtype buffers (transformf, spatial_vectorf), the view - reinterprets the same GPU memory as a flat float32 array matching the - binding's shape. For plain float32 buffers, returns the array as-is. - - The returned view is cached so that ``binding.read(view)`` sees the - same object on every call, enabling the binding's internal read cache. + Delegates to :attr:`root_view`'s + :meth:`~isaaclab_ovphysx.sim.views.OvPhysxView.try_binding_for`, which returns the + cached binding (creating it on first access) or ``None`` for tensor types that do + not apply to these prims. Args: tensor_type: TensorType key. - wp_array: Destination warp array. - floats_per_elem: Number of float32 elements per logical element - (e.g. 7 for transformf, 6 for spatial_vectorf). Pass 0 to - return the array as-is. Returns: - Float32 view suitable for ``binding.read()``, or ``None``. + The TensorBinding, or ``None`` if not available for these prims. """ - if not hasattr(self, "_read_view_cache"): - self._read_view_cache = {} - cache_key = (tensor_type, wp_array.ptr) - cached = self._read_view_cache.get(cache_key) - if cached is not None: - return cached - - binding = self._get_binding(tensor_type) - if binding is None: - self._read_view_cache[cache_key] = None - return None - - if floats_per_elem > 0: - view = wp.array( - ptr=wp_array.ptr, - shape=binding.shape, - dtype=wp.float32, - device=str(wp_array.device), - copy=False, - ) - else: - view = wp_array - - self._read_view_cache[cache_key] = view - return view + return self._view.try_binding_for(tensor_type) def _read_binding_into_buf(self, tensor_type: int, buf: TimestampedBuffer) -> None: - """Read from an ovphysx binding into a :class:`TimestampedBuffer`, skipping if fresh. + """Refresh *buf* from the matching binding via the view, skipping if fresh or absent. - Args: - tensor_type: TensorType key. - buf: Timestamped buffer to refresh. - """ - if buf.timestamp >= self._sim_timestamp: - return - view = self._get_read_view(tensor_type, buf.data) - if view is None: - return - self._get_binding(tensor_type).read(view) - buf.timestamp = self._sim_timestamp - - def _read_transform_binding(self, tensor_type: int, buf: TimestampedBuffer) -> None: - """Read a pose binding (float32 view of transformf buffer), skipping if fresh. - - CPU-only bindings (e.g. ``BODY_COM_POSE``) are routed through a - pinned-host staging buffer via :meth:`_binding_read` so the wheel's - device-match requirement is satisfied even on a GPU sim. - - Args: - tensor_type: TensorType key. - buf: Timestamped :class:`wp.transformf` buffer to refresh. - """ - if buf.timestamp >= self._sim_timestamp: - return - binding = self._get_binding(tensor_type) - if binding is None: - return - view = self._get_read_view(tensor_type, buf.data, 7) - if view is None: - return - self._binding_read(tensor_type, binding, view) - buf.timestamp = self._sim_timestamp - - def _read_spatial_vector_binding(self, tensor_type: int, buf: TimestampedBuffer) -> None: - """Read a velocity binding (float32 view of spatial_vectorf buffer), skipping if fresh. + Reads route through :meth:`~isaaclab_ovphysx.sim.views.OvPhysxView.read_into`, which + derives the structured reinterpret from the binding shape (so transform, spatial-vector, + and scalar buffers all use the same path) and reuses that reinterpret across calls; + CPU-only property bindings are staged onto the simulation device inside + :meth:`_binding_read`. Args: tensor_type: TensorType key. - buf: Timestamped :class:`wp.spatial_vectorf` buffer to refresh. + buf: Timestamped buffer to refresh. """ if buf.timestamp >= self._sim_timestamp: return - view = self._get_read_view(tensor_type, buf.data, 6) - if view is None: + if self._get_binding(tensor_type) is None: return - self._get_binding(tensor_type).read(view) + self._binding_read(tensor_type, buf.data) buf.timestamp = self._sim_timestamp - def _read_scalar_binding(self, tensor_type: int, buf: TimestampedBuffer) -> None: - """Refresh a scalar or flat float32 buffer from the matching binding if stale. - - Identical timestamp-gating contract as :meth:`_read_transform_binding` - but without a structured-dtype reinterpret cast. CPU-only bindings - (e.g. ``DOF_STIFFNESS``, ``DOF_LIMIT``) are routed through a - pre-allocated pinned-host staging buffer via :meth:`_binding_read` so - the wheel's device-match requirement is satisfied even on a GPU sim. - - Args: - tensor_type: TensorType key identifying the binding. - buf: Timestamped buffer whose :attr:`~TimestampedBuffer.data` field - will be refreshed. - """ - if buf.timestamp >= self._sim_timestamp: - return - binding = self._get_binding(tensor_type) - if binding is None: - return - self._binding_read(tensor_type, binding, buf.data) - buf.timestamp = self._sim_timestamp + # ``read_into`` derives the reinterpret from the binding shape and ``_binding_read`` handles + # CPU-only staging, so the transform / spatial-vector / scalar read paths are now identical; + # keep the distinct names as aliases for call-site readability. + _read_transform_binding = _read_binding_into_buf + _read_spatial_vector_binding = _read_binding_into_buf + _read_scalar_binding = _read_binding_into_buf def _get_pos_from_transform(self, transform: wp.array) -> wp.array: """Return a position view aliased into a transform array. diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/__init__.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/__init__.py index 2c6c0e67ffc4..a1b56269d5ac 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/__init__.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/__init__.py @@ -3,4 +3,4 @@ # # SPDX-License-Identifier: BSD-3-Clause -from .views import MockTensorBinding, MockOvPhysxBindingSet +from .views import MockOvPhysxBindingSet, MockOvPhysxView, MockTensorBinding diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/__init__.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/__init__.py index 6f133a18202c..b15f32483798 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/__init__.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/__init__.py @@ -3,4 +3,4 @@ # # SPDX-License-Identifier: BSD-3-Clause -from .mock_ovphysx_bindings import MockTensorBinding, MockOvPhysxBindingSet +from .mock_ovphysx_bindings import MockOvPhysxBindingSet, MockOvPhysxView, MockTensorBinding diff --git a/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/mock_ovphysx_bindings.py b/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/mock_ovphysx_bindings.py index 51e96d9bb427..603e9f448d63 100644 --- a/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/mock_ovphysx_bindings.py +++ b/source/isaaclab_ovphysx/isaaclab_ovphysx/test/mock_interfaces/views/mock_ovphysx_bindings.py @@ -158,6 +158,129 @@ def set_random_data(self, low: float = -1.0, high: float = 1.0) -> None: self._data = np.random.uniform(low, high, self._shape).astype(np.float32) +class MockOvPhysxView: + """Mock of :class:`~isaaclab_ovphysx.sim.views.OvPhysxView` over a dict of + :class:`MockTensorBinding`. + + Lets a unit test inject a working ``_root_view`` into an OVPhysX asset/data class + without standing up the real view, a real ``PhysX``, or a USD stage. It mirrors the + consumed surface of ``OvPhysxView`` -- ``binding_for`` / ``try_binding_for``, + ``get_attribute`` / ``read_into`` / ``set_attribute``, the discoverability helpers, + and the metadata passthrough -- delegating reads/writes to the mock bindings, which + already implement ``read``/``write`` against numpy buffers. + + Names resolve like the real view: pass either a :class:`TensorType` member or its + lowercased name (e.g. ``"articulation_dof_stiffness"``). Unlike the real view this + keeps everything on CPU and applies no device/dtype/read-only guards -- it is a test + double for the binding-routing surface, not the device policy. + """ + + def __init__(self, bindings: dict[int, MockTensorBinding]): + self._bindings = bindings + + def _resolve(self, name): + """Resolve a TensorType member or its lowercased name to the member.""" + if isinstance(name, str): + enum_cls = type(next(iter(self._bindings))) + try: + return enum_cls[name.upper()] + except KeyError: + raise KeyError(f"Unknown attribute {name!r}") from None + return name + + def _sample(self) -> MockTensorBinding: + """A representative binding to read shared metadata from.""" + return next(iter(self._bindings.values())) + + # -- core read / write ----------------------------------------------------- + + def get_attribute(self, name, *, out=None): + """Read the full attribute tensor; fill ``out`` if given, else allocate float32.""" + binding = self._bindings[self._resolve(name)] + if out is not None: + binding.read(out) + return out + import warp as wp + + buf = wp.zeros(tuple(binding.shape), dtype=wp.float32, device="cpu") + binding.read(buf) + return buf + + def read_into(self, name, dst) -> None: + """Fill ``dst`` in place from the attribute binding.""" + self._bindings[self._resolve(name)].read(dst) + + def set_attribute(self, name, values, *, indices=None, mask=None) -> None: + """Write a full attribute tensor; ``indices``/``mask`` select which rows apply.""" + self._bindings[self._resolve(name)].write(values, indices=indices, mask=mask) + + # -- raw binding access ---------------------------------------------------- + + def binding_for(self, name) -> MockTensorBinding: + """Return the underlying binding, raising ``KeyError`` if absent for these prims.""" + return self._bindings[self._resolve(name)] + + def try_binding_for(self, name) -> MockTensorBinding | None: + """Like :meth:`binding_for`, but return ``None`` when the attribute is absent.""" + return self._bindings.get(self._resolve(name)) + + # -- discoverability ------------------------------------------------------- + + def has_attribute(self, name) -> bool: + """Return whether a binding is instantiated for ``name`` on these prims.""" + return self._resolve(name) in self._bindings + + def __contains__(self, name) -> bool: + return self.has_attribute(name) + + @property + def available_attributes(self) -> list[str]: + """Names with a live binding (lowercased ``TensorType`` members).""" + return sorted(tt.name.lower() for tt in self._bindings) + + # -- metadata passthrough (from a sample binding) -------------------------- + + @property + def count(self) -> int: + return self._sample().count + + @property + def dof_count(self) -> int: + return self._sample().dof_count + + @property + def body_count(self) -> int: + return self._sample().body_count + + @property + def joint_count(self) -> int: + return self._sample().joint_count + + @property + def is_fixed_base(self) -> bool: + return self._sample().is_fixed_base + + @property + def dof_names(self) -> list[str]: + return list(self._sample().dof_names) + + @property + def body_names(self) -> list[str]: + return list(self._sample().body_names) + + @property + def joint_names(self) -> list[str]: + return list(self._sample().joint_names) + + @property + def fixed_tendon_count(self) -> int: + return self._sample().fixed_tendon_count + + @property + def spatial_tendon_count(self) -> int: + return self._sample().spatial_tendon_count + + class MockOvPhysxBindingSet: """Factory that creates a full set of MockTensorBinding objects for a given articulation configuration. @@ -307,6 +430,20 @@ def __init__( } ) + @property + def view(self) -> MockOvPhysxView: + """A mock :class:`OvPhysxView` over this set's bindings. + + Inject as an asset's ``_root_view`` to exercise the migrated binding-routing + code paths without a real view or ``PhysX``. Cached so repeated access returns + the same object, like the single view an asset holds. + """ + v = getattr(self, "_view", None) + if v is None: + v = MockOvPhysxView(self.bindings) + self._view = v + return v + def set_random_data(self) -> None: """Fill all bindings with random data.""" for b in self.bindings.values(): diff --git a/source/isaaclab_ovphysx/test/assets/test_articulation.py b/source/isaaclab_ovphysx/test/assets/test_articulation.py index 76df62e1abba..6a34225ba177 100644 --- a/source/isaaclab_ovphysx/test/assets/test_articulation.py +++ b/source/isaaclab_ovphysx/test/assets/test_articulation.py @@ -19,11 +19,14 @@ environment. PhysX-specific ``cube_object.root_view.set_X(...)`` / ``get_X(...)`` calls are -adapted to OVPhysX by going through the backend's per-tensor-type binding -dictionary (``cube_object._bindings`` / :meth:`~isaaclab_ovphysx.assets.Articulation._get_binding`) -and the public setters (:meth:`set_masses_index`, :meth:`set_coms_index`, -:meth:`set_inertias_index`). Reads use the data-class properties -(``cube_object.data.body_mass``, ``body_inertia``, ``body_com_pose_b``). +adapted to OVPhysX by going through +:attr:`~isaaclab_ovphysx.assets.Articulation.root_view`, an +:class:`~isaaclab_ovphysx.sim.views.OvPhysxView` over the per-tensor-type bindings +(``root_view.get_attribute(tensor_type)`` / +:meth:`~isaaclab_ovphysx.assets.Articulation._get_binding`), and the public setters +(:meth:`set_masses_index`, :meth:`set_coms_index`, :meth:`set_inertias_index`). +Reads use the data-class properties (``cube_object.data.body_mass``, +``body_inertia``, ``body_com_pose_b``). Process-global device lock -------------------------- @@ -51,7 +54,6 @@ import sys -import numpy as np import pytest import torch import warp as wp @@ -94,26 +96,25 @@ _MATERIAL_GAP_REASON = ( "Requires a ``RIGID_BODY_MATERIAL`` TensorType (or a view-helper) on the " - "ovphysx wheel side. ``Articulation.root_view`` is a per-tensor-type " - "bindings dict on OVPhysX, so ``root_view.get_material_properties()`` / " + "ovphysx wheel side. ``Articulation.root_view`` is an ``OvPhysxView`` over " + "the per-tensor-type bindings on OVPhysX, so ``root_view.get_material_properties()`` / " "``set_material_properties()`` / ``max_shapes`` are not available. See " "docs/superpowers/specs/2026-04-28-ovphysx-wheel-gaps-for-marco.md." ) def _read_binding_to_torch(articulation: Articulation, tensor_type: int, device: str | torch.device) -> torch.Tensor: - """Read an OVPhysX TensorBinding into a torch tensor on *device*. + """Read an OVPhysX attribute into a torch tensor on *device*. Test-side adapter for the verbatim PhysX mirror. PhysX cross-checks the data class against the simulation via ``articulation.root_view.get_X()`` - accessors; on OVPhysX, ``root_view`` is a per-tensor-type bindings dict - (no view-level getters), so we read the binding directly into a CPU - numpy buffer (CPU-only types) and move the result to *device*. + accessors; on OVPhysX we go through the equivalent + :meth:`~isaaclab_ovphysx.sim.views.OvPhysxView.get_attribute`, which returns a + freshly allocated ``float32`` array on the attribute's native device (CPU for + CPU-only property types), then move the result to *device*. """ - binding = articulation.root_view[tensor_type] - np_buf = np.zeros(binding.shape, dtype=np.float32) - binding.read(np_buf) - return torch.from_numpy(np_buf).to(device) + arr = articulation.root_view.get_attribute(tensor_type) + return wp.to_torch(arr).to(device) # Session-locked device. Set on the first parametrized test that runs and @@ -365,15 +366,17 @@ def test_initialization_floating_base_non_root(sim, num_articulations, device, a # Cross-check binding shapes against cached counts. PhysX does this via # ``root_view.max_dofs == shared_metatype.dof_count``; on OVPhysX - # ``root_view`` is the per-tensor-type bindings dict, so the equivalent + # ``root_view`` is an ``OvPhysxView`` over the per-tensor-type bindings, so the equivalent # invariant is that each per-DOF / per-link binding's shape agrees with # the count cached on the asset. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # Body-name ordering check is degenerate on OVPhysX: ``body_names`` is # sourced from binding metadata (``sample.body_names``), so the PhysX # ``link_paths[0]`` round-trip is a no-op here and is omitted. @@ -428,15 +431,17 @@ def test_initialization_floating_base(sim, num_articulations, device, add_ground # Cross-check binding shapes against cached counts. PhysX does this via # ``root_view.max_dofs == shared_metatype.dof_count``; on OVPhysX - # ``root_view`` is the per-tensor-type bindings dict, so the equivalent + # ``root_view`` is an ``OvPhysxView`` over the per-tensor-type bindings, so the equivalent # invariant is that each per-DOF / per-link binding's shape agrees with # the count cached on the asset. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # Body-name ordering check is degenerate on OVPhysX: ``body_names`` is # sourced from binding metadata (``sample.body_names``), so the PhysX # ``link_paths[0]`` round-trip is a no-op here and is omitted. @@ -490,15 +495,17 @@ def test_initialization_fixed_base(sim, num_articulations, device): # Cross-check binding shapes against cached counts. PhysX does this via # ``root_view.max_dofs == shared_metatype.dof_count``; on OVPhysX - # ``root_view`` is the per-tensor-type bindings dict, so the equivalent + # ``root_view`` is an ``OvPhysxView`` over the per-tensor-type bindings, so the equivalent # invariant is that each per-DOF / per-link binding's shape agrees with # the count cached on the asset. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # Body-name ordering check is degenerate on OVPhysX: ``body_names`` is # sourced from binding metadata (``sample.body_names``), so the PhysX # ``link_paths[0]`` round-trip is a no-op here and is omitted. @@ -561,15 +568,17 @@ def test_initialization_fixed_base_single_joint(sim, num_articulations, device, # Cross-check binding shapes against cached counts. PhysX does this via # ``root_view.max_dofs == shared_metatype.dof_count``; on OVPhysX - # ``root_view`` is the per-tensor-type bindings dict, so the equivalent + # ``root_view`` is an ``OvPhysxView`` over the per-tensor-type bindings, so the equivalent # invariant is that each per-DOF / per-link binding's shape agrees with # the count cached on the asset. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # Body-name ordering check is degenerate on OVPhysX: ``body_names`` is # sourced from binding metadata (``sample.body_names``), so the PhysX # ``link_paths[0]`` round-trip is a no-op here and is omitted. @@ -634,11 +643,13 @@ def test_initialization_hand_with_tendons(sim, num_articulations, device): # PhysX ``root_view.max_dofs == shared_metatype.dof_count`` identity is # replaced with binding-shape checks on OVPhysX. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # -- actuator type for actuator_name, actuator in articulation.actuators.items(): is_implicit_model_cfg = isinstance(articulation_cfg.actuators[actuator_name], ImplicitActuatorCfg) @@ -690,15 +701,17 @@ def test_initialization_floating_base_made_fixed_base(sim, num_articulations, de # Cross-check binding shapes against cached counts. PhysX does this via # ``root_view.max_dofs == shared_metatype.dof_count``; on OVPhysX - # ``root_view`` is the per-tensor-type bindings dict, so the equivalent + # ``root_view`` is an ``OvPhysxView`` over the per-tensor-type bindings, so the equivalent # invariant is that each per-DOF / per-link binding's shape agrees with # the count cached on the asset. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # Body-name ordering check is degenerate on OVPhysX: ``body_names`` is # sourced from binding metadata (``sample.body_names``), so the PhysX # ``link_paths[0]`` round-trip is a no-op here and is omitted. @@ -756,15 +769,17 @@ def test_initialization_fixed_base_made_floating_base(sim, num_articulations, de # Cross-check binding shapes against cached counts. PhysX does this via # ``root_view.max_dofs == shared_metatype.dof_count``; on OVPhysX - # ``root_view`` is the per-tensor-type bindings dict, so the equivalent + # ``root_view`` is an ``OvPhysxView`` over the per-tensor-type bindings, so the equivalent # invariant is that each per-DOF / per-link binding's shape agrees with # the count cached on the asset. for tt in (TT.DOF_POSITION, TT.DOF_VELOCITY, TT.DOF_STIFFNESS): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_joints + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_joints for tt in (TT.BODY_MASS, TT.BODY_COM_POSE): - if tt in articulation.root_view: - assert articulation.root_view[tt].shape[1] == articulation.num_bodies + binding = articulation.root_view.try_binding_for(tt) + if binding is not None: + assert binding.shape[1] == articulation.num_bodies # Body-name ordering check is degenerate on OVPhysX: ``body_names`` is # sourced from binding metadata (``sample.body_names``), so the PhysX # ``link_paths[0]`` round-trip is a no-op here and is omitted. diff --git a/source/isaaclab_ovphysx/test/assets/test_articulation_helpers.py b/source/isaaclab_ovphysx/test/assets/test_articulation_helpers.py index da85e53facbc..1356daec70da 100644 --- a/source/isaaclab_ovphysx/test/assets/test_articulation_helpers.py +++ b/source/isaaclab_ovphysx/test/assets/test_articulation_helpers.py @@ -84,7 +84,9 @@ def _make_articulation_shell() -> Articulation: num_fixed_tendons=1, num_spatial_tendons=1, ) - object.__setattr__(articulation, "_bindings", bindings.bindings) + # The migrated Articulation reads tendon counts off its OvPhysxView; inject the mock + # view over these bindings so the metadata passthrough resolves without a real view. + object.__setattr__(articulation, "_root_view", bindings.view) object.__setattr__(articulation, "_articulation_root_path", "/World/envs/env_0/Robot/root") object.__setattr__(articulation, "_initialize_handle", None) object.__setattr__(articulation, "_invalidate_initialize_handle", None)