Source code for isaaclab_ov.assets.articulation.articulation

# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

# Flag for pyright to ignore type errors in this file.
# pyright: reportPrivateUsage=false

from __future__ import annotations

import logging
import re
import warnings
from collections.abc import Sequence
from typing import Any

import numpy as np
import torch
import warp as wp

from pxr import Usd, UsdPhysics

import isaaclab.sim as sim_utils
from isaaclab.assets.articulation import ordering_kernels
from isaaclab.assets.articulation.articulation_cfg import ArticulationCfg
from isaaclab.assets.articulation.base_articulation import BaseArticulation
from isaaclab.assets.articulation.ordering_resolvers import (
    _BODY_KIND,
    _JOINT_KIND,
    _canonical_joint_dof_name,
)
from isaaclab.physics import PhysicsManager
from isaaclab.utils.buffers import TimestampedBufferWarp
from isaaclab.utils.string import resolve_matching_names
from isaaclab.utils.warp import ProxyArray
from isaaclab.utils.wrench_composer import WrenchComposer

from isaaclab_ov import tensor_types as TT
from isaaclab_ov.assets import kernels as shared_kernels
from isaaclab_ov.physics import OvPhysxManager
from isaaclab_ov.sim.views.ovphysx_view import OvPhysxView

from .articulation_data import ArticulationData
from .kernels import (
    clamp_default_joint_pos_and_update_soft_limits_index_kernel,
    clamp_default_joint_pos_and_update_soft_limits_mask,
    update_soft_joint_pos_limits,
    write_joint_friction_data_to_buffer_index_kernel,
    write_joint_friction_data_to_buffer_mask,
    write_joint_position_with_sim_ids_kernel,
    write_joint_state_with_sim_ids_kernel,
    write_joint_velocity_with_sim_ids_kernel,
)

# import logger
logger = logging.getLogger(__name__)


[docs] class Articulation(BaseArticulation): """An articulation asset class. An articulation is a collection of rigid bodies connected by joints. The joints can be either fixed or actuated. The joints can be of different types, such as revolute, prismatic, D-6, etc. However, the articulation class has currently been tested with revolute and prismatic joints. The class supports both floating-base and fixed-base articulations. The type of articulation is determined based on the root joint of the articulation. If the root joint is fixed, then the articulation is considered a fixed-base system. Otherwise, it is considered a floating-base system. This can be checked using the :attr:`Articulation.is_fixed_base` attribute. For an asset to be considered an articulation, the root prim of the asset must have the `USD ArticulationRootAPI`_. This API is used to define the sub-tree of the articulation using the reduced coordinate formulation. On playing the simulation, the physics engine parses the articulation root prim and creates the corresponding articulation in the physics engine. The articulation root prim can be specified using the :attr:`AssetBaseCfg.prim_path` attribute. OVPhysX exposes per-tensor-type :class:`ovphysx.TensorBinding` objects rather than a single opaque view; binding handles are created eagerly in :meth:`_initialize_impl` and reused across reads and writes. CPU-only bindings (mass, CoM, inertia, joint properties, tendon properties) are routed through pinned-host staging buffers managed by :class:`ArticulationData`. .. _`USD ArticulationRootAPI`: https://openusd.org/dev/api/class_usd_physics_articulation_root_a_p_i.html """ cfg: ArticulationCfg """Configuration instance for the articulation.""" __backend_name__: str = "ovphysx" """The name of the backend for the articulation.""" __backend_native_orderings__: tuple[str, ...] = ("physx",) """OVPhysX tensor-view order already matches the ``"physx"`` convention."""
[docs] def __init__(self, cfg: ArticulationCfg): """Initialize the articulation. Args: cfg: A configuration instance. """ super().__init__(cfg) # 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 """ @property def data(self) -> ArticulationData: return self._data @property def num_instances(self) -> int: return self._num_instances @property def is_fixed_base(self) -> bool: """Whether the articulation is a fixed-base or floating-base system.""" return self._is_fixed_base @property def num_joints(self) -> int: """Number of joints in articulation.""" return self._num_joints @property def num_fixed_tendons(self) -> int: """Number of fixed tendons in articulation.""" return self._num_fixed_tendons @property def num_spatial_tendons(self) -> int: """Number of spatial tendons in articulation.""" return self._num_spatial_tendons @property def num_bodies(self) -> int: """Number of bodies in articulation.""" return self._num_bodies @property def fixed_tendon_names(self) -> list[str]: """Ordered names of fixed tendons in articulation.""" return self._fixed_tendon_names @property def spatial_tendon_names(self) -> list[str]: """Ordered names of spatial tendons in articulation.""" return self._spatial_tendon_names @property def backend_joint_names(self) -> list[str]: """Ordered names of joints as exposed by the active backend.""" return self._joint_names @property def backend_body_names(self) -> list[str]: """Ordered names of bodies as exposed by the active backend.""" return self._body_names @property def root_view(self) -> OvPhysxView: """Root view for the asset. On OVPhysX this is an :class:`~isaaclab_ov.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_ov.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._root_view @property def instantaneous_wrench_composer(self) -> WrenchComposer: """Instantaneous wrench composer. Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench composer are only valid for the current simulation step. At the end of the simulation step, the wrenches set to this object are discarded. This is useful to apply forces that change all the time, things like drag forces for instance. """ return self._instantaneous_wrench_composer @property def permanent_wrench_composer(self) -> WrenchComposer: """Permanent wrench composer. Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench composer are persistent and are applied to the simulation at every step. This is useful to apply forces that are constant over a period of time, things like the thrust of a motor for instance. """ return self._permanent_wrench_composer """ Operations. """ def reset( self, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_mask: wp.array | None = None ) -> None: """Reset the articulation. .. caution:: If both `env_ids` and `env_mask` are provided, then `env_mask` takes precedence over `env_ids`. Args: env_ids: Environment indices. If None, then all indices are used. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). """ if (env_ids is None) or (env_ids == slice(None)): env_ids = slice(None) # reset external wrenches. self._instantaneous_wrench_composer.reset(env_ids, env_mask) self._permanent_wrench_composer.reset(env_ids, env_mask) def write_data_to_sim(self) -> None: """Write external wrenches and joint commands to the simulation. If any explicit actuators are present, then the actuator models are used to compute the joint commands. Otherwise, the joint commands are directly set into the simulation. .. note:: We write external wrench to the simulation here since this function is called before the simulation step. This ensures that the external wrench is applied at every simulation step. """ # write external wrench inst = self._instantaneous_wrench_composer perm = self._permanent_wrench_composer if inst.active or perm.active: if inst.active: if perm.active: inst.add_raw_buffers_from(perm) force_b = inst.out_force_b.warp torque_b = inst.out_torque_b.warp else: force_b = perm.out_force_b.warp torque_b = perm.out_torque_b.warp # rotate body-frame wrenches into the world frame expected by ``LINK_WRENCH``. # Read the link poses directly from the backend-order ``LINK_POSE`` buffer: the # kernel indexes them in backend order (same physical body as the public wrench), # so no public-order pose shadow refresh / reorder launch is needed here. poses = self._data._backend_body_link_pose_w has_body_ordering = self.data.has_body_ordering wp.launch( shared_kernels._body_wrench_to_world_ordered, dim=(self._num_instances, self._num_bodies), inputs=[force_b, torque_b, poses, self._body_user_to_backend_map(), has_body_ordering], outputs=[self._wrench_buf], device=self._device, ) if self._get_binding(TT.LINK_WRENCH) is not None: self._root_view.set_attribute(TT.LINK_WRENCH, self._wrench_buf) if inst.active: inst.reset() # apply actuator models self._apply_actuator_model() # write actions into simulation (zeros are safe when no actuators are active). # ``_applied_torque`` is the actuator-computed output (may differ from the raw # commanded target, e.g. once clipped), so it must be reordered into its own # scratch buffer rather than ``_joint_effort_target_backend``. The latter is the # persistent mirror of the raw target that partial writes rely on for their # unselected joints (see ``set_joint_effort_target_index``/``_mask``). write_effort = self._can_write_effort # position and velocity targets only for implicit actuators write_pos = self._has_implicit_actuators and self._can_write_pos_target write_vel = self._has_implicit_actuators and self._can_write_vel_target if self.data.has_joint_ordering: if write_effort or write_pos or write_vel: # One fused gather replaces the per-target reorder launches. The # fourth joint-acceleration output is disabled. wp.launch( ordering_kernels.reorder_joint_targets_user_to_backend, dim=(self._num_instances, self._num_joints), inputs=[ self._data._applied_torque, self._data._joint_pos_target, self._data._joint_vel_target, self.data.joint_ordering.backend_to_user, write_effort, write_pos, write_vel, False, ], outputs=[ self._applied_torque_backend, self._joint_pos_target_backend, self._joint_vel_target_backend, None, ], device=self._device, ) effort = self._applied_torque_backend pos_target = self._joint_pos_target_backend vel_target = self._joint_vel_target_backend else: effort = self._data._applied_torque pos_target = self._data._joint_pos_target vel_target = self._data._joint_vel_target if write_effort: self._root_view.set_attribute(TT.DOF_ACTUATION_FORCE, effort) if write_pos: self._root_view.set_attribute(TT.DOF_POSITION_TARGET, pos_target) if write_vel: self._root_view.set_attribute(TT.DOF_VELOCITY_TARGET, vel_target) def update(self, dt: float) -> None: """Updates the simulation data. Args: dt: The time step size in seconds. """ self._data.update(dt) """ Operations - Finders. """ def find_bodies( self, name_keys: str | Sequence[str], preserve_order: bool = False, *, as_proxy: bool = False, ) -> tuple[list[int] | ProxyArray, list[str]]: """Find bodies in the articulation based on the name keys. Please check the :func:`isaaclab.utils.string.resolve_matching_names` function for more information on the name matching. Args: name_keys: A regular expression or a list of regular expressions to match the body names. preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. as_proxy: Whether to return cached proxy indices. Defaults to False. Returns: Matched body indices and names. """ body_ids, body_names = resolve_matching_names(name_keys, self.body_names, preserve_order) resolved_ids = self._resolve_finder_indices(body_ids, domain="body", as_proxy=as_proxy, legacy_type="list") return resolved_ids, body_names def find_joints( self, name_keys: str | Sequence[str], joint_subset: list[str] | None = None, preserve_order: bool = False, *, as_proxy: bool = False, ) -> tuple[list[int] | ProxyArray, list[str]]: """Find joints in the articulation based on the name keys. Please see the :func:`isaaclab.utils.string.resolve_matching_names` function for more information on the name matching. Args: name_keys: A regular expression or a list of regular expressions to match the joint names. joint_subset: A subset of joints to search for. Defaults to None, which means all joints in the articulation are searched. preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. as_proxy: Whether to return cached proxy indices. Subset searches use asset-global proxy indices. Defaults to False. Returns: Matched joint indices and names. """ if joint_subset is None: joint_subset = self.joint_names # find joints joint_ids, joint_names = resolve_matching_names(name_keys, joint_subset, preserve_order) proxy_joint_ids = [self.joint_names.index(name) for name in joint_names] resolved_ids = self._resolve_finder_indices( joint_ids, domain="joint", proxy_indices=proxy_joint_ids, as_proxy=as_proxy, legacy_type="list", ) return resolved_ids, joint_names def find_fixed_tendons( self, name_keys: str | Sequence[str], tendon_subsets: list[str] | None = None, preserve_order: bool = False, *, as_proxy: bool = False, ) -> tuple[list[int] | ProxyArray, list[str]]: """Find fixed tendons in the articulation based on the name keys. Please see the :func:`isaaclab.utils.string.resolve_matching_names` function for more information on the name matching. Args: name_keys: A regular expression or a list of regular expressions to match the joint names with fixed tendons. tendon_subsets: A subset of joints with fixed tendons to search for. Defaults to None, which means all joints in the articulation are searched. preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. as_proxy: Whether to return cached proxy indices. Subset searches use asset-global proxy indices. Defaults to False. Returns: Matched fixed-tendon indices and names. """ if tendon_subsets is None: # tendons follow the joint names they are attached to tendon_subsets = self.fixed_tendon_names # find tendons tendon_ids, tendon_names = resolve_matching_names(name_keys, tendon_subsets, preserve_order) proxy_tendon_ids = [self.fixed_tendon_names.index(name) for name in tendon_names] resolved_ids = self._resolve_finder_indices( tendon_ids, domain="fixed_tendon", proxy_indices=proxy_tendon_ids, as_proxy=as_proxy, legacy_type="list", ) return resolved_ids, tendon_names def find_spatial_tendons( self, name_keys: str | Sequence[str], tendon_subsets: list[str] | None = None, preserve_order: bool = False, *, as_proxy: bool = False, ) -> tuple[list[int] | ProxyArray, list[str]]: """Find spatial tendons in the articulation based on the name keys. Please see the :func:`isaaclab.utils.string.resolve_matching_names` function for more information on the name matching. Args: name_keys: A regular expression or a list of regular expressions to match the tendon names. tendon_subsets: A subset of tendons to search for. Defaults to None, which means all tendons in the articulation are searched. preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. as_proxy: Whether to return cached proxy indices. Subset searches use asset-global proxy indices. Defaults to False. Returns: Matched spatial-tendon indices and names. """ if tendon_subsets is None: tendon_subsets = self.spatial_tendon_names # find tendons tendon_ids, tendon_names = resolve_matching_names(name_keys, tendon_subsets, preserve_order) proxy_tendon_ids = [self.spatial_tendon_names.index(name) for name in tendon_names] resolved_ids = self._resolve_finder_indices( tendon_ids, domain="spatial_tendon", proxy_indices=proxy_tendon_ids, as_proxy=as_proxy, legacy_type="list", ) return resolved_ids, tendon_names """ Operations - State Writers. """ def write_root_pose_to_sim_index( self, *, root_pose: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root pose over selected environment indices into the simulation. The root pose comprises of the cartesian position and quaternion orientation in (x, y, z, w). .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_pose: Root poses in simulation frame. Shape is (len(env_ids), 7) or (len(env_ids),) with dtype wp.transformf. env_ids: Environment indices. If None, then all indices are used. skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ self.write_root_link_pose_to_sim_index(root_pose=root_pose, env_ids=env_ids, skip_forward=skip_forward) def write_root_pose_to_sim_mask( self, *, root_pose: torch.Tensor | wp.array, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root pose over selected environment mask into the simulation. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_pose: Root poses in simulation frame. Shape is (num_instances, 7) or (num_instances,) with dtype wp.transformf. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ self.write_root_link_pose_to_sim_mask(root_pose=root_pose, env_mask=env_mask, skip_forward=skip_forward) def write_root_link_pose_to_sim_index( self, *, root_pose: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root link pose over selected environment indices into the simulation. The root pose comprises of the cartesian position and quaternion orientation in (x, y, z, w). .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_pose: Root link poses in simulation frame. Shape is (len(env_ids), 7) or (len(env_ids),) with dtype wp.transformf. env_ids: Environment indices. If None, then all indices are used. skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_ids = self._resolve_env_ids(env_ids) self.assert_shape_and_dtype(root_pose, (env_ids.shape[0],), wp.transformf, "root_pose") sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.set_root_link_pose_to_sim_index_kernel(env_ids), dim=env_ids.shape[0], inputs=[root_pose, env_ids], outputs=[self.data.root_link_pose_w, sim_env_ids], device=self._device, ) # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose() self._root_view.set_attribute( TT.ROOT_POSE, self.data._root_link_pose_w.data.view(wp.float32), indices=sim_env_ids ) def write_root_link_pose_to_sim_mask( self, *, root_pose: torch.Tensor | wp.array, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root link pose over selected environment mask into the simulation. The root pose comprises of the cartesian position and quaternion orientation in (x, y, z, w). .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_pose: Root poses in simulation frame. Shape is (num_instances, 7) or (num_instances,) with dtype wp.transformf. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_mask_wp = self._resolve_env_mask(env_mask) self.assert_shape_and_dtype(root_pose, (self._num_instances,), wp.transformf, "root_pose") wp.launch( shared_kernels.set_root_link_pose_to_sim_mask, dim=self._num_instances, inputs=[root_pose, env_mask_wp], outputs=[self.data.root_link_pose_w], device=self._device, ) # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose() 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, *, root_pose: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root center of mass pose over selected environment indices into the simulation. The root pose comprises of the cartesian position and quaternion orientation in (x, y, z, w). The orientation is the orientation of the principal axes of inertia. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_pose: Root center of mass poses in simulation frame. Shape is (len(env_ids), 7) or (len(env_ids),) with dtype wp.transformf. env_ids: Environment indices. If None, then all indices are used. skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_ids = self._resolve_env_ids(env_ids) self.assert_shape_and_dtype(root_pose, (env_ids.shape[0],), wp.transformf, "root_pose") sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.set_root_com_pose_to_sim_index_kernel(env_ids), dim=env_ids.shape[0], inputs=[root_pose, self.data._backend_body_com_pose_b, env_ids], outputs=[self.data.root_com_pose_w, self.data.root_link_pose_w, sim_env_ids], device=self._device, ) # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose(from_link=False) self._root_view.set_attribute( TT.ROOT_POSE, self.data._root_link_pose_w.data.view(wp.float32), indices=sim_env_ids ) def write_root_com_pose_to_sim_mask( self, *, root_pose: torch.Tensor | wp.array, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root center of mass pose over selected environment mask into the simulation. The root pose comprises of the cartesian position and quaternion orientation in (x, y, z, w). The orientation is the orientation of the principal axes of inertia. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_pose: Root center of mass poses in simulation frame. Shape is (num_instances, 7) or (num_instances,) with dtype wp.transformf. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_mask_wp = self._resolve_env_mask(env_mask) self.assert_shape_and_dtype(root_pose, (self._num_instances,), wp.transformf, "root_pose") wp.launch( shared_kernels.set_root_com_pose_to_sim_mask, dim=self._num_instances, inputs=[root_pose, self.data._backend_body_com_pose_b, env_mask_wp], outputs=[self.data.root_com_pose_w, self.data.root_link_pose_w], device=self._device, ) # Let the data class handle the invalidation of pose-dependent properties. if not skip_forward: self.data._reset_pose(from_link=False) 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, *, root_velocity: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root center of mass velocity over selected environment indices into the simulation. The velocity comprises linear velocity (x, y, z) and angular velocity (x, y, z) in that order. .. note:: This sets the velocity of the root's center of mass rather than the root's frame. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_velocity: Root center of mass velocities in simulation world frame. Shape is (len(env_ids), 6) or (len(env_ids),) with dtype wp.spatial_vectorf. env_ids: Environment indices. If None, then all indices are used. skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ self.write_root_com_velocity_to_sim_index( root_velocity=root_velocity, env_ids=env_ids, skip_forward=skip_forward ) def write_root_velocity_to_sim_mask( self, *, root_velocity: torch.Tensor | wp.array, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root center of mass velocity over selected environment mask into the simulation. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_velocity: Root center of mass velocities in simulation world frame. Shape is (num_instances, 6) or (num_instances,) with dtype wp.spatial_vectorf. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ self.write_root_com_velocity_to_sim_mask( root_velocity=root_velocity, env_mask=env_mask, skip_forward=skip_forward ) def write_root_com_velocity_to_sim_index( self, *, root_velocity: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root center of mass velocity over selected environment indices into the simulation. The velocity comprises linear velocity (x, y, z) and angular velocity (x, y, z) in that order. .. note:: This sets the velocity of the root's center of mass rather than the root's frame. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_velocity: Root center of mass velocities in simulation world frame. Shape is (len(env_ids), 6) or (len(env_ids),) with dtype wp.spatial_vectorf. env_ids: Environment indices. If None, then all indices are used. skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_ids = self._resolve_env_ids(env_ids) self.assert_shape_and_dtype(root_velocity, (env_ids.shape[0],), wp.spatial_vectorf, "root_velocity") sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.set_root_com_velocity_to_sim_index_kernel(env_ids), dim=env_ids.shape[0], inputs=[root_velocity, env_ids, self._num_bodies], outputs=[self.data.root_com_vel_w, self.data.body_com_acc_w, sim_env_ids], device=self._device, ) # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity() self._root_view.set_attribute( TT.ROOT_VELOCITY, self.data._root_com_vel_w.data.view(wp.float32), indices=sim_env_ids ) def write_root_com_velocity_to_sim_mask( self, *, root_velocity: torch.Tensor | wp.array, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root center of mass velocity over selected environment mask into the simulation. The velocity comprises linear velocity (x, y, z) and angular velocity (x, y, z) in that order. .. note:: This sets the velocity of the root's center of mass rather than the root's frame. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_velocity: Root center of mass velocities in simulation world frame. Shape is (num_instances, 6) or (num_instances,) with dtype wp.spatial_vectorf. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_mask_wp = self._resolve_env_mask(env_mask) self.assert_shape_and_dtype(root_velocity, (self._num_instances,), wp.spatial_vectorf, "root_velocity") wp.launch( shared_kernels.set_root_com_velocity_to_sim_mask, dim=self._num_instances, inputs=[root_velocity, env_mask_wp, self._num_bodies], outputs=[self.data.root_com_vel_w, self.data.body_com_acc_w], device=self._device, ) # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity() 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, *, root_velocity: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root link velocity over selected environment indices into the simulation. The velocity comprises linear velocity (x, y, z) and angular velocity (x, y, z) in that order. .. note:: This sets the velocity of the root's frame rather than the root's center of mass. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_velocity: Root frame velocities in simulation world frame. Shape is (len(env_ids), 6) or (len(env_ids),) with dtype wp.spatial_vectorf. env_ids: Environment indices. If None, then all indices are used. skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_ids = self._resolve_env_ids(env_ids) self.assert_shape_and_dtype(root_velocity, (env_ids.shape[0],), wp.spatial_vectorf, "root_velocity") sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.set_root_link_velocity_to_sim_index_kernel(env_ids), dim=env_ids.shape[0], inputs=[ root_velocity, self.data._backend_body_com_pose_b, self.data.root_link_pose_w, env_ids, self._num_bodies, ], outputs=[self.data.root_link_vel_w, self.data.root_com_vel_w, self.data.body_com_acc_w, sim_env_ids], device=self._device, ) # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity(from_com=False) self._root_view.set_attribute( TT.ROOT_VELOCITY, self.data._root_com_vel_w.data.view(wp.float32), indices=sim_env_ids ) def write_root_link_velocity_to_sim_mask( self, *, root_velocity: torch.Tensor | wp.array, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set the root link velocity over selected environment mask into the simulation. The velocity comprises linear velocity (x, y, z) and angular velocity (x, y, z) in that order. .. note:: This sets the velocity of the root's frame rather than the root's center of mass. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: root_velocity: Root frame velocities in simulation world frame. Shape is (num_instances, 6) or (num_instances,) with dtype wp.spatial_vectorf. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ env_mask_wp = self._resolve_env_mask(env_mask) self.assert_shape_and_dtype(root_velocity, (self._num_instances,), wp.spatial_vectorf, "root_velocity") wp.launch( shared_kernels.set_root_link_velocity_to_sim_mask, dim=self._num_instances, inputs=[ root_velocity, self.data._backend_body_com_pose_b, self.data.root_link_pose_w, env_mask_wp, self._num_bodies, ], outputs=[self.data.root_link_vel_w, self.data.root_com_vel_w, self.data.body_com_acc_w], device=self._device, ) # Let the data class handle the invalidation of velocity-dependent properties. if not skip_forward: self.data._reset_velocity(from_com=False) 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_state_to_sim_index( self, *, position: torch.Tensor | wp.array, velocity: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set joint positions and velocities over selected indices into the simulation. The joint position and velocity caches, finite-difference velocity baseline, and joint acceleration cache are updated in one device kernel. .. note:: This method expects partial data. Args: position: Joint positions [m or rad, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. velocity: Joint velocities [m/s or rad/s, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ joint_selection_is_partial = joint_ids is not None env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) expected_shape = (env_ids.shape[0], joint_ids.shape[0]) self.assert_shape_and_dtype(position, expected_shape, wp.float32, "position") self.assert_shape_and_dtype(velocity, expected_shape, wp.float32, "velocity") if env_ids.shape[0] == 0 or joint_ids.shape[0] == 0: return joint_pos_backend = self._data._get_joint_pos_write_buffer(joint_selection_is_partial) joint_vel_backend = self._data._get_joint_vel_write_buffer(joint_selection_is_partial) sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( write_joint_state_with_sim_ids_kernel(env_ids, joint_ids), dim=expected_shape, inputs=[ position, velocity, env_ids, joint_ids, self._joint_user_to_backend_map(), self.data.has_joint_ordering, ], outputs=[ self._data._joint_pos_buf.data, self._data._joint_vel_buf.data, self._data._previous_joint_vel, self._data._joint_acc.data, joint_pos_backend, joint_vel_backend, sim_env_ids, ], device=self._device, ) self._data._joint_acc.timestamp = self._data._sim_timestamp if not skip_forward: self._data._reset_pose() self._data._reset_velocity() self._root_view.set_attribute(TT.DOF_POSITION, joint_pos_backend, indices=sim_env_ids) self._root_view.set_attribute(TT.DOF_VELOCITY, joint_vel_backend, indices=sim_env_ids) def write_joint_position_to_sim_index( self, *, position: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set joint positions over selected env / joint indices into the simulation. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: position: Joint positions [m or rad, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ joint_selection_is_partial = joint_ids is not None env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) self.assert_shape_and_dtype(position, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "position") if env_ids.shape[0] == 0 or joint_ids.shape[0] == 0: return joint_pos_backend = self._data._get_joint_pos_write_buffer(joint_selection_is_partial) has_joint_ordering = self.data.has_joint_ordering sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( write_joint_position_with_sim_ids_kernel(env_ids, joint_ids), dim=(env_ids.shape[0], joint_ids.shape[0]), inputs=[position, env_ids, joint_ids, self._joint_user_to_backend_map(), has_joint_ordering], outputs=[self._data._joint_pos_buf.data, joint_pos_backend, sim_env_ids], device=self._device, ) # Let the data class handle the invalidation of pose- and velocity-dependent properties. # A position write does not change joint velocities, so the joint acceleration is unaffected. if not skip_forward: self._data._reset_pose() self._data._reset_velocity() self._root_view.set_attribute(TT.DOF_POSITION, joint_pos_backend, indices=sim_env_ids) def write_joint_position_to_sim_mask( self, *, position: torch.Tensor | wp.array, env_mask: wp.array | None = None, joint_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set joint positions over selected env / joint masks into the simulation. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: position: Joint positions [m or rad, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ joint_selection_is_partial = joint_mask is not None env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) self.assert_shape_and_dtype(position, (self._num_instances, self._num_joints), wp.float32, "position") joint_pos_backend = self._data._get_joint_pos_write_buffer(joint_selection_is_partial) has_joint_ordering = self.data.has_joint_ordering ordering_kernels.write_float_user_to_backend_with_mask( position, env_mask_wp, joint_mask_wp, self._joint_user_to_backend_map(), has_joint_ordering, self._data._joint_pos_buf.data, joint_pos_backend, device=self._device, ) # Let the data class handle the invalidation of pose- and velocity-dependent properties. # A position write does not change joint velocities, so the joint acceleration is unaffected. if not skip_forward: self._data._reset_pose() self._data._reset_velocity() self._root_view.set_attribute(TT.DOF_POSITION, joint_pos_backend, mask=env_mask_wp) def write_joint_velocity_to_sim_index( self, *, velocity: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, skip_forward: bool = False, ) -> None: """Set joint velocities over selected env / joint indices into the simulation. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: velocity: Joint velocities [m/s or rad/s, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ joint_selection_is_partial = joint_ids is not None env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) self.assert_shape_and_dtype(velocity, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "velocity") if env_ids.shape[0] == 0 or joint_ids.shape[0] == 0: return joint_vel_backend = self._data._get_joint_vel_write_buffer(joint_selection_is_partial) has_joint_ordering = self.data.has_joint_ordering sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( write_joint_velocity_with_sim_ids_kernel(env_ids, joint_ids), dim=(env_ids.shape[0], joint_ids.shape[0]), inputs=[velocity, env_ids, joint_ids, self._joint_user_to_backend_map(), has_joint_ordering], outputs=[ self._data._joint_vel_buf.data, self._data._previous_joint_vel, self._data._joint_acc.data, joint_vel_backend, sim_env_ids, ], device=self._device, ) self._data._joint_acc.timestamp = self._data._sim_timestamp if not skip_forward: self._data._reset_velocity() self._root_view.set_attribute(TT.DOF_VELOCITY, joint_vel_backend, indices=sim_env_ids) def write_joint_velocity_to_sim_mask( self, *, velocity: torch.Tensor | wp.array, env_mask: wp.array | None = None, joint_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Set joint velocities over selected env / joint masks into the simulation. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: velocity: Joint velocities [m/s or rad/s, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ joint_selection_is_partial = joint_mask is not None env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) self.assert_shape_and_dtype(velocity, (self._num_instances, self._num_joints), wp.float32, "velocity") joint_vel_backend = self._data._get_joint_vel_write_buffer(joint_selection_is_partial) has_joint_ordering = self.data.has_joint_ordering wp.launch( ordering_kernels.write_joint_vel_user_to_backend_with_mask, dim=(self._num_instances, self._num_joints), inputs=[velocity, env_mask_wp, joint_mask_wp, self._joint_user_to_backend_map(), has_joint_ordering], outputs=[ self._data._joint_vel_buf.data, self._data._previous_joint_vel, self._data._joint_acc.data, joint_vel_backend, ], device=self._device, ) self._data._joint_acc.timestamp = self._data._sim_timestamp if not skip_forward: self._data._reset_velocity() self._root_view.set_attribute(TT.DOF_VELOCITY, joint_vel_backend, mask=env_mask_wp) def write_joint_state_to_sim_mask( self, *, position: torch.Tensor | wp.array, velocity: torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, skip_forward: bool = False, ) -> None: """Write joint positions and velocities over selected environment mask into the simulation. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: position: Joint positions [m or rad, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. velocity: Joint velocities [m/s or rad/s, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). skip_forward: Whether to skip invalidating cached data after the write. When True, the caller must invalidate stale cached data before reading it back. Defaults to False. """ joint_selection_is_partial = joint_mask is not None env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) self.assert_shape_and_dtype(position, (self._num_instances, self._num_joints), wp.float32, "position") self.assert_shape_and_dtype(velocity, (self._num_instances, self._num_joints), wp.float32, "velocity") joint_pos_backend = self._data._get_joint_pos_write_buffer(joint_selection_is_partial) joint_vel_backend = self._data._get_joint_vel_write_buffer(joint_selection_is_partial) has_joint_ordering = self.data.has_joint_ordering wp.launch( ordering_kernels.write_joint_state_user_to_backend_with_mask, dim=(self._num_instances, self._num_joints), inputs=[ position, velocity, env_mask_wp, joint_mask_wp, self._joint_user_to_backend_map(), has_joint_ordering, ], outputs=[ self._data._joint_pos_buf.data, self._data._joint_vel_buf.data, self._data._previous_joint_vel, self._data._joint_acc.data, joint_pos_backend, joint_vel_backend, ], device=self._device, ) self._data._joint_acc.timestamp = self._data._sim_timestamp if not skip_forward: self._data._reset_pose() self._data._reset_velocity() self._root_view.set_attribute(TT.DOF_POSITION, joint_pos_backend, mask=env_mask_wp) self._root_view.set_attribute(TT.DOF_VELOCITY, joint_vel_backend, mask=env_mask_wp) """ Operations - Simulation Parameters Writers. """ def write_joint_stiffness_to_sim_index( self, *, stiffness: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint stiffness over selected env / joint indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: stiffness: Joint stiffness [N/m or N·m/rad, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) stiffness = self._broadcast_scalar_to_2d(stiffness, shape) self.assert_shape_and_dtype(stiffness, shape, wp.float32, "stiffness") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, joint_ids), dim=shape, inputs=[stiffness, env_ids, joint_ids], outputs=[self._data._joint_stiffness.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_STIFFNESS, self._data._joint_stiffness.data, self._data._joint_stiffness_backend, cpu_buffer=self.data._cpu_joint_stiffness, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_stiffness_to_sim_mask( self, *, stiffness: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint stiffness over selected env / joint masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: stiffness: Joint stiffness [N/m or N·m/rad, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) stiffness = self._broadcast_scalar_to_2d(stiffness, shape) self.assert_shape_and_dtype(stiffness, shape, wp.float32, "stiffness") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[stiffness, env_mask_wp, joint_mask_wp], outputs=[self._data._joint_stiffness.data], device=self._device, ) self._push_joint_property( TT.DOF_STIFFNESS, self._data._joint_stiffness.data, self._data._joint_stiffness_backend, cpu_buffer=self.data._cpu_joint_stiffness, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_damping_to_sim_index( self, *, damping: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint damping over selected env / joint indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_DAMPING`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: damping: Joint damping [N·s/m or N·m·s/rad, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) damping = self._broadcast_scalar_to_2d(damping, shape) self.assert_shape_and_dtype(damping, shape, wp.float32, "damping") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, joint_ids), dim=shape, inputs=[damping, env_ids, joint_ids], outputs=[self._data._joint_damping.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_DAMPING, self._data._joint_damping.data, self._data._joint_damping_backend, cpu_buffer=self.data._cpu_joint_damping, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_damping_to_sim_mask( self, *, damping: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint damping over selected env / joint masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_DAMPING`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: damping: Joint damping [N·s/m or N·m·s/rad, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) damping = self._broadcast_scalar_to_2d(damping, shape) self.assert_shape_and_dtype(damping, shape, wp.float32, "damping") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[damping, env_mask_wp, joint_mask_wp], outputs=[self._data._joint_damping.data], device=self._device, ) self._push_joint_property( TT.DOF_DAMPING, self._data._joint_damping.data, self._data._joint_damping_backend, cpu_buffer=self.data._cpu_joint_damping, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_position_limit_to_sim_index( self, *, limits: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, warn_limit_violation: bool = True, ) -> None: """Set joint position limits over selected env / joint indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_LIMIT`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limits: Joint position limits ``[lower, upper]`` [m or rad, depending on joint type]. Either shape (len(env_ids), len(joint_ids), 2) with dtype wp.float32, or shape (len(env_ids), len(joint_ids)) with dtype wp.vec2f. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). warn_limit_violation: If True, log a warning when the provided limits are inconsistent (lower > upper). Defaults to True. """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) # Position limits cannot be scalar-broadcast (they pair lower/upper); # match PhysX which explicitly rejects floats here. if isinstance(limits, float): raise ValueError("Joint position limits must be a tensor or array, not a float.") # Accept both wp.vec2f shape (N, J) and the legacy (N, J, 2) wp.float32 # form (canonical PhysX/Newton layout uses vec2f). if isinstance(limits, wp.array) and limits.dtype == wp.vec2f: self.assert_shape_and_dtype(limits, (env_ids.shape[0], joint_ids.shape[0]), wp.vec2f, "limits") # Reinterpret the vec2f input as a (N, J, 2) float32 view for the kernel. kernel_limits = wp.array( ptr=limits.ptr, shape=(env_ids.shape[0], joint_ids.shape[0], 2), dtype=wp.float32, device=str(limits.device), copy=False, ) else: self.assert_shape_and_dtype(limits, (env_ids.shape[0], joint_ids.shape[0], 2), wp.float32, "limits") kernel_limits = limits if env_ids.shape[0] == 0 or joint_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) # Scatter [lower, upper] pairs into the vec2f cache buffer. wp.launch( shared_kernels.write_joint_position_limit_to_buffer_index_kernel(env_ids, joint_ids), dim=(env_ids.shape[0], joint_ids.shape[0]), inputs=[kernel_limits, env_ids, joint_ids], outputs=[self._data._joint_pos_limits.data, sim_env_ids], device=self._device, ) # Clamp default_joint_pos to the new limits and refresh soft_joint_pos_limits. clamped_count = wp.zeros(1, dtype=wp.int32, device=self._device) wp.launch( clamp_default_joint_pos_and_update_soft_limits_index_kernel(env_ids, joint_ids), dim=(env_ids.shape[0], joint_ids.shape[0]), inputs=[ self._data._joint_pos_limits.data, env_ids, joint_ids, self.cfg.soft_joint_pos_limit_factor, ], outputs=[ self._data._default_joint_pos, self._data._soft_joint_pos_limits, clamped_count, ], device=self._device, ) if clamped_count.numpy()[0] > 0: violation_message = ( "Some default joint positions are outside of the range of the new joint limits. Default joint" " positions will be clamped to be within the new joint limits." ) if warn_limit_violation: logger.warning(violation_message) else: logger.info(violation_message) # Stage to pinned-host CPU: flatten the vec2f buffer to float32 view. self._push_joint_property( TT.DOF_LIMIT, self._data._joint_pos_limits.data, self._data._joint_pos_limits_backend, cpu_buffer=self.data._cpu_joint_position_limit, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_position_limit_to_sim_mask( self, *, limits: torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, warn_limit_violation: bool = True, ) -> None: """Set joint position limits over selected env / joint masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_LIMIT`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limits: Joint position limits ``[lower, upper]`` [m or rad, depending on joint type]. Either shape (num_instances, num_joints, 2) with dtype wp.float32, or shape (num_instances, num_joints) with dtype wp.vec2f. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). warn_limit_violation: If True, log a warning when the provided limits are inconsistent (lower > upper). Defaults to True. """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) # Position limits cannot be scalar-broadcast (they pair lower/upper); # match PhysX which explicitly rejects floats here. if isinstance(limits, float): raise ValueError("Joint position limits must be a tensor or array, not a float.") # Accept both wp.vec2f shape (N, J) and the legacy (N, J, 2) wp.float32 # form (canonical PhysX/Newton layout uses vec2f). if isinstance(limits, wp.array) and limits.dtype == wp.vec2f: self.assert_shape_and_dtype(limits, (self._num_instances, self._num_joints), wp.vec2f, "limits") kernel_limits = wp.array( ptr=limits.ptr, shape=(self._num_instances, self._num_joints, 2), dtype=wp.float32, device=str(limits.device), copy=False, ) else: self.assert_shape_and_dtype(limits, (self._num_instances, self._num_joints, 2), wp.float32, "limits") kernel_limits = limits wp.launch( shared_kernels.write_joint_position_limit_to_buffer_mask, dim=(self._num_instances, self._num_joints), inputs=[kernel_limits, env_mask_wp, joint_mask_wp], outputs=[self._data._joint_pos_limits.data], device=self._device, ) # Clamp default_joint_pos to the new limits and refresh soft_joint_pos_limits. clamped_count = wp.zeros(1, dtype=wp.int32, device=self._device) wp.launch( clamp_default_joint_pos_and_update_soft_limits_mask, dim=(self._num_instances, self._num_joints), inputs=[ self._data._joint_pos_limits.data, env_mask_wp, joint_mask_wp, self.cfg.soft_joint_pos_limit_factor, ], outputs=[ self._data._default_joint_pos, self._data._soft_joint_pos_limits, clamped_count, ], device=self._device, ) if clamped_count.numpy()[0] > 0: violation_message = ( "Some default joint positions are outside of the range of the new joint limits. Default joint" " positions will be clamped to be within the new joint limits." ) if warn_limit_violation: logger.warning(violation_message) else: logger.info(violation_message) self._push_joint_property( TT.DOF_LIMIT, self._data._joint_pos_limits.data, self._data._joint_pos_limits_backend, cpu_buffer=self.data._cpu_joint_position_limit, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_velocity_limit_to_sim_index( self, *, limits: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint velocity limits over selected env / joint indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_MAX_VELOCITY`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limits: Joint velocity limits [m/s or rad/s, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) limits = self._broadcast_scalar_to_2d(limits, shape) self.assert_shape_and_dtype(limits, shape, wp.float32, "limits") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, joint_ids), dim=shape, inputs=[limits, env_ids, joint_ids], outputs=[self._data._joint_vel_limits.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_MAX_VELOCITY, self._data._joint_vel_limits.data, self._data._joint_vel_limits_backend, cpu_buffer=self.data._cpu_joint_velocity_limit, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_velocity_limit_to_sim_mask( self, *, limits: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint velocity limits over selected env / joint masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_MAX_VELOCITY`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limits: Joint velocity limits [m/s or rad/s, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) limits = self._broadcast_scalar_to_2d(limits, shape) self.assert_shape_and_dtype(limits, shape, wp.float32, "limits") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[limits, env_mask_wp, joint_mask_wp], outputs=[self._data._joint_vel_limits.data], device=self._device, ) self._push_joint_property( TT.DOF_MAX_VELOCITY, self._data._joint_vel_limits.data, self._data._joint_vel_limits_backend, cpu_buffer=self.data._cpu_joint_velocity_limit, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_effort_limit_to_sim_index( self, *, limits: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint effort limits over selected env / joint indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_MAX_FORCE`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limits: Joint effort limits [N or N·m, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) limits = self._broadcast_scalar_to_2d(limits, shape) self.assert_shape_and_dtype(limits, shape, wp.float32, "limits") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, joint_ids), dim=shape, inputs=[limits, env_ids, joint_ids], outputs=[self._data._joint_effort_limits.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_MAX_FORCE, self._data._joint_effort_limits.data, self._data._joint_effort_limits_backend, cpu_buffer=self.data._cpu_joint_effort_limit, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_effort_limit_to_sim_mask( self, *, limits: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint effort limits over selected env / joint masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_MAX_FORCE`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limits: Joint effort limits [N or N·m, depending on joint type]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) limits = self._broadcast_scalar_to_2d(limits, shape) self.assert_shape_and_dtype(limits, shape, wp.float32, "limits") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[limits, env_mask_wp, joint_mask_wp], outputs=[self._data._joint_effort_limits.data], device=self._device, ) self._push_joint_property( TT.DOF_MAX_FORCE, self._data._joint_effort_limits.data, self._data._joint_effort_limits_backend, cpu_buffer=self.data._cpu_joint_effort_limit, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_armature_to_sim_index( self, *, armature: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint armature over selected env / joint indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_ARMATURE`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: armature: Joint armature [kg·m²]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) armature = self._broadcast_scalar_to_2d(armature, shape) self.assert_shape_and_dtype(armature, shape, wp.float32, "armature") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, joint_ids), dim=shape, inputs=[armature, env_ids, joint_ids], outputs=[self._data._joint_armature.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_ARMATURE, self._data._joint_armature.data, self._data._joint_armature_backend, cpu_buffer=self.data._cpu_joint_armature, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) self._data._reset_dynamics(mass_matrix=True) def write_joint_armature_to_sim_mask( self, *, armature: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint armature over selected env / joint masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``DOF_ARMATURE`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: armature: Joint armature [kg·m²]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) armature = self._broadcast_scalar_to_2d(armature, shape) self.assert_shape_and_dtype(armature, shape, wp.float32, "armature") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[armature, env_mask_wp, joint_mask_wp], outputs=[self._data._joint_armature.data], device=self._device, ) self._push_joint_property( TT.DOF_ARMATURE, self._data._joint_armature.data, self._data._joint_armature_backend, cpu_buffer=self.data._cpu_joint_armature, mask=self._get_cpu_env_mask(env_mask_wp), ) self._data._reset_dynamics(mass_matrix=True) def write_joint_friction_coefficient_to_sim_index( self, *, joint_friction_coeff: float | torch.Tensor | wp.array, joint_dynamic_friction_coeff: float | torch.Tensor | wp.array | None = None, joint_viscous_friction_coeff: float | torch.Tensor | wp.array | None = None, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: r"""Write joint friction coefficients over selected env / joint indices into the simulation. Mirrors :meth:`isaaclab_physx.assets.Articulation.write_joint_friction_coefficient_to_sim_index`: Coulomb (static & dynamic) friction with an optional viscous term. Any of the three components can be left unset by passing ``None``; the corresponding slot in the combined ``DOF_FRICTION_PROPERTIES`` ``(N, J, 3)`` binding is preserved. ``DOF_FRICTION_PROPERTIES`` is a CPU-only OVPhysX binding, so the write is routed through pinned-host staging. .. note:: This method expects partial data. Each component, if provided, may be a scalar :class:`float` (broadcast to ``(len(env_ids), len(joint_ids))``) or a 2D tensor / warp array. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: joint_friction_coeff: Static friction coefficient :math:`\mu_s` [dimensionless]. joint_dynamic_friction_coeff: Dynamic (Coulomb) friction coefficient :math:`\mu_d`. If ``None``, the dynamic component is preserved. joint_viscous_friction_coeff: Viscous friction coefficient :math:`c_v`. If ``None``, the viscous component is preserved. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) joint_friction_coeff = self._broadcast_scalar_to_2d(joint_friction_coeff, shape) if joint_dynamic_friction_coeff is not None: joint_dynamic_friction_coeff = self._broadcast_scalar_to_2d(joint_dynamic_friction_coeff, shape) if joint_viscous_friction_coeff is not None: joint_viscous_friction_coeff = self._broadcast_scalar_to_2d(joint_viscous_friction_coeff, shape) self.assert_shape_and_dtype(joint_friction_coeff, shape, wp.float32, "joint_friction_coeff") if joint_dynamic_friction_coeff is not None: self.assert_shape_and_dtype(joint_dynamic_friction_coeff, shape, wp.float32, "joint_dynamic_friction_coeff") if joint_viscous_friction_coeff is not None: self.assert_shape_and_dtype(joint_viscous_friction_coeff, shape, wp.float32, "joint_viscous_friction_coeff") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) # refresh the combined (N, J, 3) buffer from the binding so unchanged # components are preserved on the round-trip self._data._read_joint_friction_binding() wp.launch( write_joint_friction_data_to_buffer_index_kernel(env_ids, joint_ids), dim=shape, inputs=[ joint_friction_coeff, joint_dynamic_friction_coeff, joint_viscous_friction_coeff, env_ids, joint_ids, ], outputs=[self._data._joint_friction_props_buf.data, sim_env_ids], device=self._device, ) # Stage the combined (N, J, 3) buffer to pinned-host CPU and write to the binding. self._push_joint_property( TT.DOF_FRICTION_PROPERTIES, self._data._joint_friction_props_buf.data, self._data._joint_friction_props_backend, component_count=3, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_friction_coefficient_to_sim_mask( self, *, joint_friction_coeff: float | torch.Tensor | wp.array, joint_dynamic_friction_coeff: float | torch.Tensor | wp.array | None = None, joint_viscous_friction_coeff: float | torch.Tensor | wp.array | None = None, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: r"""Mask variant of :meth:`write_joint_friction_coefficient_to_sim_index`. Args: joint_friction_coeff: Static friction coefficient :math:`\mu_s`. Full data, shape ``(num_instances, num_joints)``. May be a scalar :class:`float`. joint_dynamic_friction_coeff: Dynamic friction. ``None`` to preserve. joint_viscous_friction_coeff: Viscous friction. ``None`` to preserve. joint_mask: Joint mask. If None, all joints are updated. env_mask: Environment mask. If None, all instances are updated. """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) joint_friction_coeff = self._broadcast_scalar_to_2d(joint_friction_coeff, shape) if joint_dynamic_friction_coeff is not None: joint_dynamic_friction_coeff = self._broadcast_scalar_to_2d(joint_dynamic_friction_coeff, shape) if joint_viscous_friction_coeff is not None: joint_viscous_friction_coeff = self._broadcast_scalar_to_2d(joint_viscous_friction_coeff, shape) self.assert_shape_and_dtype(joint_friction_coeff, shape, wp.float32, "joint_friction_coeff") if joint_dynamic_friction_coeff is not None: self.assert_shape_and_dtype(joint_dynamic_friction_coeff, shape, wp.float32, "joint_dynamic_friction_coeff") if joint_viscous_friction_coeff is not None: self.assert_shape_and_dtype(joint_viscous_friction_coeff, shape, wp.float32, "joint_viscous_friction_coeff") # refresh the (N, J, 3) buffer first (see ``_index`` variant) self._data._read_joint_friction_binding() wp.launch( write_joint_friction_data_to_buffer_mask, dim=shape, inputs=[ joint_friction_coeff, joint_dynamic_friction_coeff, joint_viscous_friction_coeff, env_mask_wp, joint_mask_wp, ], outputs=[self._data._joint_friction_props_buf.data], device=self._device, ) self._push_joint_property( TT.DOF_FRICTION_PROPERTIES, self._data._joint_friction_props_buf.data, self._data._joint_friction_props_backend, component_count=3, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_dynamic_friction_coefficient_to_sim_index( self, *, joint_dynamic_friction_coeff: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: r"""Write joint dynamic friction coefficients over selected env / joint indices into the simulation. Mirrors :meth:`isaaclab_physx.assets.Articulation.write_joint_dynamic_friction_coefficient_to_sim_index`: updates only the dynamic (Coulomb) slot of the combined ``DOF_FRICTION_PROPERTIES`` ``(N, J, 3)`` binding; the static and viscous components are preserved. ``DOF_FRICTION_PROPERTIES`` is a CPU-only OVPhysX binding, so the write is routed through pinned-host staging. .. note:: This method expects partial data. ``joint_dynamic_friction_coeff`` may be a scalar :class:`float` (broadcast to ``(len(env_ids), len(joint_ids))``) or a 2D tensor / warp array. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: joint_dynamic_friction_coeff: Dynamic (Coulomb) friction coefficient :math:`\mu_d` [dimensionless]. Shape is ``(len(env_ids), len(joint_ids))`` with dtype wp.float32, or a scalar that is broadcast. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) joint_dynamic_friction_coeff = self._broadcast_scalar_to_2d(joint_dynamic_friction_coeff, shape) self.assert_shape_and_dtype(joint_dynamic_friction_coeff, shape, wp.float32, "joint_dynamic_friction_coeff") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) # refresh the combined (N, J, 3) buffer from the binding so unchanged # components are preserved on the round-trip self._data._read_joint_friction_binding() wp.launch( write_joint_friction_data_to_buffer_index_kernel(env_ids, joint_ids), dim=shape, inputs=[ None, # in_static — preserved joint_dynamic_friction_coeff, None, # in_viscous — preserved env_ids, joint_ids, ], outputs=[self._data._joint_friction_props_buf.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_FRICTION_PROPERTIES, self._data._joint_friction_props_buf.data, self._data._joint_friction_props_backend, component_count=3, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_dynamic_friction_coefficient_to_sim_mask( self, *, joint_dynamic_friction_coeff: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: r"""Mask variant of :meth:`write_joint_dynamic_friction_coefficient_to_sim_index`. Updates only the dynamic (Coulomb) slot of the combined ``DOF_FRICTION_PROPERTIES`` ``(N, J, 3)`` binding; the static and viscous components are preserved. Args: joint_dynamic_friction_coeff: Dynamic (Coulomb) friction coefficient :math:`\mu_d` [dimensionless]. Full data, shape ``(num_instances, num_joints)``. May be a scalar :class:`float`. joint_mask: Joint mask. If None, all joints are updated. env_mask: Environment mask. If None, all instances are updated. """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) joint_dynamic_friction_coeff = self._broadcast_scalar_to_2d(joint_dynamic_friction_coeff, shape) self.assert_shape_and_dtype(joint_dynamic_friction_coeff, shape, wp.float32, "joint_dynamic_friction_coeff") # refresh the (N, J, 3) buffer first (see ``_index`` variant) self._data._read_joint_friction_binding() wp.launch( write_joint_friction_data_to_buffer_mask, dim=shape, inputs=[ None, # in_static — preserved joint_dynamic_friction_coeff, None, # in_viscous — preserved env_mask_wp, joint_mask_wp, ], outputs=[self._data._joint_friction_props_buf.data], device=self._device, ) self._push_joint_property( TT.DOF_FRICTION_PROPERTIES, self._data._joint_friction_props_buf.data, self._data._joint_friction_props_backend, component_count=3, mask=self._get_cpu_env_mask(env_mask_wp), ) def write_joint_viscous_friction_coefficient_to_sim_index( self, *, joint_viscous_friction_coeff: float | torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: r"""Write joint viscous friction coefficients over selected env / joint indices into the simulation. Mirrors :meth:`isaaclab_physx.assets.Articulation.write_joint_viscous_friction_coefficient_to_sim_index`: updates only the viscous slot of the combined ``DOF_FRICTION_PROPERTIES`` ``(N, J, 3)`` binding; the static and dynamic components are preserved. ``DOF_FRICTION_PROPERTIES`` is a CPU-only OVPhysX binding, so the write is routed through pinned-host staging. .. note:: This method expects partial data. ``joint_viscous_friction_coeff`` may be a scalar :class:`float` (broadcast to ``(len(env_ids), len(joint_ids))``) or a 2D tensor / warp array. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: joint_viscous_friction_coeff: Viscous friction coefficient :math:`c_v` [N·m·s/rad or N·s/m, depending on joint type]. Shape is ``(len(env_ids), len(joint_ids))`` with dtype wp.float32, or a scalar that is broadcast. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) joint_ids = self._resolve_joint_ids(joint_ids) shape = (env_ids.shape[0], joint_ids.shape[0]) joint_viscous_friction_coeff = self._broadcast_scalar_to_2d(joint_viscous_friction_coeff, shape) self.assert_shape_and_dtype(joint_viscous_friction_coeff, shape, wp.float32, "joint_viscous_friction_coeff") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(shape[0]) # refresh the combined (N, J, 3) buffer from the binding so unchanged # components are preserved on the round-trip self._data._read_joint_friction_binding() wp.launch( write_joint_friction_data_to_buffer_index_kernel(env_ids, joint_ids), dim=shape, inputs=[ None, # in_static — preserved None, # in_dynamic — preserved joint_viscous_friction_coeff, env_ids, joint_ids, ], outputs=[self._data._joint_friction_props_buf.data, sim_env_ids], device=self._device, ) self._push_joint_property( TT.DOF_FRICTION_PROPERTIES, self._data._joint_friction_props_buf.data, self._data._joint_friction_props_backend, component_count=3, indices=self._get_cpu_env_ids(env_ids, sim_env_ids), ) def write_joint_viscous_friction_coefficient_to_sim_mask( self, *, joint_viscous_friction_coeff: float | torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: r"""Mask variant of :meth:`write_joint_viscous_friction_coefficient_to_sim_index`. Updates only the viscous slot of the combined ``DOF_FRICTION_PROPERTIES`` ``(N, J, 3)`` binding; the static and dynamic components are preserved. Args: joint_viscous_friction_coeff: Viscous friction coefficient :math:`c_v` [N·m·s/rad or N·s/m, depending on joint type]. Full data, shape ``(num_instances, num_joints)``. May be a scalar :class:`float`. joint_mask: Joint mask. If None, all joints are updated. env_mask: Environment mask. If None, all instances are updated. """ env_mask_wp = self._resolve_env_mask(env_mask) joint_mask_wp = self._resolve_joint_mask(joint_mask) shape = (self._num_instances, self._num_joints) joint_viscous_friction_coeff = self._broadcast_scalar_to_2d(joint_viscous_friction_coeff, shape) self.assert_shape_and_dtype(joint_viscous_friction_coeff, shape, wp.float32, "joint_viscous_friction_coeff") # refresh the (N, J, 3) buffer first (see ``_index`` variant) self._data._read_joint_friction_binding() wp.launch( write_joint_friction_data_to_buffer_mask, dim=shape, inputs=[ None, # in_static — preserved None, # in_dynamic — preserved joint_viscous_friction_coeff, env_mask_wp, joint_mask_wp, ], outputs=[self._data._joint_friction_props_buf.data], device=self._device, ) self._push_joint_property( TT.DOF_FRICTION_PROPERTIES, self._data._joint_friction_props_buf.data, self._data._joint_friction_props_backend, component_count=3, mask=self._get_cpu_env_mask(env_mask_wp), ) """ Operations - Setters. """ def set_masses_index( self, *, masses: torch.Tensor | wp.array, body_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set body masses over selected env / body indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``BODY_MASS`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: masses: Body masses [kg]. Shape is (len(env_ids), len(body_ids)) with dtype wp.float32. body_ids: Body indices. Defaults to None (all bodies). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) body_ids = self._resolve_body_ids(body_ids) self.assert_shape_and_dtype(masses, (env_ids.shape[0], body_ids.shape[0]), wp.float32, "masses") if env_ids.shape[0] == 0 or body_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) has_body_ordering = self.data.has_body_ordering body_mass_backend = self._data._body_mass.data if has_body_ordering: backend_staging = self._data._body_mass_backend body_mass_backend = backend_staging.data ordering_kernels.write_float_user_to_backend_with_indices_and_sim_ids( masses, env_ids, body_ids, self._body_user_to_backend_map(), has_body_ordering, False, self._data._body_mass.data, body_mass_backend, sim_env_ids, device=self._device, ) self._data._body_mass.timestamp = self._data._sim_timestamp cpu_env_ids = self._get_cpu_env_ids(env_ids, sim_env_ids) wp.copy(self.data._cpu_body_mass, body_mass_backend) self._root_view.set_attribute(TT.BODY_MASS, self.data._cpu_body_mass, indices=cpu_env_ids) self._data._reset_dynamics(mass_matrix=True, gravity_compensation=True) def set_masses_mask( self, *, masses: torch.Tensor | wp.array, body_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set body masses over selected env / body masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``BODY_MASS`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: masses: Body masses [kg]. Shape is (num_instances, num_bodies) with dtype wp.float32. body_mask: Body mask. If None, all bodies are updated. Shape is (num_bodies,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) body_mask_wp = self._resolve_body_mask(body_mask) self.assert_shape_and_dtype(masses, (self._num_instances, self._num_bodies), wp.float32, "masses") has_body_ordering = self.data.has_body_ordering body_mass_backend = self._data._body_mass.data if has_body_ordering: backend_staging = self._data._body_mass_backend body_mass_backend = backend_staging.data ordering_kernels.write_float_user_to_backend_with_mask( masses, env_mask_wp, body_mask_wp, self._body_user_to_backend_map(), has_body_ordering, self._data._body_mass.data, body_mass_backend, device=self._device, ) self._data._body_mass.timestamp = self._data._sim_timestamp wp.copy(self.data._cpu_body_mass, body_mass_backend) self._root_view.set_attribute(TT.BODY_MASS, self.data._cpu_body_mass, mask=self._get_cpu_env_mask(env_mask_wp)) self._data._reset_dynamics(mass_matrix=True, gravity_compensation=True) def _set_coms( self, coms: torch.Tensor | wp.array, *, env_sel: Sequence[int] | torch.Tensor | wp.array | None, body_sel: Sequence[int] | torch.Tensor | wp.array | None, all_envs_selected: bool, all_bodies_selected: bool, use_mask: bool, ) -> None: """Write body center-of-mass poses to the public buffer and push them to ``BODY_COM_POSE``. Shared implementation behind :meth:`set_coms_index` and :meth:`set_coms_mask`. This is a CPU-only write routed through pinned-host staging because ``BODY_COM_POSE`` is a CPU-only OVPhysX binding. The public-order pose is always written; under a non-identity body ordering the value is additionally scattered into backend-order staging, and the staging buffer is the one copied to the pinned host and pushed to the binding. The static COM cache validity is preserved: a full (all envs, all bodies) write leaves the cache valid, while a partial write first refreshes the cache (:meth:`~ArticulationData._ensure_body_com_pose_b_current`) and keeps it valid only if it was already current, otherwise invalidating it via the ``0.0`` / ``-1.0`` timestamp sentinel. Args: coms: Body center-of-mass poses [m, quaternion (x, y, z, w)]. Shape is (len(env_ids), len(body_ids)) for index selection or (num_instances, num_bodies) for mask selection, with dtype wp.transformf. env_sel: Environment indices (index selection) or mask (mask selection). None selects all. body_sel: Body indices (index selection) or mask (mask selection). None selects all. all_envs_selected: Whether every environment is selected (the original selector was None). all_bodies_selected: Whether every body is selected (the original selector was None). use_mask: Whether :paramref:`env_sel` and :paramref:`body_sel` are masks (True) or indices (False). """ sim_env_ids = None if use_mask: env_sel = self._resolve_env_mask(env_sel) body_sel = self._resolve_body_mask(body_sel) self.assert_shape_and_dtype(coms, (self._num_instances, self._num_bodies), wp.transformf, "coms") else: env_sel = self._resolve_env_ids(env_sel) body_sel = self._resolve_body_ids(body_sel) self.assert_shape_and_dtype(coms, (env_sel.shape[0], body_sel.shape[0]), wp.transformf, "coms") if env_sel.shape[0] == 0 or body_sel.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_sel.shape[0]) has_body_ordering = self.data.has_body_ordering backend_staging = self._data._body_com_pose_b_backend body_com_backend = self._data._body_com_pose_b.data if has_body_ordering: body_com_backend = backend_staging.data if not all_bodies_selected: self._data._ensure_body_com_pose_b_current() cache_is_valid = (all_envs_selected and all_bodies_selected) or ( self._data._body_com_pose_b.timestamp >= 0.0 and (not has_body_ordering or backend_staging.timestamp >= 0.0) ) if use_mask: ordering_kernels.write_2d_user_to_backend_with_mask( coms, env_sel, body_sel, self._body_user_to_backend_map(), has_body_ordering, self._data._body_com_pose_b.data, body_com_backend, dtype=wp.transformf, device=self._device, ) else: ordering_kernels.write_2d_user_to_backend_with_indices_and_sim_ids( coms, env_sel, body_sel, self._body_user_to_backend_map(), has_body_ordering, False, self._data._body_com_pose_b.data, body_com_backend, sim_env_ids, dtype=wp.transformf, device=self._device, ) self._data._reset_body_com_pose_b_dependents() validity = 0.0 if cache_is_valid else -1.0 self._data._body_com_pose_b.timestamp = validity if has_body_ordering: backend_staging.timestamp = validity wp.copy(self.data._cpu_body_coms, body_com_backend.view(wp.float32)) if use_mask: self._root_view.set_attribute( TT.BODY_COM_POSE, self.data._cpu_body_coms, mask=self._get_cpu_env_mask(env_sel) ) else: self._root_view.set_attribute( TT.BODY_COM_POSE, self.data._cpu_body_coms, indices=self._get_cpu_env_ids(env_sel, sim_env_ids), ) def set_coms_index( self, *, coms: torch.Tensor | wp.array, body_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set body center-of-mass poses over selected env / body indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``BODY_COM_POSE`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: coms: Body center-of-mass poses [m, quaternion (x, y, z, w)]. Shape is (len(env_ids), len(body_ids)) with dtype wp.transformf. body_ids: Body indices. Defaults to None (all bodies). env_ids: Environment indices. Defaults to None (all environments). """ self._set_coms( coms, env_sel=env_ids, body_sel=body_ids, all_envs_selected=env_ids is None, all_bodies_selected=body_ids is None, use_mask=False, ) def set_coms_mask( self, *, coms: torch.Tensor | wp.array, body_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set body center-of-mass poses over selected env / body masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``BODY_COM_POSE`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: coms: Body center-of-mass poses [m, quaternion (x, y, z, w)]. Shape is (num_instances, num_bodies) with dtype wp.transformf. body_mask: Body mask. If None, all bodies are updated. Shape is (num_bodies,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ self._set_coms( coms, env_sel=env_mask, body_sel=body_mask, all_envs_selected=env_mask is None, all_bodies_selected=body_mask is None, use_mask=True, ) def set_inertias_index( self, *, inertias: torch.Tensor | wp.array, body_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set body inertia tensors over selected env / body indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``BODY_INERTIA`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: inertias: Body inertia tensors [kg·m²]. Shape is (len(env_ids), len(body_ids), 9) with dtype wp.float32. body_ids: Body indices. Defaults to None (all bodies). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) body_ids = self._resolve_body_ids(body_ids) self.assert_shape_and_dtype(inertias, (env_ids.shape[0], body_ids.shape[0], 9), wp.float32, "inertias") if env_ids.shape[0] == 0 or body_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) has_body_ordering = self.data.has_body_ordering body_inertia_backend = self._data._body_inertia.data if has_body_ordering: backend_staging = self._data._body_inertia_backend body_inertia_backend = backend_staging.data ordering_kernels.write_3d_user_to_backend_with_indices_and_sim_ids( inertias, env_ids, body_ids, self._body_user_to_backend_map(), has_body_ordering, False, self._data._body_inertia.data, body_inertia_backend, sim_env_ids, dtype=wp.float32, device=self._device, ) self._data._body_inertia.timestamp = self._data._sim_timestamp cpu_env_ids = self._get_cpu_env_ids(env_ids, sim_env_ids) wp.copy(self.data._cpu_body_inertia, body_inertia_backend) self._root_view.set_attribute(TT.BODY_INERTIA, self.data._cpu_body_inertia, indices=cpu_env_ids) self._data._reset_dynamics(mass_matrix=True) def set_inertias_mask( self, *, inertias: torch.Tensor | wp.array, body_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set body inertia tensors over selected env / body masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``BODY_INERTIA`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: inertias: Body inertia tensors [kg·m²]. Shape is (num_instances, num_bodies, 9) with dtype wp.float32. body_mask: Body mask. If None, all bodies are updated. Shape is (num_bodies,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) body_mask_wp = self._resolve_body_mask(body_mask) self.assert_shape_and_dtype(inertias, (self._num_instances, self._num_bodies, 9), wp.float32, "inertias") has_body_ordering = self.data.has_body_ordering body_inertia_backend = self._data._body_inertia.data if has_body_ordering: backend_staging = self._data._body_inertia_backend body_inertia_backend = backend_staging.data ordering_kernels.write_3d_user_to_backend_with_mask( inertias, env_mask_wp, body_mask_wp, self._body_user_to_backend_map(), has_body_ordering, self._data._body_inertia.data, body_inertia_backend, dtype=wp.float32, device=self._device, ) self._data._body_inertia.timestamp = self._data._sim_timestamp wp.copy(self.data._cpu_body_inertia, body_inertia_backend) self._root_view.set_attribute( TT.BODY_INERTIA, self.data._cpu_body_inertia, mask=self._get_cpu_env_mask(env_mask_wp) ) self._data._reset_dynamics(mass_matrix=True) def _write_joint_target( self, target: torch.Tensor | wp.array, *, user_buffer: wp.array, backend_buffer: wp.array | None, tensor_type: TT.TensorType, env_sel: Sequence[int] | torch.Tensor | wp.array | None, joint_sel: Sequence[int] | torch.Tensor | wp.array | None, use_mask: bool, ) -> None: """Write a joint target into the public buffer and push it to the backend binding. Shared implementation behind the six ``set_joint_{position,velocity,effort}_target_{index,mask}`` setters. The public-order target is always written to :paramref:`user_buffer`; under a non-identity joint ordering the value is additionally scattered into :paramref:`backend_buffer` (backend-order staging), and that staging buffer is the one pushed to the simulation. Otherwise :paramref:`user_buffer` is pushed directly. Args: target: Joint targets [m, rad, m/s, rad/s, N, or N·m, depending on the setter and joint type]. Shape is (len(env_ids), len(joint_ids)) for index selection or (num_instances, num_joints) for mask selection, with dtype wp.float32. user_buffer: Public-order destination buffer for the target. backend_buffer: Backend-order staging destination, or None when the joint ordering is identity. It is guaranteed non-None while a non-identity joint ordering is active because :meth:`_ordering_configure_backend_staging` allocates it during initialization. tensor_type: Backend binding key the target is pushed to. env_sel: Environment indices (index selection) or mask (mask selection). None selects all. joint_sel: Joint indices (index selection) or mask (mask selection). None selects all. use_mask: Whether :paramref:`env_sel` and :paramref:`joint_sel` are masks (True) or indices (False). """ if use_mask: env_sel = self._resolve_env_mask(env_sel) joint_sel = self._resolve_joint_mask(joint_sel) self.assert_shape_and_dtype(target, (self._num_instances, self._num_joints), wp.float32, "target") else: env_sel = self._resolve_env_ids(env_sel) joint_sel = self._resolve_joint_ids(joint_sel) self.assert_shape_and_dtype(target, (env_sel.shape[0], joint_sel.shape[0]), wp.float32, "target") if env_sel.shape[0] == 0 or joint_sel.shape[0] == 0: return # Under a non-identity ordering the backend staging receives the reordered copy and is the # buffer pushed to the binding; the identity case writes and pushes the public buffer. has_joint_ordering = self.data.has_joint_ordering if has_joint_ordering: target_backend = backend_buffer else: target_backend = user_buffer if use_mask: ordering_kernels.write_float_user_to_backend_with_mask( target, env_sel, joint_sel, self._joint_user_to_backend_map(), has_joint_ordering, user_buffer, target_backend, device=self._device, ) self._root_view.set_attribute(tensor_type, target_backend, mask=env_sel) else: sim_env_ids = self._sim_env_ids_view(env_sel.shape[0]) ordering_kernels.write_float_user_to_backend_with_indices_and_sim_ids( target, env_sel, joint_sel, self._joint_user_to_backend_map(), has_joint_ordering, False, user_buffer, target_backend, sim_env_ids, device=self._device, ) self._root_view.set_attribute( tensor_type, target_backend, indices=self._get_sim_env_ids(env_sel, sim_env_ids) ) def set_joint_position_target_index( self, *, target: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint position targets into internal buffers using indices. This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call :meth:`write_data_to_sim`. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: target: Joint position targets [m or rad, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ self._write_joint_target( target, user_buffer=self._data._joint_pos_target, backend_buffer=self._joint_pos_target_backend, tensor_type=TT.DOF_POSITION_TARGET, env_sel=env_ids, joint_sel=joint_ids, use_mask=False, ) def set_joint_position_target_mask( self, *, target: torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint position targets into internal buffers using masks. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: target: Joint position targets [m or rad, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ self._write_joint_target( target, user_buffer=self._data._joint_pos_target, backend_buffer=self._joint_pos_target_backend, tensor_type=TT.DOF_POSITION_TARGET, env_sel=env_mask, joint_sel=joint_mask, use_mask=True, ) def set_joint_velocity_target_index( self, *, target: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint velocity targets into internal buffers using indices. This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call :meth:`write_data_to_sim`. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: target: Joint velocity targets [m/s or rad/s, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ self._write_joint_target( target, user_buffer=self._data._joint_vel_target, backend_buffer=self._joint_vel_target_backend, tensor_type=TT.DOF_VELOCITY_TARGET, env_sel=env_ids, joint_sel=joint_ids, use_mask=False, ) def set_joint_velocity_target_mask( self, *, target: torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint velocity targets into internal buffers using masks. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: target: Joint velocity targets [m/s or rad/s, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ self._write_joint_target( target, user_buffer=self._data._joint_vel_target, backend_buffer=self._joint_vel_target_backend, tensor_type=TT.DOF_VELOCITY_TARGET, env_sel=env_mask, joint_sel=joint_mask, use_mask=True, ) def set_joint_effort_target_index( self, *, target: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set joint effort targets into internal buffers using indices. This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call :meth:`write_data_to_sim`. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: target: Joint effort targets [N or N·m, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ self._write_joint_target( target, user_buffer=self._data._joint_effort_target, backend_buffer=self._joint_effort_target_backend, tensor_type=TT.DOF_ACTUATION_FORCE, env_sel=env_ids, joint_sel=joint_ids, use_mask=False, ) def set_joint_effort_target_mask( self, *, target: torch.Tensor | wp.array, joint_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set joint effort targets into internal buffers using masks. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: target: Joint effort targets [N or N·m, depending on joint type]. Shape is (num_instances, num_joints) with dtype wp.float32. joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ self._write_joint_target( target, user_buffer=self._data._joint_effort_target, backend_buffer=self._joint_effort_target_backend, tensor_type=TT.DOF_ACTUATION_FORCE, env_sel=env_mask, joint_sel=joint_mask, use_mask=True, ) """ Operations - Tendons. """ def set_fixed_tendon_stiffness_index( self, *, stiffness: float | torch.Tensor | wp.array, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set fixed-tendon stiffness over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: stiffness: Fixed-tendon stiffness [N/m]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(fixed_tendon_ids)) with dtype wp.float32. fixed_tendon_ids: Fixed-tendon indices. Defaults to None (all fixed tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_fixed_tendon_ids(fixed_tendon_ids) shape = (env_ids.shape[0], tendon_ids.shape[0]) stiffness = self._broadcast_scalar_to_2d(stiffness, shape) self.assert_shape_and_dtype(stiffness, shape, wp.float32, "stiffness") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=shape, inputs=[stiffness, env_ids, tendon_ids], outputs=[self._data._fixed_tendon_stiffness.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.FIXED_TENDON_STIFFNESS, self._data._fixed_tendon_stiffness.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_fixed_tendon_stiffness_mask( self, *, stiffness: float | torch.Tensor | wp.array, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set fixed-tendon stiffness over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: stiffness: Fixed-tendon stiffness [N/m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_fixed_tendons) with dtype wp.float32. fixed_tendon_mask: Fixed-tendon mask. If None, all fixed tendons are updated. Shape is (num_fixed_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_fixed_tendon_mask(fixed_tendon_mask) shape = (self._num_instances, self._num_fixed_tendons) stiffness = self._broadcast_scalar_to_2d(stiffness, shape) self.assert_shape_and_dtype(stiffness, shape, wp.float32, "stiffness") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[stiffness, env_mask_wp, tendon_mask_wp], outputs=[self._data._fixed_tendon_stiffness.data], device=self._device, ) 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, *, damping: float | torch.Tensor | wp.array, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set fixed-tendon damping over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_DAMPING`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: damping: Fixed-tendon damping [N·s/m]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(fixed_tendon_ids)) with dtype wp.float32. fixed_tendon_ids: Fixed-tendon indices. Defaults to None (all fixed tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_fixed_tendon_ids(fixed_tendon_ids) shape = (env_ids.shape[0], tendon_ids.shape[0]) damping = self._broadcast_scalar_to_2d(damping, shape) self.assert_shape_and_dtype(damping, shape, wp.float32, "damping") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=shape, inputs=[damping, env_ids, tendon_ids], outputs=[self._data._fixed_tendon_damping.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.FIXED_TENDON_DAMPING, self._data._fixed_tendon_damping.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_fixed_tendon_damping_mask( self, *, damping: float | torch.Tensor | wp.array, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set fixed-tendon damping over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_DAMPING`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: damping: Fixed-tendon damping [N·s/m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_fixed_tendons) with dtype wp.float32. fixed_tendon_mask: Fixed-tendon mask. If None, all fixed tendons are updated. Shape is (num_fixed_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_fixed_tendon_mask(fixed_tendon_mask) shape = (self._num_instances, self._num_fixed_tendons) damping = self._broadcast_scalar_to_2d(damping, shape) self.assert_shape_and_dtype(damping, shape, wp.float32, "damping") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[damping, env_mask_wp, tendon_mask_wp], outputs=[self._data._fixed_tendon_damping.data], device=self._device, ) 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, *, limit_stiffness: float | torch.Tensor | wp.array, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set fixed-tendon limit stiffness over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_LIMIT_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limit_stiffness: Fixed-tendon limit stiffness [N/m]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(fixed_tendon_ids)) with dtype wp.float32. fixed_tendon_ids: Fixed-tendon indices. Defaults to None (all fixed tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_fixed_tendon_ids(fixed_tendon_ids) shape = (env_ids.shape[0], tendon_ids.shape[0]) limit_stiffness = self._broadcast_scalar_to_2d(limit_stiffness, shape) self.assert_shape_and_dtype(limit_stiffness, shape, wp.float32, "limit_stiffness") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=shape, inputs=[limit_stiffness, env_ids, tendon_ids], outputs=[self._data._fixed_tendon_limit_stiffness.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.FIXED_TENDON_LIMIT_STIFFNESS, self._data._fixed_tendon_limit_stiffness.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_fixed_tendon_limit_stiffness_mask( self, *, limit_stiffness: float | torch.Tensor | wp.array, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set fixed-tendon limit stiffness over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_LIMIT_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limit_stiffness: Fixed-tendon limit stiffness [N/m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_fixed_tendons) with dtype wp.float32. fixed_tendon_mask: Fixed-tendon mask. If None, all fixed tendons are updated. Shape is (num_fixed_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_fixed_tendon_mask(fixed_tendon_mask) shape = (self._num_instances, self._num_fixed_tendons) limit_stiffness = self._broadcast_scalar_to_2d(limit_stiffness, shape) self.assert_shape_and_dtype(limit_stiffness, shape, wp.float32, "limit_stiffness") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[limit_stiffness, env_mask_wp, tendon_mask_wp], outputs=[self._data._fixed_tendon_limit_stiffness.data], device=self._device, ) 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, *, limit: torch.Tensor | wp.array, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set fixed-tendon position limits over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_LIMIT`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limit: Fixed-tendon position limits ``[lower, upper]`` [m]. Shape is (len(env_ids), len(fixed_tendon_ids), 2) with dtype wp.float32. fixed_tendon_ids: Fixed-tendon indices. Defaults to None (all fixed tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_fixed_tendon_ids(fixed_tendon_ids) self.assert_shape_and_dtype(limit, (env_ids.shape[0], tendon_ids.shape[0], 2), wp.float32, "limit") if env_ids.shape[0] == 0 or tendon_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) # Scatter [lower, upper] pairs into the vec2f cache buffer. wp.launch( shared_kernels.write_joint_position_limit_to_buffer_index_kernel(env_ids, tendon_ids), dim=(env_ids.shape[0], tendon_ids.shape[0]), inputs=[limit, env_ids, tendon_ids], outputs=[self._data._fixed_tendon_pos_limits.data, sim_env_ids], device=self._device, ) # reinterpret the vec2f buffer as a (N, T, 2) float32 view for the binding flat_src = wp.array( ptr=self._data._fixed_tendon_pos_limits.data.ptr, shape=(self._num_instances, self._num_fixed_tendons, 2), dtype=wp.float32, device=self._device, copy=False, ) self._root_view.set_attribute( TT.FIXED_TENDON_LIMIT, flat_src, indices=self._get_sim_env_ids(env_ids, sim_env_ids) ) def set_fixed_tendon_position_limit_mask( self, *, limit: torch.Tensor | wp.array, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set fixed-tendon position limits over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_LIMIT`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limit: Fixed-tendon position limits ``[lower, upper]`` [m]. Shape is (num_instances, num_fixed_tendons, 2) with dtype wp.float32. fixed_tendon_mask: Fixed-tendon mask. If None, all fixed tendons are updated. Shape is (num_fixed_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_fixed_tendon_mask(fixed_tendon_mask) self.assert_shape_and_dtype(limit, (self._num_instances, self._num_fixed_tendons, 2), wp.float32, "limit") wp.launch( shared_kernels.write_joint_position_limit_to_buffer_mask, dim=(self._num_instances, self._num_fixed_tendons), inputs=[limit, env_mask_wp, tendon_mask_wp], outputs=[self._data._fixed_tendon_pos_limits.data], device=self._device, ) flat_src = wp.array( ptr=self._data._fixed_tendon_pos_limits.data.ptr, shape=(self._num_instances, self._num_fixed_tendons, 2), dtype=wp.float32, device=self._device, copy=False, ) self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, mask=env_mask_wp) def set_fixed_tendon_rest_length_index( self, *, rest_length: float | torch.Tensor | wp.array, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set fixed-tendon rest lengths over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_REST_LENGTH`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: rest_length: Fixed-tendon rest lengths [m]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(fixed_tendon_ids)) with dtype wp.float32. fixed_tendon_ids: Fixed-tendon indices. Defaults to None (all fixed tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_fixed_tendon_ids(fixed_tendon_ids) shape = (env_ids.shape[0], tendon_ids.shape[0]) rest_length = self._broadcast_scalar_to_2d(rest_length, shape) self.assert_shape_and_dtype(rest_length, shape, wp.float32, "rest_length") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=shape, inputs=[rest_length, env_ids, tendon_ids], outputs=[self._data._fixed_tendon_rest_length.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_fixed_tendon_rest_length_mask( self, *, rest_length: float | torch.Tensor | wp.array, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set fixed-tendon rest lengths over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_REST_LENGTH`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: rest_length: Fixed-tendon rest lengths [m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_fixed_tendons) with dtype wp.float32. fixed_tendon_mask: Fixed-tendon mask. If None, all fixed tendons are updated. Shape is (num_fixed_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_fixed_tendon_mask(fixed_tendon_mask) shape = (self._num_instances, self._num_fixed_tendons) rest_length = self._broadcast_scalar_to_2d(rest_length, shape) self.assert_shape_and_dtype(rest_length, shape, wp.float32, "rest_length") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[rest_length, env_mask_wp, tendon_mask_wp], outputs=[self._data._fixed_tendon_rest_length.data], device=self._device, ) 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, *, offset: float | torch.Tensor | wp.array, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set fixed-tendon offsets over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_OFFSET`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: offset: Fixed-tendon offsets [m]. May be a scalar :class:`float` (broadcast), or shape (len(env_ids), len(fixed_tendon_ids)) with dtype wp.float32. fixed_tendon_ids: Fixed-tendon indices. Defaults to None (all fixed tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_fixed_tendon_ids(fixed_tendon_ids) shape = (env_ids.shape[0], tendon_ids.shape[0]) offset = self._broadcast_scalar_to_2d(offset, shape) self.assert_shape_and_dtype(offset, shape, wp.float32, "offset") if shape[0] == 0 or shape[1] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=shape, inputs=[offset, env_ids, tendon_ids], outputs=[self._data._fixed_tendon_offset.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_fixed_tendon_offset_mask( self, *, offset: float | torch.Tensor | wp.array, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set fixed-tendon offsets over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``FIXED_TENDON_OFFSET`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: offset: Fixed-tendon offsets [m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_fixed_tendons) with dtype wp.float32. fixed_tendon_mask: Fixed-tendon mask. If None, all fixed tendons are updated. Shape is (num_fixed_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_fixed_tendon_mask(fixed_tendon_mask) shape = (self._num_instances, self._num_fixed_tendons) offset = self._broadcast_scalar_to_2d(offset, shape) self.assert_shape_and_dtype(offset, shape, wp.float32, "offset") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[offset, env_mask_wp, tendon_mask_wp], outputs=[self._data._fixed_tendon_offset.data], device=self._device, ) 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, *, fixed_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Push the cached fixed-tendon properties to the simulation in a single batch. PhysX exposes a single ``root_view.set_fixed_tendon_properties`` that writes all six tendon property buffers at once. OVPhysX has no such batch setter, so this method writes each ``FIXED_TENDON_*`` binding individually from the matching ``self._data._fixed_tendon_*`` buffer. .. note:: Only env indices apply to the simulation write; ``fixed_tendon_ids`` is accepted for API parity with PhysX but is unused (the simulation writes all tendons of the selected envs). Args: fixed_tendon_ids: Accepted for PhysX API parity; ignored. env_ids: Environment indices. If None, all environments are written. """ env_ids = self._resolve_env_ids(env_ids) if env_ids.shape[0] == 0: return sim_env_ids = self._get_sim_env_ids(env_ids) for tt, buf in ( (TT.FIXED_TENDON_STIFFNESS, self._data._fixed_tendon_stiffness), (TT.FIXED_TENDON_DAMPING, self._data._fixed_tendon_damping), (TT.FIXED_TENDON_LIMIT_STIFFNESS, self._data._fixed_tendon_limit_stiffness), (TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length), (TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset), ): if self._get_binding(tt) is not None: self._root_view.set_attribute(tt, buf.data, indices=sim_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: flat_src = wp.array( ptr=self._data._fixed_tendon_pos_limits.data.ptr, shape=(self._num_instances, self._num_fixed_tendons, 2), dtype=wp.float32, device=self._device, copy=False, ) self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, indices=sim_env_ids) def write_fixed_tendon_properties_to_sim_mask( self, *, fixed_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Mask variant of :meth:`write_fixed_tendon_properties_to_sim_index`. Args: fixed_tendon_mask: Accepted for PhysX API parity; ignored. env_mask: Environment mask. If None, all environments are written. """ env_mask_wp = self._resolve_env_mask(env_mask) for tt, buf in ( (TT.FIXED_TENDON_STIFFNESS, self._data._fixed_tendon_stiffness), (TT.FIXED_TENDON_DAMPING, self._data._fixed_tendon_damping), (TT.FIXED_TENDON_LIMIT_STIFFNESS, self._data._fixed_tendon_limit_stiffness), (TT.FIXED_TENDON_REST_LENGTH, self._data._fixed_tendon_rest_length), (TT.FIXED_TENDON_OFFSET, self._data._fixed_tendon_offset), ): 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( ptr=self._data._fixed_tendon_pos_limits.data.ptr, shape=(self._num_instances, self._num_fixed_tendons, 2), dtype=wp.float32, device=self._device, copy=False, ) self._root_view.set_attribute(TT.FIXED_TENDON_LIMIT, flat_src, mask=env_mask_wp) def set_spatial_tendon_stiffness_index( self, *, stiffness: float | torch.Tensor | wp.array, spatial_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set spatial-tendon stiffness over selected env / tendon indices into the simulation. ``SPATIAL_TENDON_STIFFNESS`` is a sim-device binding on OVPhysX (tendon properties are applied without a CPU clone), so the write goes directly from the sim-device buffer to the binding. .. note:: This method expects partial data. A scalar :class:`float` is broadcast to ``(len(env_ids), len(spatial_tendon_ids))``. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: stiffness: Spatial-tendon stiffness [N/m]. Scalar :class:`float`, or shape ``(len(env_ids), len(spatial_tendon_ids))`` with dtype wp.float32. spatial_tendon_ids: Spatial-tendon indices. Defaults to None (all spatial tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_spatial_tendon_ids(spatial_tendon_ids) stiffness = self._broadcast_scalar_to_2d(stiffness, (env_ids.shape[0], tendon_ids.shape[0])) self.assert_shape_and_dtype(stiffness, (env_ids.shape[0], tendon_ids.shape[0]), wp.float32, "stiffness") if env_ids.shape[0] == 0 or tendon_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=(env_ids.shape[0], tendon_ids.shape[0]), inputs=[stiffness, env_ids, tendon_ids], outputs=[self._data._spatial_tendon_stiffness.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.SPATIAL_TENDON_STIFFNESS, self._data._spatial_tendon_stiffness.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_spatial_tendon_stiffness_mask( self, *, stiffness: float | torch.Tensor | wp.array, spatial_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set spatial-tendon stiffness over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``SPATIAL_TENDON_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: stiffness: Spatial-tendon stiffness [N/m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_spatial_tendons) with dtype wp.float32. spatial_tendon_mask: Spatial-tendon mask. If None, all spatial tendons are updated. Shape is (num_spatial_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_spatial_tendon_mask(spatial_tendon_mask) shape = (self._num_instances, self._num_spatial_tendons) stiffness = self._broadcast_scalar_to_2d(stiffness, shape) self.assert_shape_and_dtype(stiffness, shape, wp.float32, "stiffness") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[stiffness, env_mask_wp, tendon_mask_wp], outputs=[self._data._spatial_tendon_stiffness.data], device=self._device, ) 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, *, damping: float | torch.Tensor | wp.array, spatial_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set spatial-tendon damping over selected env / tendon indices into the simulation. This is a CPU-only write routed through pinned-host staging because ``SPATIAL_TENDON_DAMPING`` is a CPU-only OVPhysX binding. .. note:: This method expects partial data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: damping: Spatial-tendon damping [N·s/m]. Shape is (len(env_ids), len(spatial_tendon_ids)) with dtype wp.float32. spatial_tendon_ids: Spatial-tendon indices. Defaults to None (all spatial tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_spatial_tendon_ids(spatial_tendon_ids) damping = self._broadcast_scalar_to_2d(damping, (env_ids.shape[0], tendon_ids.shape[0])) self.assert_shape_and_dtype(damping, (env_ids.shape[0], tendon_ids.shape[0]), wp.float32, "damping") if env_ids.shape[0] == 0 or tendon_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=(env_ids.shape[0], tendon_ids.shape[0]), inputs=[damping, env_ids, tendon_ids], outputs=[self._data._spatial_tendon_damping.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.SPATIAL_TENDON_DAMPING, self._data._spatial_tendon_damping.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_spatial_tendon_damping_mask( self, *, damping: float | torch.Tensor | wp.array, spatial_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set spatial-tendon damping over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``SPATIAL_TENDON_DAMPING`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: damping: Spatial-tendon damping [N·s/m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_spatial_tendons) with dtype wp.float32. spatial_tendon_mask: Spatial-tendon mask. If None, all spatial tendons are updated. Shape is (num_spatial_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_spatial_tendon_mask(spatial_tendon_mask) shape = (self._num_instances, self._num_spatial_tendons) damping = self._broadcast_scalar_to_2d(damping, shape) self.assert_shape_and_dtype(damping, shape, wp.float32, "damping") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[damping, env_mask_wp, tendon_mask_wp], outputs=[self._data._spatial_tendon_damping.data], device=self._device, ) 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, *, limit_stiffness: float | torch.Tensor | wp.array, spatial_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set spatial-tendon limit stiffness over selected env / tendon indices into the simulation. ``SPATIAL_TENDON_LIMIT_STIFFNESS`` is a sim-device binding on OVPhysX; the write goes directly from the sim-device buffer to the binding. .. note:: This method expects partial data. A scalar :class:`float` is broadcast to ``(len(env_ids), len(spatial_tendon_ids))``. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limit_stiffness: Spatial-tendon limit stiffness [N/m]. Scalar :class:`float`, or shape ``(len(env_ids), len(spatial_tendon_ids))`` with dtype wp.float32. spatial_tendon_ids: Spatial-tendon indices. Defaults to None (all spatial tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_spatial_tendon_ids(spatial_tendon_ids) limit_stiffness = self._broadcast_scalar_to_2d(limit_stiffness, (env_ids.shape[0], tendon_ids.shape[0])) self.assert_shape_and_dtype( limit_stiffness, (env_ids.shape[0], tendon_ids.shape[0]), wp.float32, "limit_stiffness" ) if env_ids.shape[0] == 0 or tendon_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=(env_ids.shape[0], tendon_ids.shape[0]), inputs=[limit_stiffness, env_ids, tendon_ids], outputs=[self._data._spatial_tendon_limit_stiffness.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_spatial_tendon_limit_stiffness_mask( self, *, limit_stiffness: float | torch.Tensor | wp.array, spatial_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set spatial-tendon limit stiffness over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``SPATIAL_TENDON_LIMIT_STIFFNESS`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: limit_stiffness: Spatial-tendon limit stiffness [N/m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_spatial_tendons) with dtype wp.float32. spatial_tendon_mask: Spatial-tendon mask. If None, all spatial tendons are updated. Shape is (num_spatial_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_spatial_tendon_mask(spatial_tendon_mask) shape = (self._num_instances, self._num_spatial_tendons) limit_stiffness = self._broadcast_scalar_to_2d(limit_stiffness, shape) self.assert_shape_and_dtype(limit_stiffness, shape, wp.float32, "limit_stiffness") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[limit_stiffness, env_mask_wp, tendon_mask_wp], outputs=[self._data._spatial_tendon_limit_stiffness.data], device=self._device, ) 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, *, offset: float | torch.Tensor | wp.array, spatial_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Set spatial-tendon offsets over selected env / tendon indices into the simulation. ``SPATIAL_TENDON_OFFSET`` is a sim-device binding on OVPhysX; the write goes directly from the sim-device buffer to the binding. .. note:: This method expects partial data. A scalar :class:`float` is broadcast to ``(len(env_ids), len(spatial_tendon_ids))``. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: offset: Spatial-tendon offsets [m]. Scalar :class:`float`, or shape ``(len(env_ids), len(spatial_tendon_ids))`` with dtype wp.float32. spatial_tendon_ids: Spatial-tendon indices. Defaults to None (all spatial tendons). env_ids: Environment indices. Defaults to None (all environments). """ env_ids = self._resolve_env_ids(env_ids) tendon_ids = self._resolve_spatial_tendon_ids(spatial_tendon_ids) offset = self._broadcast_scalar_to_2d(offset, (env_ids.shape[0], tendon_ids.shape[0])) self.assert_shape_and_dtype(offset, (env_ids.shape[0], tendon_ids.shape[0]), wp.float32, "offset") if env_ids.shape[0] == 0 or tendon_ids.shape[0] == 0: return sim_env_ids = self._sim_env_ids_view(env_ids.shape[0]) wp.launch( shared_kernels.write_2d_data_to_buffer_with_indices_and_sim_ids_kernel(env_ids, tendon_ids), dim=(env_ids.shape[0], tendon_ids.shape[0]), inputs=[offset, env_ids, tendon_ids], outputs=[self._data._spatial_tendon_offset.data, sim_env_ids], device=self._device, ) self._root_view.set_attribute( TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset.data, indices=self._get_sim_env_ids(env_ids, sim_env_ids), ) def set_spatial_tendon_offset_mask( self, *, offset: float | torch.Tensor | wp.array, spatial_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Set spatial-tendon offsets over selected env / tendon masks into the simulation. This is a CPU-only write routed through pinned-host staging because ``SPATIAL_TENDON_OFFSET`` is a CPU-only OVPhysX binding. .. note:: This method expects full data. .. tip:: Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. However, to allow graphed pipelines, the mask method must be used. Args: offset: Spatial-tendon offsets [m]. May be a scalar :class:`float` (broadcast), or shape (num_instances, num_spatial_tendons) with dtype wp.float32. spatial_tendon_mask: Spatial-tendon mask. If None, all spatial tendons are updated. Shape is (num_spatial_tendons,). env_mask: Environment mask. If None, all instances are updated. Shape is (num_instances,). """ env_mask_wp = self._resolve_env_mask(env_mask) tendon_mask_wp = self._resolve_spatial_tendon_mask(spatial_tendon_mask) shape = (self._num_instances, self._num_spatial_tendons) offset = self._broadcast_scalar_to_2d(offset, shape) self.assert_shape_and_dtype(offset, shape, wp.float32, "offset") wp.launch( shared_kernels.write_2d_data_to_buffer_with_mask, dim=shape, inputs=[offset, env_mask_wp, tendon_mask_wp], outputs=[self._data._spatial_tendon_offset.data], device=self._device, ) 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, *, spatial_tendon_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Push the cached spatial-tendon properties to the simulation in a single batch. Mirrors :meth:`write_fixed_tendon_properties_to_sim_index` for spatial tendons. Only the four wheel-supported tensor types are written; ``ARTICULATION_SPATIAL_TENDON_LIMIT`` and ``ARTICULATION_SPATIAL_TENDON_REST_LENGTH`` are forward-compat stubs (see ``docs/superpowers/specs/2026-04-28-ovphysx-wheel-gaps-for-marco.md``). Args: spatial_tendon_ids: Accepted for PhysX API parity; ignored. env_ids: Environment indices. If None, all environments are written. """ env_ids = self._resolve_env_ids(env_ids) if env_ids.shape[0] == 0: return sim_env_ids = self._get_sim_env_ids(env_ids) for tt, buf in ( (TT.SPATIAL_TENDON_STIFFNESS, self._data._spatial_tendon_stiffness), (TT.SPATIAL_TENDON_DAMPING, self._data._spatial_tendon_damping), (TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness), (TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset), ): if self._get_binding(tt) is not None: self._root_view.set_attribute(tt, buf.data, indices=sim_env_ids) def write_spatial_tendon_properties_to_sim_mask( self, *, spatial_tendon_mask: wp.array | None = None, env_mask: wp.array | None = None, ) -> None: """Mask variant of :meth:`write_spatial_tendon_properties_to_sim_index`.""" env_mask_wp = self._resolve_env_mask(env_mask) for tt, buf in ( (TT.SPATIAL_TENDON_STIFFNESS, self._data._spatial_tendon_stiffness), (TT.SPATIAL_TENDON_DAMPING, self._data._spatial_tendon_damping), (TT.SPATIAL_TENDON_LIMIT_STIFFNESS, self._data._spatial_tendon_limit_stiffness), (TT.SPATIAL_TENDON_OFFSET, self._data._spatial_tendon_offset), ): if self._get_binding(tt) is not None: self._root_view.set_attribute(tt, buf.data, mask=env_mask_wp) """ Internal helper. """ def _initialize_impl(self) -> None: """Initialize the articulation from the OVPhysX simulation backend.""" # obtain global simulation view physx_instance = OvPhysxManager.get_physx_instance() if physx_instance is None: raise RuntimeError("OvPhysxManager has not been initialized yet.") self._ovphysx = physx_instance self._device = OvPhysxManager.get_device() # Resolve the articulation root expression. if self.cfg.articulation_root_prim_path is not None: root_prim_path_expr = self.cfg.prim_path + self.cfg.articulation_root_prim_path else: def has_articulation_root_api(prim) -> bool: return bool(prim.HasAPI(UsdPhysics.ArticulationRootAPI)) resolve_kwargs = {"predicate": has_articulation_root_api, "expected_num_matches": 1} root_matches = sim_utils.resolve_matching_prims_from_source(self.cfg.prim_path, **resolve_kwargs) _, root_prim_path_expr = root_matches[0] # Validate the prim exists on the live stage -- ``create_tensor_binding`` silently # returns a 0-count binding when the pattern matches nothing, surfacing as obscure # AttributeErrors deep in property accessors. Also stash the concrete source-side # root path for tendon discovery downstream. stage = PhysicsManager._sim.stage first_match = sim_utils.find_first_matching_prim(root_prim_path_expr, stage=stage) if first_match is None: raise RuntimeError(f"Failed to find articulation root prim at '{root_prim_path_expr}'.") self._articulation_root_path = first_match.GetPath().pathString # IsaacLab paths may use ``.*`` regex or ``{ENV_REGEX_NS}`` placeholder; ovphysx # ``create_tensor_binding`` expects fnmatch globs. pattern = re.sub(r"\{ENV_REGEX_NS\}", "*", root_prim_path_expr) pattern = sim_utils.path_expr_to_glob(pattern) self._binding_pattern = pattern # eagerly create every binding the data container reads at init, so # failures surface here rather than as KeyError downstream eager_types = [ TT.ROOT_POSE, TT.ROOT_VELOCITY, TT.LINK_POSE, TT.LINK_VELOCITY, TT.LINK_ACCELERATION, TT.DOF_POSITION, TT.DOF_VELOCITY, TT.JACOBIAN, TT.MASS_MATRIX, TT.GRAVITY_FORCE, TT.DOF_STIFFNESS, TT.DOF_DAMPING, TT.DOF_LIMIT, TT.DOF_MAX_VELOCITY, TT.DOF_MAX_FORCE, TT.DOF_ARMATURE, TT.DOF_FRICTION_PROPERTIES, TT.BODY_MASS, 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: self._root_view.try_binding_for(tt) 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 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, 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, TT.FIXED_TENDON_DAMPING, TT.FIXED_TENDON_LIMIT_STIFFNESS, TT.FIXED_TENDON_LIMIT, TT.FIXED_TENDON_REST_LENGTH, TT.FIXED_TENDON_OFFSET, ): self._root_view.try_binding_for(tt) if self._num_spatial_tendons > 0: for tt in ( TT.SPATIAL_TENDON_STIFFNESS, TT.SPATIAL_TENDON_DAMPING, TT.SPATIAL_TENDON_LIMIT_STIFFNESS, TT.SPATIAL_TENDON_OFFSET, ): self._root_view.try_binding_for(tt) # construct the data container; counts come from the view's bindings joint_dof_signs = self._resolve_joint_dof_signs(stage) self._data = ArticulationData(self._root_view, self._device) if -1 in joint_dof_signs: self._data._joint_dof_signs = wp.array(joint_dof_signs, dtype=wp.int32, device=self.device) self._data._has_reversed_joints = True self._resolve_and_install_ordering_maps() self._data.fixed_tendon_names = self._fixed_tendon_names self._data.spatial_tendon_names = self._spatial_tendon_names # allocate asset-side buffers self._create_buffers() # apply initial state from config self._process_cfg() # build actuator instances and write drive properties to PhysX self._process_actuators_cfg() self._can_write_effort = self._get_binding(TT.DOF_ACTUATION_FORCE) is not None self._can_write_pos_target = self._get_binding(TT.DOF_POSITION_TARGET) is not None self._can_write_vel_target = self._get_binding(TT.DOF_VELOCITY_TARGET) is not None # validate the resolved configuration AFTER actuator/tendon processing # so the values reflect any overrides applied by the actuator models self._validate_cfg() # prime the data by performing the first read self.update(0.0) # mark data as ready self._data.is_primed = True def _resolve_joint_dof_signs(self, stage: Usd.Stage) -> tuple[int, ...]: """Resolve joint directions once from the source USD.""" source_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path, stage=stage) if source_prim is None: return (1,) * self.num_joints joint_prims_by_name = {} for prim in Usd.PrimRange(source_prim, Usd.TraverseInstanceProxies()): if not prim.IsA(UsdPhysics.Joint): continue joint_prims_by_name[_JOINT_KIND.resolve_target_name(prim)] = prim body_indices = {name: index for index, name in enumerate(self._body_names)} signs = [] for dof_name in self._joint_names: canonical_dof_name = _canonical_joint_dof_name(dof_name) matches = [ (name, prim) for name, prim in joint_prims_by_name.items() if canonical_dof_name == _canonical_joint_dof_name(name) or canonical_dof_name.startswith(_canonical_joint_dof_name(name) + "_") ] if not matches: signs.append(1) continue joint = UsdPhysics.Joint(max(matches, key=lambda item: len(item[0]))[1]) body0 = joint.GetBody0Rel().GetTargets() body1 = joint.GetBody1Rel().GetTargets() body0_name = _BODY_KIND.resolve_target_name(stage.GetPrimAtPath(body0[0])) if body0 else "" body1_name = _BODY_KIND.resolve_target_name(stage.GetPrimAtPath(body1[0])) if body1 else "" body0_index = body_indices.get(body0_name) body1_index = body_indices.get(body1_name) signs.append(-1 if body0_index is not None and body1_index is not None and body0_index > body1_index else 1) return tuple(signs) def _create_buffers(self) -> None: """Allocate asset-side buffers (index/mask constants, wrench buf, pinned CPU staging).""" N = self._num_instances B = self._num_bodies J = self._num_joints FT = self._num_fixed_tendons ST = self._num_spatial_tendons device = self._device # Index constants. self._ALL_INDICES = wp.array(np.arange(N, dtype=np.int32), device=device) self._ALL_BODY_INDICES = wp.array(np.arange(B, dtype=np.int32), device=device) self._ALL_JOINT_INDICES = wp.array(np.arange(J, dtype=np.int32), device=device) self._ALL_FIXED_TENDON_INDICES = wp.array(np.arange(FT, dtype=np.int32), device=device) self._ALL_SPATIAL_TENDON_INDICES = wp.array(np.arange(ST, dtype=np.int32), device=device) self._sim_env_ids = wp.empty(N, dtype=wp.int32, device=device) self._sim_env_ids_views: dict[int, wp.array] = {} self._joint_pos_target_backend: wp.array | None = None self._joint_vel_target_backend: wp.array | None = None self._joint_effort_target_backend: wp.array | None = None self._applied_torque_backend: wp.array | None = None self._ordering_configure_backend_staging() # All-true masks. self._ALL_TRUE_ENV_MASK = wp.array(np.ones(N, dtype=bool), dtype=wp.bool, device=device) self._ALL_TRUE_BODY_MASK = wp.array(np.ones(B, dtype=bool), dtype=wp.bool, device=device) self._ALL_TRUE_JOINT_MASK = wp.array(np.ones(J, dtype=bool), dtype=wp.bool, device=device) self._ALL_TRUE_FIXED_TENDON_MASK = wp.array(np.ones(FT, dtype=bool), dtype=wp.bool, device=device) self._ALL_TRUE_SPATIAL_TENDON_MASK = wp.array(np.ones(ST, dtype=bool), dtype=wp.bool, device=device) # Wrench buffer (force, torque, position) per body, written by the # ``_body_wrench_to_world_ordered`` kernel and consumed by the # ``LINK_WRENCH`` binding which expects the 3D ``(N, B, 9)`` shape. self._wrench_buf = wp.zeros((N, B, 9), dtype=wp.float32, device=device) # Wrench composers. self._instantaneous_wrench_composer = WrenchComposer(self) self._permanent_wrench_composer = WrenchComposer(self) # Wrench scratch buffer (used by _apply_external_wrenches, not yet allocated above). # Joint-index arrays for each actuator (populated by _process_actuators_cfg). self._joint_ids_per_actuator: dict[str, slice | torch.Tensor] = {} # Pinned-host CPU staging for env ids/masks (PR #5329 pattern). self._cpu_env_ids_all = wp.zeros(N, dtype=wp.int32, device="cpu", pinned=True) wp.copy(self._cpu_env_ids_all, self._ALL_INDICES) self._cpu_env_ids = wp.empty(N, dtype=wp.int32, device="cpu", pinned=True) self._cpu_env_ids_views: dict[int, wp.array] = {} self._cpu_env_mask = wp.zeros(N, dtype=wp.bool, device="cpu", pinned=True) def _process_cfg(self) -> None: """Populate default state buffers from the config (mirrors RigidObject and Newton Articulation).""" cfg = self.cfg N = self._num_instances D = self._num_joints dev = self._device # Default root state from config (matching PhysX pattern). default_root_pose = tuple(cfg.init_state.pos) + tuple(cfg.init_state.rot) default_root_vel = tuple(cfg.init_state.lin_vel) + tuple(cfg.init_state.ang_vel) np_pose = np.tile(np.array(default_root_pose, dtype=np.float32), (N, 1)) np_vel = np.tile(np.array(default_root_vel, dtype=np.float32), (N, 1)) self._data.default_root_pose = wp.array(np_pose, dtype=wp.transformf, device=dev) self._data.default_root_vel = wp.array(np_vel, dtype=wp.spatial_vectorf, device=dev) # Default joint positions / velocities from config patterns. # cfg.init_state.joint_pos is a dict[str, float] where keys are regex patterns # matching joint names. We expand this into a (N, D) buffer. self._resolve_joint_values(cfg.init_state.joint_pos, self._data._default_joint_pos) self._resolve_joint_values(cfg.init_state.joint_vel, self._data._default_joint_vel) # Compute soft joint position limits from the hard limits read from the binding # (or zeros if no joints). This matches the PhysX/Newton path. if D > 0: wp.launch( update_soft_joint_pos_limits, dim=(N, D), inputs=[self._data.joint_pos_limits, cfg.soft_joint_pos_limit_factor], outputs=[self._data._soft_joint_pos_limits], device=dev, ) def _process_tendons(self) -> None: """Discover tendon counts from binding metadata and names from USD. Tendon counts come from the ovphysx binding metadata. Tendon names are recovered from the in-memory USD articulation subtree because ovphysx exposes joint names/counts, but not the per-joint USD paths that the PhysX backend can query directly. """ self._fixed_tendon_names = [] self._spatial_tendon_names = [] 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_usda = OvPhysxManager._stage_usda if stage_usda is not None: try: from pxr import Sdf, Usd from isaaclab.sim.utils.queries import get_all_matching_child_prims layer = Sdf.Layer.CreateAnonymous("isaaclab_ov.usda") if not layer.ImportFromString(stage_usda): raise RuntimeError("Failed to import the serialized OVPhysX stage.") stage = Usd.Stage.Open(layer) articulation_root_path = getattr(self, "_articulation_root_path", None) if articulation_root_path is None: joint_prims = stage.Traverse() else: joint_prims = get_all_matching_child_prims( articulation_root_path, predicate=lambda p: p.IsA(UsdPhysics.Joint), stage=stage, traverse_instance_prims=False, ) for prim in joint_prims: if not prim.IsA(UsdPhysics.Joint): continue schema_names = list(prim.GetAppliedSchemas()) metadata = prim.GetMetadata("apiSchemas") if metadata is not None: for field in ("prependedItems", "appendedItems", "explicitItems"): items = getattr(metadata, field, None) if items: schema_names.extend(str(item) for item in items) schemas_str = " ".join(schema_names) name = prim.GetPath().name if "PhysxTendonAxisRootAPI" in schemas_str: self._fixed_tendon_names.append(name) elif ( "PhysxTendonAttachmentRootAPI" in schemas_str or "PhysxTendonAttachmentLeafAPI" in schemas_str ): self._spatial_tendon_names.append(name) except Exception: logger.debug("Could not parse in-memory USD stage for tendon names", exc_info=True) def _get_binding(self, tensor_type: int): """Return a cached TensorBinding, creating it on first access. Bindings are lightweight handles (a pointer + shape metadata into PhysX's shared GPU buffer). Creating one does NOT allocate new GPU memory -- the underlying simulation buffers are allocated once by PhysX regardless of how many bindings point into them. Still, we defer creation so that tensor types the user never queries are never looked up. Args: tensor_type: The TensorType constant identifying which simulation buffer to bind (e.g. :attr:`~isaaclab_ov.tensor_types.ROOT_POSE`). Returns: A TensorBinding object, or ``None`` if the binding could not be created. """ 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. Builds values on CPU then copies to buffer's device (GPU arrays' ``.numpy()`` returns a read-only copy, not a writable view). Args: pattern_dict: A mapping from regex pattern strings to scalar values. Matches joint names returned by :attr:`joint_names`. buffer: Target warp array of shape ``(num_instances, num_joints)`` to populate. """ buf_np = buffer.numpy() modified = False for pattern, value in pattern_dict.items(): for j, name in enumerate(self.joint_names): if re.fullmatch(pattern, name): buf_np[:, j] = value modified = True if modified: wp.copy(buffer, wp.from_numpy(buf_np, dtype=buffer.dtype, device=str(buffer.device))) def _n_envs_index(self, env_ids) -> int: """Return the number of environments from an ``env_ids`` argument.""" if env_ids is None: return self._num_instances if isinstance(env_ids, (list, tuple)): return len(env_ids) return env_ids.shape[0] if hasattr(env_ids, "shape") else len(env_ids) def _nft(self) -> int: """Return the number of fixed tendons (0 if none).""" return self._num_fixed_tendons def _nst(self) -> int: """Return the number of spatial tendons (0 if none).""" return self._num_spatial_tendons """ Internal simulation callbacks. """ 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 -- Ordering. """ _ordering_joint_staging_names: tuple[str, ...] = ( "_joint_pos_target_backend", "_joint_vel_target_backend", "_joint_effort_target_backend", "_applied_torque_backend", ) """Backend-order joint staging buffers managed by :meth:`_ordering_configure_backend_staging`. ``_joint_pos_target_backend`` / ``_joint_vel_target_backend`` / ``_joint_effort_target_backend`` are persistent backend-order mirrors of the corresponding user-order target buffers, kept current by the partial :meth:`set_joint_position_target_index`-style setters. ``_applied_torque_backend`` is separate, purely transient scratch: :meth:`write_data_to_sim` fully overwrites it every step with the backend-order actuator output, so it must not alias ``_joint_effort_target_backend`` (whose unselected rows a partial effort-target write relies on to still hold the persisted target, not the last pushed applied torque). """ """ Internal helpers -- Actuators. """ def _process_actuators_cfg(self) -> None: """Build actuator instances from the config and write drive properties to PhysX. Mirrors the PhysX backend's ``_process_actuators_cfg``: * For :class:`~isaaclab.actuators.ImplicitActuator`: write the configured stiffness/damping to the PhysX drive so the solver uses exactly those values. * For all explicit actuators: zero out PhysX stiffness/damping so USD-authored drive gains cannot interfere with the explicit torque path. * For all actuators: write :attr:`~isaaclab.actuators.ActuatorBase.effort_limit_sim` and :attr:`~isaaclab.actuators.ActuatorBase.velocity_limit_sim`. """ from isaaclab.actuators import ImplicitActuator self.actuators: dict[str, Any] = {} self._has_implicit_actuators = False for name, act_cfg in self.cfg.actuators.items(): joint_ids, joint_names = self.find_joints(act_cfg.joint_names_expr, as_proxy=True) if not joint_names: logger.warning("Actuator '%s': no joints matched '%s'", name, act_cfg.joint_names_expr) continue actuator_joint_ids = slice(None) if joint_names == self.joint_names else joint_ids.torch torch_joint_ids = actuator_joint_ids act_cfg_copy = act_cfg.copy() # seed the actuator with the simulation's already-correct DOF defaults # (USD-authored ``physxJoint:maxJointVelocity`` etc. parsed at scene-load). # Without these the ActuatorBase constructor falls back to ``inf`` for unset # cfg fields, and the ``write_joint_*_to_sim_index`` calls below then # overwrite the correct values with ``inf``. act = act_cfg_copy.class_type( act_cfg_copy, joint_names=joint_names, joint_ids=actuator_joint_ids, num_envs=self._num_instances, device=self._device, stiffness=self._data.joint_stiffness.torch[:, torch_joint_ids], damping=self._data.joint_damping.torch[:, torch_joint_ids], armature=self._data.joint_armature.torch[:, torch_joint_ids], friction=self._data.joint_friction_coeff.torch[:, torch_joint_ids], dynamic_friction=self._data.joint_dynamic_friction_coeff.torch[:, torch_joint_ids], viscous_friction=self._data.joint_viscous_friction_coeff.torch[:, torch_joint_ids], effort_limit=self._data.joint_effort_limits.torch[:, torch_joint_ids].clone(), velocity_limit=self._data.joint_vel_limits.torch[:, torch_joint_ids], ) self.actuators[name] = act self._joint_ids_per_actuator[name] = actuator_joint_ids # Write drive gains and limits to PhysX to match the actuator config. # Without this, PhysX retains whatever stiffness/damping was authored in the # USD file, which can produce large restoring forces when the USD gains differ # from the actuator config. if isinstance(act, ImplicitActuator): self._has_implicit_actuators = True stiffness = act.stiffness # torch (N, J) damping = act.damping # torch (N, J) else: stiffness = wp.zeros((self._num_instances, len(joint_names)), dtype=wp.float32, device=self._device) damping = wp.zeros((self._num_instances, len(joint_names)), dtype=wp.float32, device=self._device) self.write_joint_stiffness_to_sim_index(stiffness=stiffness, joint_ids=actuator_joint_ids) self.write_joint_damping_to_sim_index(damping=damping, joint_ids=actuator_joint_ids) self.write_joint_effort_limit_to_sim_index(limits=act.effort_limit_sim, joint_ids=actuator_joint_ids) self.write_joint_velocity_limit_to_sim_index(limits=act.velocity_limit_sim, joint_ids=actuator_joint_ids) def _apply_actuator_model(self) -> None: """Run the actuator model to compute joint torques from user-supplied targets. IsaacLab actuators are torch-based. The method converts Warp buffers to torch via DLPack (zero-copy on GPU), runs each actuator's :meth:`~isaaclab.actuators.ActuatorBase.compute` method, then writes the computed effort back to the private ``_computed_torque`` / ``_applied_torque`` buffers of the data container. :meth:`write_data_to_sim` then pushes ``_applied_torque`` to the ``DOF_ACTUATION_FORCE`` binding in one shot. """ from isaaclab.utils.types import ArticulationActions for name, act in self.actuators.items(): joint_ids = self._joint_ids_per_actuator[name] all_joints = isinstance(joint_ids, slice) torch_joint_ids = joint_ids # Warp -> torch (zero-copy on same device via DLPack). jp_target_full = self._data.joint_pos_target.torch jv_target_full = self._data.joint_vel_target.torch je_target_full = self._data.joint_effort_target.torch jp_target = jp_target_full if all_joints else jp_target_full[:, torch_joint_ids] jv_target = jv_target_full if all_joints else jv_target_full[:, torch_joint_ids] je_target = je_target_full if all_joints else je_target_full[:, torch_joint_ids] control_action = ArticulationActions( joint_positions=jp_target, joint_velocities=jv_target, joint_efforts=je_target, ) jp_cur_full = self._data.joint_pos.torch jv_cur_full = self._data.joint_vel.torch jp_cur = jp_cur_full if all_joints else jp_cur_full[:, torch_joint_ids] jv_cur = jv_cur_full if all_joints else jv_cur_full[:, torch_joint_ids] control_action = act.compute(control_action, jp_cur, jv_cur) if act.computed_effort is not None: ct = wp.to_torch(self._data._computed_torque) at = wp.to_torch(self._data._applied_torque) if all_joints: ct[:] = act.computed_effort at[:] = act.applied_effort else: ct[:, torch_joint_ids] = act.computed_effort at[:, torch_joint_ids] = act.applied_effort """ Internal helpers -- Debugging. """ def _validate_cfg(self) -> None: """Validate the configuration after processing. Mirrors :meth:`isaaclab_physx.assets.Articulation._validate_cfg` (raises ``ValueError`` with a per-joint message when any default joint position is outside ``[lower, upper]`` or any default joint velocity exceeds the per-joint max velocity). Reads come from :attr:`ArticulationData` accessors instead of PhysX's ``root_view.get_dof_limits`` / ``get_dof_max_velocities`` because OVPhysX's ``root_view`` is the per-tensor-type bindings dict. .. note:: Must be called only after :meth:`_create_buffers` / :meth:`_process_cfg` / :meth:`_process_actuators_cfg`, otherwise limits and defaults may not yet reflect the final values. """ # check that the default joint positions are within the limits joint_pos_limits = self._data.joint_pos_limits.torch[0] # (num_joints, 2) default_joint_pos = self._data.default_joint_pos.torch[0] # (num_joints,) out_of_range = default_joint_pos < joint_pos_limits[:, 0] out_of_range |= default_joint_pos > joint_pos_limits[:, 1] violated_indices = torch.nonzero(out_of_range, as_tuple=False).squeeze(-1) if len(violated_indices) > 0: msg = "The following joints have default positions out of the limits: \n" for idx in violated_indices: joint_name = self._data.joint_names[idx] joint_limit = joint_pos_limits[idx] joint_pos = default_joint_pos[idx] msg += f"\t- '{joint_name}': {joint_pos:.3f} not in [{joint_limit[0]:.3f}, {joint_limit[1]:.3f}]\n" raise ValueError(msg) # check that the default joint velocities are within the limits joint_max_vel = self._data.joint_vel_limits.torch[0] # (num_joints,) default_joint_vel = self._data.default_joint_vel.torch[0] # (num_joints,) out_of_range = torch.abs(default_joint_vel) > joint_max_vel violated_indices = torch.nonzero(out_of_range, as_tuple=False).squeeze(-1) if len(violated_indices) > 0: msg = "The following joints have default velocities out of the limits: \n" for idx in violated_indices: joint_name = self._data.joint_names[idx] joint_limit = [-joint_max_vel[idx], joint_max_vel[idx]] joint_vel = default_joint_vel[idx] msg += f"\t- '{joint_name}': {joint_vel:.3f} not in [{joint_limit[0]:.3f}, {joint_limit[1]:.3f}]\n" raise ValueError(msg) def _log_articulation_info(self) -> None: pass def _resolve_env_ids(self, env_ids: Sequence[int] | torch.Tensor | wp.array | None) -> wp.array | torch.Tensor: """Resolve environment indices on ``self._device``.""" if env_ids is None or (isinstance(env_ids, slice) and env_ids == slice(None)): return self._ALL_INDICES if isinstance(env_ids, ProxyArray): raise TypeError("ProxyArray is output-only; pass .warp or .torch explicitly.") if isinstance(env_ids, list): return wp.array(env_ids, dtype=wp.int32, device=self._device) if isinstance(env_ids, torch.Tensor): return env_ids.to(device=self._device) if isinstance(env_ids, wp.array) and str(env_ids.device) != self._device: env_ids = wp.clone(env_ids, device=self._device) return env_ids def _resolve_body_ids(self, body_ids: Sequence[int] | torch.Tensor | wp.array | None) -> wp.array | torch.Tensor: """Resolve body indices to a Warp signed-integer array on ``self._device``.""" if isinstance(body_ids, ProxyArray): raise TypeError("ProxyArray is output-only; pass .warp or .torch explicitly.") if body_ids is None or body_ids == slice(None): return self._ALL_BODY_INDICES if isinstance(body_ids, list): return wp.array(body_ids, dtype=wp.int32, device=self._device) if isinstance(body_ids, torch.Tensor): return body_ids.to(device=self._device) if isinstance(body_ids, wp.array) and str(body_ids.device) != self._device: body_ids = wp.clone(body_ids, device=self._device) return body_ids def _resolve_joint_ids(self, joint_ids: Sequence[int] | torch.Tensor | wp.array | None) -> wp.array | torch.Tensor: """Resolve joint indices to a Warp signed-integer array on ``self._device``.""" if isinstance(joint_ids, ProxyArray): raise TypeError("ProxyArray is output-only; pass .warp or .torch explicitly.") if joint_ids is None or joint_ids == slice(None): return self._ALL_JOINT_INDICES if isinstance(joint_ids, list): return wp.array(joint_ids, dtype=wp.int32, device=self._device) if isinstance(joint_ids, torch.Tensor): return joint_ids.to(device=self._device) if isinstance(joint_ids, wp.array) and str(joint_ids.device) != self._device: joint_ids = wp.clone(joint_ids, device=self._device) return joint_ids def _resolve_fixed_tendon_ids( self, tendon_ids: Sequence[int] | torch.Tensor | wp.array | None ) -> wp.array | torch.Tensor: """Resolve fixed-tendon indices to a Warp signed-integer array on ``self._device``.""" if isinstance(tendon_ids, ProxyArray): raise TypeError("ProxyArray is output-only; pass .warp or .torch explicitly.") if tendon_ids is None or tendon_ids == slice(None): return self._ALL_FIXED_TENDON_INDICES if isinstance(tendon_ids, list): return wp.array(tendon_ids, dtype=wp.int32, device=self._device) if isinstance(tendon_ids, torch.Tensor): return tendon_ids.to(device=self._device) if isinstance(tendon_ids, wp.array) and str(tendon_ids.device) != self._device: tendon_ids = wp.clone(tendon_ids, device=self._device) return tendon_ids def _resolve_spatial_tendon_ids( self, tendon_ids: Sequence[int] | torch.Tensor | wp.array | None ) -> wp.array | torch.Tensor: """Resolve spatial-tendon indices to a Warp signed-integer array on ``self._device``.""" if isinstance(tendon_ids, ProxyArray): raise TypeError("ProxyArray is output-only; pass .warp or .torch explicitly.") if tendon_ids is None or tendon_ids == slice(None): return self._ALL_SPATIAL_TENDON_INDICES if isinstance(tendon_ids, list): return wp.array(tendon_ids, dtype=wp.int32, device=self._device) if isinstance(tendon_ids, torch.Tensor): return tendon_ids.to(device=self._device) if isinstance(tendon_ids, wp.array) and str(tendon_ids.device) != self._device: tendon_ids = wp.clone(tendon_ids, device=self._device) return tendon_ids def _broadcast_scalar_to_2d( self, value: float | torch.Tensor | wp.array, shape: tuple[int, int] ) -> torch.Tensor | wp.array: """Broadcast a scalar :class:`float` to a ``(rows, cols)`` torch ``float32`` tensor. Tendon and joint setters accept ``float | torch.Tensor | wp.array``; the underlying ``shared_kernels.write_2d_data_to_buffer_*`` kernels only accept 2D arrays. This helper expands a Python float into a constant tensor on :attr:`_device`; tensor / warp inputs are returned as-is. Mirrors the PhysX backend's ``isinstance(value, float)`` branching, which dispatches to ``articulation_kernels.float_data_to_buffer_with_*``. OVPhysX does not have those scalar kernels, so we materialize the broadcast on the Python side. Args: value: Scalar float or 2D tensor / warp array. shape: ``(rows, cols)`` target shape used when broadcasting a scalar. Returns: A 2D :class:`torch.Tensor` on ``self._device`` if *value* was a float; otherwise *value* unchanged. """ if isinstance(value, float): return torch.full(shape, value, dtype=torch.float32, device=self._device) return value 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) 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 if isinstance(env_mask, torch.Tensor): return wp.from_torch(env_mask.to(torch.bool), dtype=wp.bool) if isinstance(env_mask, wp.array) and str(env_mask.device) != self._device: env_mask = wp.clone(env_mask, device=self._device) return env_mask def _sim_env_ids_view(self, count: int) -> wp.array: """Return a cached prefix view of the simulator-index scratch buffer.""" if count not in self._sim_env_ids_views: self._sim_env_ids_views[count] = wp.array( ptr=self._sim_env_ids.ptr, shape=(count,), dtype=wp.int32, device=self._device, copy=False, ) return self._sim_env_ids_views[count] def _resolve_body_mask(self, body_mask: wp.array | None) -> wp.array: """Resolve a body mask to a ``wp.bool`` array on ``self._device`` (Newton-style).""" if body_mask is None: return self._ALL_TRUE_BODY_MASK if isinstance(body_mask, torch.Tensor): return wp.from_torch(body_mask.to(torch.bool), dtype=wp.bool) if isinstance(body_mask, wp.array) and str(body_mask.device) != self._device: body_mask = wp.clone(body_mask, device=self._device) return body_mask def _resolve_joint_mask(self, joint_mask: wp.array | None) -> wp.array: """Resolve a joint mask to a ``wp.bool`` array on ``self._device``.""" if joint_mask is None: return self._ALL_TRUE_JOINT_MASK if isinstance(joint_mask, torch.Tensor): return wp.from_torch(joint_mask.to(torch.bool), dtype=wp.bool) if isinstance(joint_mask, wp.array) and str(joint_mask.device) != self._device: joint_mask = wp.clone(joint_mask, device=self._device) return joint_mask def _resolve_fixed_tendon_mask(self, tendon_mask: wp.array | None) -> wp.array: """Resolve a fixed-tendon mask to a ``wp.bool`` array on ``self._device``.""" if tendon_mask is None: return self._ALL_TRUE_FIXED_TENDON_MASK if isinstance(tendon_mask, torch.Tensor): return wp.from_torch(tendon_mask.to(torch.bool), dtype=wp.bool) if isinstance(tendon_mask, wp.array) and str(tendon_mask.device) != self._device: tendon_mask = wp.clone(tendon_mask, device=self._device) return tendon_mask def _resolve_spatial_tendon_mask(self, tendon_mask: wp.array | None) -> wp.array: """Resolve a spatial-tendon mask to a ``wp.bool`` array on ``self._device``.""" if tendon_mask is None: return self._ALL_TRUE_SPATIAL_TENDON_MASK if isinstance(tendon_mask, torch.Tensor): return wp.from_torch(tendon_mask.to(torch.bool), dtype=wp.bool) if isinstance(tendon_mask, wp.array) and str(tendon_mask.device) != self._device: tendon_mask = wp.clone(tendon_mask, device=self._device) return tendon_mask 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``; ``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 def _get_cpu_env_ids(self, env_ids: wp.array | torch.Tensor, sim_env_ids: wp.array | None = None) -> wp.array: """Return CPU int32 indices, using the pre-allocated pinned ``_cpu_env_ids_all`` fast path when *env_ids* matches ``_ALL_INDICES`` (PR #5329 pattern). """ if isinstance(env_ids, torch.Tensor): if env_ids.dtype == torch.int64 and sim_env_ids is None: return wp.from_torch(env_ids.to(device="cpu", dtype=torch.int32), dtype=wp.int32) env_ids = wp.from_torch(env_ids) if env_ids.ptr == self._ALL_INDICES.ptr: return self._cpu_env_ids_all if env_ids.dtype == wp.int64: if sim_env_ids is None: return wp.from_torch(wp.to_torch(env_ids).to(device="cpu", dtype=torch.int32), dtype=wp.int32) env_ids = sim_env_ids if str(env_ids.device) == "cpu": return env_ids cpu_env_ids = self._cpu_env_ids_view(env_ids.shape[0]) wp.copy(cpu_env_ids, env_ids) return cpu_env_ids def _get_sim_env_ids(self, env_ids: wp.array | torch.Tensor, sim_env_ids: wp.array | None = None) -> wp.array: """Return int32 environment indices for OVPhysX.""" if isinstance(env_ids, torch.Tensor): if env_ids.dtype == torch.int64 and sim_env_ids is None: return wp.from_torch(env_ids.to(device=self._device, dtype=torch.int32), dtype=wp.int32) env_ids = wp.from_torch(env_ids) if env_ids.dtype == wp.int64: if sim_env_ids is None: return wp.from_torch(wp.to_torch(env_ids).to(device=self._device, dtype=torch.int32), dtype=wp.int32) return sim_env_ids if str(env_ids.device) != self._device: return wp.clone(env_ids, device=self._device) return env_ids def _cpu_env_ids_view(self, count: int) -> wp.array: """Return a cached prefix of the CPU simulator-index scratch buffer.""" if count not in self._cpu_env_ids_views: self._cpu_env_ids_views[count] = wp.array( ptr=self._cpu_env_ids.ptr, shape=(count,), dtype=wp.int32, device="cpu", copy=False, ) return self._cpu_env_ids_views[count] def _push_joint_property( self, tensor_type: int, user_buffer: wp.array, backend_buffer: wp.array | TimestampedBufferWarp | None, *, cpu_buffer: wp.array | None = None, component_count: int | None = None, indices: wp.array | None = None, mask: wp.array | None = None, ) -> None: """Push a public-order joint property through backend and CPU staging.""" property_backend = self._get_backend_ordered_joint_buffer( user_buffer, backend_buffer, component_count=component_count, ) if cpu_buffer is None: cpu_buffer = self._data._stage_to_pinned_cpu(tensor_type, "write", property_backend) else: source = property_backend if source.dtype != wp.float32: source = wp.array( ptr=source.ptr, shape=cpu_buffer.shape, dtype=wp.float32, device=str(source.device), copy=False, ) wp.copy(cpu_buffer, source) if indices is not None: self._root_view.set_attribute(tensor_type, cpu_buffer, indices=indices) else: self._root_view.set_attribute(tensor_type, cpu_buffer, mask=mask) """ Deprecated methods. """ def write_root_state_to_sim( self, root_state: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated; use :meth:`write_root_link_pose_to_sim_index` and :meth:`write_root_com_velocity_to_sim_index` instead. Args: root_state: Root state [m, m, m, qw, qx, qy, qz, m/s, m/s, m/s, rad/s, rad/s, rad/s]. Shape is (len(env_ids), 13) with dtype wp.float32. env_ids: Environment indices. Defaults to None (all environments). """ warnings.warn( "The function 'write_root_state_to_sim' will be deprecated in a future release. Please" " use 'write_root_link_pose_to_sim_index' and 'write_root_com_velocity_to_sim_index' instead.", DeprecationWarning, stacklevel=2, ) self.write_root_link_pose_to_sim_index(root_pose=root_state[:, :7], env_ids=env_ids) self.write_root_com_velocity_to_sim_index(root_velocity=root_state[:, 7:], env_ids=env_ids) def write_root_com_state_to_sim( self, root_state: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated; use :meth:`write_root_com_pose_to_sim_index` and :meth:`write_root_com_velocity_to_sim_index` instead. Args: root_state: Root CoM state [m, m, m, qw, qx, qy, qz, m/s, m/s, m/s, rad/s, rad/s, rad/s]. Shape is (len(env_ids), 13) with dtype wp.float32. env_ids: Environment indices. Defaults to None (all environments). """ warnings.warn( "The function 'write_root_com_state_to_sim' will be deprecated in a future release. Please" " use 'write_root_com_pose_to_sim_index' and 'write_root_com_velocity_to_sim_index' instead.", DeprecationWarning, stacklevel=2, ) self.write_root_com_pose_to_sim_index(root_pose=root_state[:, :7], env_ids=env_ids) self.write_root_com_velocity_to_sim_index(root_velocity=root_state[:, 7:], env_ids=env_ids) def write_root_link_state_to_sim( self, root_state: torch.Tensor | wp.array, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated; use :meth:`write_root_link_pose_to_sim_index` and :meth:`write_root_link_velocity_to_sim_index` instead. Args: root_state: Root link state [m, m, m, qw, qx, qy, qz, m/s, m/s, m/s, rad/s, rad/s, rad/s]. Shape is (len(env_ids), 13) with dtype wp.float32. env_ids: Environment indices. Defaults to None (all environments). """ warnings.warn( "The function 'write_root_link_state_to_sim' will be deprecated in a future release. Please" " use 'write_root_link_pose_to_sim_index' and 'write_root_link_velocity_to_sim_index' instead.", DeprecationWarning, stacklevel=2, ) self.write_root_link_pose_to_sim_index(root_pose=root_state[:, :7], env_ids=env_ids) self.write_root_link_velocity_to_sim_index(root_velocity=root_state[:, 7:], env_ids=env_ids) def write_joint_state_to_sim( self, position: torch.Tensor | wp.array, velocity: torch.Tensor | wp.array, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated combined joint-state write; use :meth:`write_joint_position_to_sim_index` and :meth:`write_joint_velocity_to_sim_index` instead. Args: position: Joint positions [m or rad, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. velocity: Joint velocities [m/s or rad/s, depending on joint type]. Shape is (len(env_ids), len(joint_ids)) with dtype wp.float32. joint_ids: Joint indices. Defaults to None (all joints). env_ids: Environment indices. Defaults to None (all environments). """ warnings.warn( "write_joint_state_to_sim is deprecated; use write_joint_position_to_sim_index" " and write_joint_velocity_to_sim_index instead.", DeprecationWarning, stacklevel=2, ) self.write_joint_position_to_sim_index(position=position, joint_ids=joint_ids, env_ids=env_ids) self.write_joint_velocity_to_sim_index(velocity=velocity, joint_ids=joint_ids, env_ids=env_ids) def write_joint_friction_coefficient_to_sim( self, joint_friction_coeff: torch.Tensor | wp.array | float, joint_dynamic_friction_coeff: torch.Tensor | wp.array | float | None = None, joint_viscous_friction_coeff: torch.Tensor | wp.array | float | None = None, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated, same as :meth:`write_joint_friction_coefficient_to_sim_index`.""" warnings.warn( "The function 'write_joint_friction_coefficient_to_sim' will be deprecated in a future release. Please" " use 'write_joint_friction_coefficient_to_sim_index' instead.", DeprecationWarning, stacklevel=2, ) self.write_joint_friction_coefficient_to_sim_index( joint_friction_coeff=joint_friction_coeff, joint_dynamic_friction_coeff=joint_dynamic_friction_coeff, joint_viscous_friction_coeff=joint_viscous_friction_coeff, joint_ids=joint_ids, env_ids=env_ids, ) def write_joint_dynamic_friction_coefficient_to_sim( self, joint_dynamic_friction_coeff: torch.Tensor | wp.array | float, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated, same as :meth:`write_joint_dynamic_friction_coefficient_to_sim_index`.""" warnings.warn( "The function 'write_joint_dynamic_friction_coefficient_to_sim' will be deprecated in a future release. " "Please use 'write_joint_dynamic_friction_coefficient_to_sim_index' instead.", DeprecationWarning, stacklevel=2, ) self.write_joint_dynamic_friction_coefficient_to_sim_index( joint_dynamic_friction_coeff=joint_dynamic_friction_coeff, joint_ids=joint_ids, env_ids=env_ids, ) def write_joint_viscous_friction_coefficient_to_sim( self, joint_viscous_friction_coeff: torch.Tensor | wp.array | float, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: """Deprecated, same as :meth:`write_joint_viscous_friction_coefficient_to_sim_index`.""" warnings.warn( "The function 'write_joint_viscous_friction_coefficient_to_sim' will be deprecated in a future release. " "Please use 'write_joint_viscous_friction_coefficient_to_sim_index' instead.", DeprecationWarning, stacklevel=2, ) self.write_joint_viscous_friction_coefficient_to_sim_index( joint_viscous_friction_coeff=joint_viscous_friction_coeff, joint_ids=joint_ids, env_ids=env_ids, )