# 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
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from isaaclab.utils.math import (
apply_delta_pose,
combine_frame_transforms,
compute_pose_error,
matrix_from_quat,
subtract_frame_transforms,
)
if TYPE_CHECKING:
from .operational_space_cfg import OperationalSpaceControllerCfg
def _compute_task_space_forces(
inverse_task_space_mass_matrix: torch.Tensor,
accelerations: torch.Tensor,
conditioning_thresholds: tuple[float, float],
) -> torch.Tensor:
"""Solve for task forces, selectively damping poorly conditioned inertia directions."""
inverse_task_space_mass_matrix = 0.5 * (inverse_task_space_mass_matrix + inverse_task_space_mass_matrix.mT)
lower, upper = conditioning_thresholds
num_matrices = inverse_task_space_mass_matrix.shape[0]
# Positive definiteness of A - upper * ||A||_F * I certifies lambda_min(A) / lambda_max(A) > upper.
# Only matrices that fail this sufficient condition need an eigendecomposition.
shifted = inverse_task_space_mass_matrix.clone()
shifted.diagonal(dim1=-2, dim2=-1).sub_(
upper * torch.linalg.vector_norm(inverse_task_space_mass_matrix, dim=(-2, -1)).unsqueeze(-1)
)
_, info = torch.linalg.cholesky_ex(shifted)
damping_ids = torch.nonzero(info).flatten()
if damping_ids.numel() < num_matrices:
forces = torch.linalg.solve_ex(inverse_task_space_mass_matrix, accelerations)[0]
if damping_ids.numel() == 0:
return forces
inverse_task_space_mass_matrix = inverse_task_space_mass_matrix[damping_ids]
accelerations = accelerations[damping_ids]
eigenvalues, eigenvectors = torch.linalg.eigh(inverse_task_space_mass_matrix)
tiny = torch.finfo(eigenvalues.dtype).tiny
eigenvalues = eigenvalues.clamp_min(0.0)
scale = eigenvalues[:, -1:].clamp_min(tiny)
relative_eigenvalues = eigenvalues / scale
weights = ((relative_eigenvalues - lower) / (upper - lower)).clamp(0.0, 1.0)
weights = weights.square() * (3.0 - 2.0 * weights)
damping = lower * scale * (1.0 - weights)
inverse_eigenvalues = (eigenvalues + damping).clamp_min(tiny).reciprocal()
inverse_eigenvalues = torch.where(eigenvalues[:, -1:] > 0.0, inverse_eigenvalues, 0.0)
damped_forces = eigenvectors @ (inverse_eigenvalues.unsqueeze(-1) * (eigenvectors.mT @ accelerations))
if damping_ids.numel() == num_matrices:
return damped_forces
forces[damping_ids] = damped_forces
return forces
[docs]
class OperationalSpaceController:
"""Operational-space controller.
Reference:
1. `A unified approach for motion and force control of robot manipulators: The operational space formulation <http://dx.doi.org/10.1109/JRA.1987.1087068>`_
by Oussama Khatib (Stanford University)
2. `Robot Dynamics Lecture Notes <https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2017/RD_HS2017script.pdf>`_
by Marco Hutter (ETH Zurich)
"""
[docs]
def __init__(self, cfg: OperationalSpaceControllerCfg, num_envs: int, device: str):
"""Initialize operational-space controller.
Args:
cfg: The configuration for operational-space controller.
num_envs: The number of environments.
device: The device to use for computations.
Raises:
ValueError: When an invalid control command or inertia conditioning thresholds are provided.
"""
# store inputs
self.cfg = cfg
self.num_envs = num_envs
self._device = device
lower, upper = self.cfg.inertia_conditioning_thresholds
if not 0.0 < lower < upper <= 1.0:
raise ValueError("Inertia conditioning thresholds must satisfy 0 < lower < upper <= 1.")
# resolve tasks-pace target dimensions
self.target_list = list()
for command_type in self.cfg.target_types:
if command_type == "pose_rel":
self.target_list.append(6)
elif command_type == "pose_abs":
self.target_list.append(7)
elif command_type == "wrench_abs":
self.target_list.append(6)
else:
raise ValueError(f"Invalid control command: {command_type}.")
self.target_dim = sum(self.target_list)
# create buffers
# -- selection matrices, which might be defined in the task reference frame different from the root frame
self._selection_matrix_motion_task = torch.diag_embed(
torch.tensor(self.cfg.motion_control_axes_task, dtype=torch.float, device=self._device)
.unsqueeze(0)
.repeat(self.num_envs, 1)
)
self._selection_matrix_force_task = torch.diag_embed(
torch.tensor(self.cfg.contact_wrench_control_axes_task, dtype=torch.float, device=self._device)
.unsqueeze(0)
.repeat(self.num_envs, 1)
)
# -- selection matrices in root frame
self._selection_matrix_motion_b = torch.zeros_like(self._selection_matrix_motion_task)
self._selection_matrix_force_b = torch.zeros_like(self._selection_matrix_force_task)
# -- commands
self._task_space_target_task = torch.zeros(self.num_envs, self.target_dim, device=self._device)
# -- Placeholders for motion/force control
self.desired_ee_pose_task = None
self.desired_ee_pose_b = None
self.desired_ee_wrench_task = None
self.desired_ee_wrench_b = None
# -- motion control gains
self._motion_p_gains_task = torch.diag_embed(
torch.ones(self.num_envs, 6, device=self._device)
* torch.tensor(self.cfg.motion_stiffness_task, dtype=torch.float, device=self._device)
)
# -- -- zero out the axes that are not motion controlled, as keeping them non-zero will cause other axes
# -- -- to act due to coupling
self._motion_p_gains_task[:] = self._selection_matrix_motion_task @ self._motion_p_gains_task[:]
self._motion_d_gains_task = torch.diag_embed(
2
* torch.diagonal(self._motion_p_gains_task, dim1=-2, dim2=-1).sqrt()
* torch.as_tensor(self.cfg.motion_damping_ratio_task, dtype=torch.float, device=self._device).reshape(1, -1)
)
# -- -- motion control gains in root frame
self._motion_p_gains_b = torch.zeros_like(self._motion_p_gains_task)
self._motion_d_gains_b = torch.zeros_like(self._motion_d_gains_task)
# -- force control gains
if self.cfg.contact_wrench_stiffness_task is not None:
self._contact_wrench_p_gains_task = torch.diag_embed(
torch.ones(self.num_envs, 6, device=self._device)
* torch.tensor(self.cfg.contact_wrench_stiffness_task, dtype=torch.float, device=self._device)
)
self._contact_wrench_p_gains_task[:] = (
self._selection_matrix_force_task @ self._contact_wrench_p_gains_task[:]
)
# -- -- force control gains in root frame
self._contact_wrench_p_gains_b = torch.zeros_like(self._contact_wrench_p_gains_task)
else:
self._contact_wrench_p_gains_task = None
self._contact_wrench_p_gains_b = None
# -- position gain limits
self._motion_p_gains_limits = torch.zeros(self.num_envs, 6, 2, device=self._device)
self._motion_p_gains_limits[..., 0], self._motion_p_gains_limits[..., 1] = (
self.cfg.motion_stiffness_limits_task[0],
self.cfg.motion_stiffness_limits_task[1],
)
# -- damping ratio limits
self._motion_damping_ratio_limits = torch.zeros_like(self._motion_p_gains_limits)
self._motion_damping_ratio_limits[..., 0], self._motion_damping_ratio_limits[..., 1] = (
self.cfg.motion_damping_ratio_limits_task[0],
self.cfg.motion_damping_ratio_limits_task[1],
)
# -- end-effector contact wrench
self._ee_contact_wrench_b = torch.zeros(self.num_envs, 6, device=self._device)
# -- buffers for null-space control gains
self._nullspace_p_gain = torch.tensor(self.cfg.nullspace_stiffness, dtype=torch.float, device=self._device)
self._nullspace_d_gain = (
2
* torch.sqrt(self._nullspace_p_gain)
* torch.tensor(self.cfg.nullspace_damping_ratio, dtype=torch.float, device=self._device)
)
"""
Properties.
"""
@property
def action_dim(self) -> int:
"""Dimension of the action space of controller."""
# impedance mode
if self.cfg.impedance_mode == "fixed":
# task-space targets
return self.target_dim
elif self.cfg.impedance_mode == "variable_kp":
# task-space targets + stiffness
return self.target_dim + 6
elif self.cfg.impedance_mode == "variable":
# task-space targets + stiffness + damping
return self.target_dim + 6 + 6
else:
raise ValueError(f"Invalid impedance mode: {self.cfg.impedance_mode}.")
"""
Operations.
"""
[docs]
def reset(self):
"""Reset the internals."""
self.desired_ee_pose_b = None
self.desired_ee_pose_task = None
self.desired_ee_wrench_b = None
self.desired_ee_wrench_task = None
[docs]
def set_command(
self,
command: torch.Tensor,
current_ee_pose_b: torch.Tensor | None = None,
current_task_frame_pose_b: torch.Tensor | None = None,
):
"""Set the task-space targets and impedance parameters.
Args:
command (torch.Tensor): A concatenated tensor of shape (``num_envs``, ``action_dim``) containing task-space
targets (i.e., pose/wrench) and impedance parameters.
current_ee_pose_b (torch.Tensor, optional): Current end-effector pose, in root frame, of shape
(``num_envs``, 7), containing position and quaternion ``(x, y, z, w)``. Required for relative
commands and used as the orientation fallback for degenerate absolute pose quaternions.
Defaults to None.
current_task_frame_pose_b: Current pose of the task frame, in root frame, in which the targets and the
(motion/wrench) control axes are defined. It is a tensor of shape (``num_envs``, 7),
containing position and the quaternion ``(x, y, z, w)``. Defaults to None.
Format:
Task-space targets, ordered according to 'command_types':
Absolute pose: shape (``num_envs``, 7), containing position and quaternion ``(x, y, z, w)``.
The quaternion is normalized before use. Entries that cannot be normalized (e.g. all zeros or
non-finite) fall back to the current end-effector orientation when ``current_ee_pose_b`` is
provided, and to the identity orientation otherwise.
Relative pose: shape (``num_envs``, 6), containing delta position and rotation in axis-angle form.
Absolute wrench: shape (``num_envs``, 6), containing force and torque.
Impedance parameters: stiffness for ``variable_kp``, or stiffness, followed by damping ratio for
``variable``:
Stiffness: shape (``num_envs``, 6)
Damping ratio: shape (``num_envs``, 6)
Raises:
ValueError: When the command dimensions are invalid.
ValueError: When an invalid impedance mode is provided.
ValueError: When the current end-effector pose is not provided for the ``pose_rel`` command.
ValueError: When an invalid control command is provided.
"""
# Check the input dimensions
if command.shape != (self.num_envs, self.action_dim):
raise ValueError(
f"Invalid command shape '{command.shape}'. Expected: '{(self.num_envs, self.action_dim)}'."
)
# Resolve the impedance parameters
if self.cfg.impedance_mode == "fixed":
# task space targets (i.e., pose/wrench)
self._task_space_target_task[:] = command
elif self.cfg.impedance_mode == "variable_kp":
# split input command
task_space_command, stiffness = torch.split(command, [self.target_dim, 6], dim=-1)
# format command
stiffness = stiffness.clip_(
min=self._motion_p_gains_limits[..., 0], max=self._motion_p_gains_limits[..., 1]
)
# task space targets + stiffness
self._task_space_target_task[:] = task_space_command.squeeze(dim=-1)
self._motion_p_gains_task[:] = torch.diag_embed(stiffness)
self._motion_p_gains_task[:] = self._selection_matrix_motion_task @ self._motion_p_gains_task[:]
self._motion_d_gains_task = torch.diag_embed(
2
* torch.diagonal(self._motion_p_gains_task, dim1=-2, dim2=-1).sqrt()
* torch.as_tensor(self.cfg.motion_damping_ratio_task, dtype=torch.float, device=self._device).reshape(
1, -1
)
)
elif self.cfg.impedance_mode == "variable":
# split input command
task_space_command, stiffness, damping_ratio = torch.split(command, [self.target_dim, 6, 6], dim=-1)
# format command
stiffness = stiffness.clip_(
min=self._motion_p_gains_limits[..., 0], max=self._motion_p_gains_limits[..., 1]
)
damping_ratio = damping_ratio.clip_(
min=self._motion_damping_ratio_limits[..., 0], max=self._motion_damping_ratio_limits[..., 1]
)
# task space targets + stiffness + damping
self._task_space_target_task[:] = task_space_command
self._motion_p_gains_task[:] = torch.diag_embed(stiffness)
self._motion_p_gains_task[:] = self._selection_matrix_motion_task @ self._motion_p_gains_task[:]
self._motion_d_gains_task[:] = torch.diag_embed(
2 * torch.diagonal(self._motion_p_gains_task, dim1=-2, dim2=-1).sqrt() * damping_ratio
)
else:
raise ValueError(f"Invalid impedance mode: {self.cfg.impedance_mode}.")
if current_task_frame_pose_b is None:
# xyzw format: identity quat is [0, 0, 0, 1]
current_task_frame_pose_b = torch.tensor(
[[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]] * self.num_envs, device=self._device
)
# Resolve the target commands
target_groups = torch.split(self._task_space_target_task, self.target_list, dim=1)
for command_type, target in zip(self.cfg.target_types, target_groups):
if command_type == "pose_rel":
# check input is provided
if current_ee_pose_b is None:
raise ValueError("Current pose is required for 'pose_rel' command.")
# Transform the current pose from base/root frame to task frame
current_ee_pos_task, current_ee_rot_task = subtract_frame_transforms(
current_task_frame_pose_b[:, :3],
current_task_frame_pose_b[:, 3:],
current_ee_pose_b[:, :3],
current_ee_pose_b[:, 3:],
)
# compute targets in task frame
desired_ee_pos_task, desired_ee_rot_task = apply_delta_pose(
current_ee_pos_task, current_ee_rot_task, target
)
self.desired_ee_pose_task = torch.cat([desired_ee_pos_task, desired_ee_rot_task], dim=-1)
elif command_type == "pose_abs":
# normalize the target orientation so that unnormalized policy outputs do not scale the
# orientation error; degenerate quaternions fall back to the current end-effector orientation
desired_ee_pose_task = target.clone()
desired_ee_quat_task = desired_ee_pose_task[:, 3:7]
normalized_quat = desired_ee_quat_task / torch.linalg.norm(desired_ee_quat_task, dim=-1, keepdim=True)
is_valid = torch.isfinite(normalized_quat).all(dim=-1, keepdim=True)
if current_ee_pose_b is not None:
_, fallback_quat = subtract_frame_transforms(
current_task_frame_pose_b[:, :3],
current_task_frame_pose_b[:, 3:],
current_ee_pose_b[:, :3],
current_ee_pose_b[:, 3:],
)
else:
fallback_quat = current_task_frame_pose_b.new_tensor([0.0, 0.0, 0.0, 1.0]).expand(self.num_envs, 4)
desired_ee_pose_task[:, 3:7] = torch.where(is_valid, normalized_quat, fallback_quat)
self.desired_ee_pose_task = desired_ee_pose_task
elif command_type == "wrench_abs":
# compute targets
self.desired_ee_wrench_task = target.clone()
else:
raise ValueError(f"Invalid control command: {command_type}.")
# Rotation of task frame wrt root frame, converts a coordinate from task frame to root frame.
R_task_b = matrix_from_quat(current_task_frame_pose_b[:, 3:])
# Rotation of root frame wrt task frame, converts a coordinate from root frame to task frame.
R_b_task = R_task_b.mT
# Transform motion control stiffness gains from task frame to root frame
self._motion_p_gains_b[:, 0:3, 0:3] = R_task_b @ self._motion_p_gains_task[:, 0:3, 0:3] @ R_b_task
self._motion_p_gains_b[:, 3:6, 3:6] = R_task_b @ self._motion_p_gains_task[:, 3:6, 3:6] @ R_b_task
# Transform motion control damping gains from task frame to root frame
self._motion_d_gains_b[:, 0:3, 0:3] = R_task_b @ self._motion_d_gains_task[:, 0:3, 0:3] @ R_b_task
self._motion_d_gains_b[:, 3:6, 3:6] = R_task_b @ self._motion_d_gains_task[:, 3:6, 3:6] @ R_b_task
# Transform contact wrench gains from task frame to root frame (if applicable)
if self._contact_wrench_p_gains_task is not None and self._contact_wrench_p_gains_b is not None:
self._contact_wrench_p_gains_b[:, 0:3, 0:3] = (
R_task_b @ self._contact_wrench_p_gains_task[:, 0:3, 0:3] @ R_b_task
)
self._contact_wrench_p_gains_b[:, 3:6, 3:6] = (
R_task_b @ self._contact_wrench_p_gains_task[:, 3:6, 3:6] @ R_b_task
)
# Transform selection matrices from target frame to base frame
self._selection_matrix_motion_b[:, 0:3, 0:3] = (
R_task_b @ self._selection_matrix_motion_task[:, 0:3, 0:3] @ R_b_task
)
self._selection_matrix_motion_b[:, 3:6, 3:6] = (
R_task_b @ self._selection_matrix_motion_task[:, 3:6, 3:6] @ R_b_task
)
self._selection_matrix_force_b[:, 0:3, 0:3] = (
R_task_b @ self._selection_matrix_force_task[:, 0:3, 0:3] @ R_b_task
)
self._selection_matrix_force_b[:, 3:6, 3:6] = (
R_task_b @ self._selection_matrix_force_task[:, 3:6, 3:6] @ R_b_task
)
# Transform desired pose from task frame to root frame
if self.desired_ee_pose_task is not None:
self.desired_ee_pose_b = torch.zeros_like(self.desired_ee_pose_task)
self.desired_ee_pose_b[:, :3], self.desired_ee_pose_b[:, 3:] = combine_frame_transforms(
current_task_frame_pose_b[:, :3],
current_task_frame_pose_b[:, 3:],
self.desired_ee_pose_task[:, :3],
self.desired_ee_pose_task[:, 3:],
)
# Transform desired wrenches to root frame
if self.desired_ee_wrench_task is not None:
self.desired_ee_wrench_b = torch.zeros_like(self.desired_ee_wrench_task)
self.desired_ee_wrench_b[:, :3] = (R_task_b @ self.desired_ee_wrench_task[:, :3].unsqueeze(-1)).squeeze(-1)
self.desired_ee_wrench_b[:, 3:] = (R_task_b @ self.desired_ee_wrench_task[:, 3:].unsqueeze(-1)).squeeze(
-1
) + torch.cross(current_task_frame_pose_b[:, :3], self.desired_ee_wrench_b[:, :3], dim=-1)
[docs]
def compute(
self,
jacobian_b: torch.Tensor,
current_ee_pose_b: torch.Tensor | None = None,
current_ee_vel_b: torch.Tensor | None = None,
current_ee_force_b: torch.Tensor | None = None,
mass_matrix: torch.Tensor | None = None,
gravity: torch.Tensor | None = None,
current_joint_pos: torch.Tensor | None = None,
current_joint_vel: torch.Tensor | None = None,
nullspace_joint_pos_target: torch.Tensor | None = None,
) -> torch.Tensor:
"""Performs inference with the controller.
Args:
jacobian_b: The Jacobian matrix of the end-effector in root frame. It is a tensor of shape
(``num_envs``, 6, ``num_DoF``).
current_ee_pose_b: The current end-effector pose in root frame. It is a tensor of shape
(``num_envs``, 7), which contains the position and quaternion ``(x, y, z, w)``. Defaults to ``None``.
current_ee_vel_b: The current end-effector velocity in root frame. It is a tensor of shape
(``num_envs``, 6), which contains the linear and angular velocities. Defaults to None.
current_ee_force_b: The current external force on the end-effector in root frame. It is a tensor of
shape (``num_envs``, 3), which contains the linear force. Defaults to ``None``.
mass_matrix: The joint-space mass/inertia matrix. It is a tensor of shape (``num_envs``, ``num_DoF``,
``num_DoF``). Defaults to ``None``.
gravity: The joint-space gravity vector. It is a tensor of shape (``num_envs``, ``num_DoF``). Defaults
to ``None``.
current_joint_pos: The current joint positions. It is a tensor of shape (``num_envs``, ``num_DoF``).
Defaults to ``None``.
current_joint_vel: The current joint velocities. It is a tensor of shape (``num_envs``, ``num_DoF``).
Defaults to ``None``.
nullspace_joint_pos_target: The target joint positions the null space controller is trying to enforce, when
possible. It is a tensor of shape (``num_envs``, ``num_DoF``).
Raises:
ValueError: When motion-control is enabled but the current end-effector pose or velocity is not provided.
ValueError: When inertial dynamics decoupling is enabled but the mass matrix is not provided.
ValueError: When the current end-effector pose is not provided for the ``pose_rel`` command.
ValueError: When closed-loop force control is enabled but the current end-effector force is not provided.
ValueError: When gravity compensation is enabled but the gravity vector is not provided.
ValueError: When null-space control is enabled but the system is not redundant.
ValueError: When dynamically consistent null-space control is enabled without motion targets or a
mass matrix.
ValueError: When null-space control is enabled but the current joint positions and velocities are not
provided.
ValueError: When target joint positions are provided for null-space control but their dimensions do not
match the current joint positions.
ValueError: When an invalid null-space control method is provided.
Returns:
Tensor: The joint efforts computed by the controller. It is a tensor of shape (``num_envs``, ``num_DoF``).
"""
# deduce number of DoF
num_DoF = jacobian_b.shape[2]
# create joint effort vector
joint_efforts = torch.zeros(self.num_envs, num_DoF, device=self._device)
# Compute posture acceleration before motion control so both can share the task-inertia solve.
if self.cfg.nullspace_control != "none":
if num_DoF <= 6:
raise ValueError("Null-space control is only applicable for redundant manipulators.")
if self.cfg.nullspace_control != "position":
raise ValueError(f"Invalid null-space control method: {self.cfg.nullspace_control}.")
if current_joint_pos is None or current_joint_vel is None:
raise ValueError("Current joint positions and velocities are required for null-space control.")
if nullspace_joint_pos_target is None:
nullspace_joint_pos_target = torch.zeros_like(current_joint_pos)
elif nullspace_joint_pos_target.shape != current_joint_pos.shape:
raise ValueError(
f"The target nullspace joint positions shape '{nullspace_joint_pos_target.shape}' does not"
f"match the current joint positions shape '{current_joint_pos.shape}'."
)
joint_acc_nullspace = (
self._nullspace_p_gain * (nullspace_joint_pos_target - current_joint_pos)
- self._nullspace_d_gain * current_joint_vel
).unsqueeze(-1)
if self.cfg.inertial_dynamics_decoupling and not self.cfg.partial_inertial_dynamics_decoupling:
if self.desired_ee_pose_b is None or mass_matrix is None:
raise ValueError(
"Motion targets and a mass matrix are required for dynamically consistent control."
)
# compute joint efforts for motion-control
if self.desired_ee_pose_b is not None:
# check input is provided
if current_ee_pose_b is None or current_ee_vel_b is None:
raise ValueError("Current end-effector pose and velocity are required for motion control.")
# -- end-effector tracking error
pose_error_b = torch.cat(
compute_pose_error(
current_ee_pose_b[:, :3],
current_ee_pose_b[:, 3:],
self.desired_ee_pose_b[:, :3],
self.desired_ee_pose_b[:, 3:],
rot_error_type="axis_angle",
),
dim=-1,
)
velocity_error_b = -current_ee_vel_b # zero target velocity. The target is assumed to be stationary.
# -- desired end-effector acceleration (spring-damper system)
des_ee_acc_b = self._motion_p_gains_b @ pose_error_b.unsqueeze(
-1
) + self._motion_d_gains_b @ velocity_error_b.unsqueeze(-1)
# -- Inertial dynamics decoupling
if self.cfg.inertial_dynamics_decoupling:
# check input is provided
if mass_matrix is None:
raise ValueError("Mass matrix is required for inertial decoupling.")
# Keep inertia products and their cancellation in double precision near weak task directions.
jacobian_b_d = jacobian_b.double()
mass_matrix_d = mass_matrix.double()
inverse_task_mass = jacobian_b_d @ torch.linalg.solve(mass_matrix_d, jacobian_b_d.mT)
accelerations = des_ee_acc_b.double()
if self.cfg.partial_inertial_dynamics_decoupling:
# Filter both uncoupled blocks in one batch.
blocks = torch.stack((inverse_task_mass[:, :3, :3], inverse_task_mass[:, 3:, 3:]), dim=1)
os_command_forces_b = _compute_task_space_forces(
blocks.flatten(0, 1), accelerations.reshape(-1, 3, 1), self.cfg.inertia_conditioning_thresholds
).reshape(self.num_envs, 6, 1)
else:
if self.cfg.nullspace_control == "position":
joint_acc_nullspace = joint_acc_nullspace.double()
accelerations = torch.cat((accelerations, jacobian_b_d @ joint_acc_nullspace), dim=-1)
task_forces_b = _compute_task_space_forces(
inverse_task_mass, accelerations, self.cfg.inertia_conditioning_thresholds
)
os_command_forces_b = task_forces_b[:, :, :1]
else:
# Task-space impedance control: command forces = \ddot(x_des).
# Please note that the definition of task-space impedance control varies in literature.
# This implementation ignores the inertial term. For inertial decoupling,
# use inertial_dynamics_decoupling=True.
os_command_forces_b = des_ee_acc_b
# -- joint-space commands
joint_efforts += (
(jacobian_b.mT @ self._selection_matrix_motion_b).to(os_command_forces_b.dtype) @ os_command_forces_b
).squeeze(-1)
# compute joint efforts for contact wrench/force control
if self.desired_ee_wrench_b is not None:
# -- task-space contact wrench
if self.cfg.contact_wrench_stiffness_task is not None:
# check input is provided
if current_ee_force_b is None:
raise ValueError("Current end-effector force is required for closed-loop force control.")
# We can only measure the force component at the contact, so only apply the feedback for only the force
# component, keep the control of moment components open loop
self._ee_contact_wrench_b[:, 0:3] = current_ee_force_b
self._ee_contact_wrench_b[:, 3:6] = self.desired_ee_wrench_b[:, 3:6]
# closed-loop control with feedforward term
os_contact_wrench_command_b = self.desired_ee_wrench_b.unsqueeze(
-1
) + self._contact_wrench_p_gains_b @ (self.desired_ee_wrench_b - self._ee_contact_wrench_b).unsqueeze(
-1
)
else:
# open-loop control
os_contact_wrench_command_b = self.desired_ee_wrench_b.unsqueeze(-1)
# -- joint-space commands
joint_efforts += (jacobian_b.mT @ self._selection_matrix_force_b @ os_contact_wrench_command_b).squeeze(-1)
# add gravity compensation (bias correction)
if self.cfg.gravity_compensation:
# check input is provided
if gravity is None:
raise ValueError("Gravity vector is required for gravity compensation.")
# add gravity compensation
joint_efforts += gravity
if self.cfg.nullspace_control == "position":
if self.cfg.inertial_dynamics_decoupling and not self.cfg.partial_inertial_dynamics_decoupling:
# (I - J^T Lambda J M^-1) M ddq = M ddq - J^T Lambda J ddq.
# The second solved force shares the task damping, retaining decoupling in undamped directions.
tau_null = mass_matrix_d @ joint_acc_nullspace - jacobian_b_d.mT @ task_forces_b[:, :, 1:]
else:
# Moore-Penrose projection without full inertial decoupling.
nullspace_jacobian_transpose = (
torch.eye(n=num_DoF, device=self._device) - jacobian_b.mT @ torch.pinverse(jacobian_b).mT
)
tau_null = mass_matrix @ joint_acc_nullspace if mass_matrix is not None else joint_acc_nullspace
tau_null = nullspace_jacobian_transpose @ tau_null
joint_efforts += tau_null.squeeze(-1)
return joint_efforts