| from typing import List, Optional, Set, Union |
|
|
| import numpy as np |
| import pinocchio as pin |
|
|
| from decoupled_wbc.control.robot_model.supplemental_info import RobotSupplementalInfo |
|
|
|
|
| class RobotModel: |
| def __init__( |
| self, |
| urdf_path, |
| asset_path, |
| set_floating_base=False, |
| supplemental_info: Optional[RobotSupplementalInfo] = None, |
| ): |
| self.pinocchio_wrapper = pin.RobotWrapper.BuildFromURDF( |
| filename=urdf_path, |
| package_dirs=[asset_path], |
| root_joint=pin.JointModelFreeFlyer() if set_floating_base else None, |
| ) |
| self.is_floating_base_model = set_floating_base |
|
|
| self.joint_to_dof_index = {} |
| |
| |
| names = ( |
| self.pinocchio_wrapper.model.names[2:] |
| if set_floating_base |
| else self.pinocchio_wrapper.model.names[1:] |
| ) |
| for name in names: |
| j_id = self.pinocchio_wrapper.model.getJointId(name) |
| jmodel = self.pinocchio_wrapper.model.joints[j_id] |
| self.joint_to_dof_index[name] = jmodel.idx_q |
|
|
| |
| |
| |
| |
| root_nq = 7 if set_floating_base else 0 |
| self.upper_joint_limits = self.pinocchio_wrapper.model.upperPositionLimit[root_nq:].copy() |
| self.lower_joint_limits = self.pinocchio_wrapper.model.lowerPositionLimit[root_nq:].copy() |
|
|
| |
| self.supplemental_info = supplemental_info |
| if self.supplemental_info is not None: |
| |
| self._body_actuated_joint_indices = [ |
| self.dof_index(name) for name in self.supplemental_info.body_actuated_joints |
| ] |
| self._left_hand_actuated_joint_indices = [ |
| self.dof_index(name) for name in self.supplemental_info.left_hand_actuated_joints |
| ] |
| self._right_hand_actuated_joint_indices = [ |
| self.dof_index(name) for name in self.supplemental_info.right_hand_actuated_joints |
| ] |
| self._hand_actuated_joint_indices = ( |
| self._left_hand_actuated_joint_indices + self._right_hand_actuated_joint_indices |
| ) |
|
|
| |
| self._joint_group_indices = {} |
| for group_name, group_info in self.supplemental_info.joint_groups.items(): |
| indices = [] |
| |
| indices.extend([self.dof_index(name) for name in group_info["joints"]]) |
| |
| for subgroup_name in group_info["groups"]: |
| indices.extend(self.get_joint_group_indices(subgroup_name)) |
| self._joint_group_indices[group_name] = sorted(set(indices)) |
|
|
| |
| if ( |
| hasattr(self.supplemental_info, "joint_limits") |
| and self.supplemental_info.joint_limits |
| ): |
| for joint_name, limits in self.supplemental_info.joint_limits.items(): |
| if joint_name in self.joint_to_dof_index: |
| idx = self.joint_to_dof_index[joint_name] - root_nq |
| self.lower_joint_limits[idx] = limits[0] |
| self.upper_joint_limits[idx] = limits[1] |
|
|
| |
| self.default_body_pose = self.q_zero.copy() |
|
|
| |
| if self.supplemental_info is not None: |
| default_joint_q = self.supplemental_info.default_joint_q |
| for joint, joint_values in default_joint_q.items(): |
| |
| joint_mapping = self.supplemental_info.joint_name_mapping[joint] |
|
|
| |
| if isinstance(joint_mapping, str): |
| |
| if joint_mapping in self.joint_to_dof_index: |
| joint_idx = self.dof_index(joint_mapping) |
| self.default_body_pose[joint_idx] = ( |
| joint_values |
| ) |
| else: |
| |
| for side, value in joint_values.items(): |
| if side in joint_mapping and joint_mapping[side] in self.joint_to_dof_index: |
| joint_idx = self.dof_index(joint_mapping[side]) |
| self.default_body_pose[joint_idx] = value |
|
|
| |
| self.initial_body_pose = self.default_body_pose.copy() |
|
|
| @property |
| def num_dofs(self) -> int: |
| """Get the number of degrees of freedom of the robot (floating base pose + joints).""" |
| return self.pinocchio_wrapper.model.nq |
|
|
| @property |
| def q_zero(self) -> np.ndarray: |
| """Get the zero pose of the robot.""" |
| return self.pinocchio_wrapper.q0 |
|
|
| @property |
| def joint_names(self) -> List[str]: |
| """Get the names of the joints of the robot.""" |
| return list(self.joint_to_dof_index.keys()) |
|
|
| @property |
| def num_joints(self) -> int: |
| """Get the number of joints of the robot.""" |
| return len(self.joint_to_dof_index) |
|
|
| def dof_index(self, joint_name: str) -> int: |
| """ |
| Get the index in the degrees of freedom vector corresponding |
| to the single-DoF joint with name `joint_name`. |
| """ |
| if joint_name not in self.joint_to_dof_index: |
| raise ValueError( |
| f"Unknown joint name: '{joint_name}'. " |
| f"Available joints: {list(self.joint_to_dof_index.keys())}" |
| ) |
| return self.joint_to_dof_index[joint_name] |
|
|
| def get_body_actuated_joint_indices(self) -> List[int]: |
| """ |
| Get the indices of body actuated joints in the full configuration. |
| Ordering is that of the actuated joints as defined in the supplemental info. |
| Requires supplemental_info to be provided. |
| """ |
| if self.supplemental_info is None: |
| raise ValueError("supplemental_info must be provided to use this method") |
| return self._body_actuated_joint_indices |
|
|
| def get_hand_actuated_joint_indices(self, side: str = "both") -> List[int]: |
| """ |
| Get the indices of hand actuated joints in the full configuration. |
| Ordering is that of the actuated joints as defined in the supplemental info. |
| Requires supplemental_info to be provided. |
| |
| Args: |
| side: String specifying which hand to get indices for ('left', 'right', or 'both') |
| """ |
| if self.supplemental_info is None: |
| raise ValueError("supplemental_info must be provided to use this method") |
|
|
| if side.lower() == "both": |
| return self._hand_actuated_joint_indices |
| elif side.lower() == "left": |
| return self._left_hand_actuated_joint_indices |
| elif side.lower() == "right": |
| return self._right_hand_actuated_joint_indices |
| else: |
| raise ValueError("side must be 'left', 'right', or 'both'") |
|
|
| def get_joint_group_indices(self, group_names: Union[str, Set[str]]) -> List[int]: |
| """ |
| Get the indices of joints in one or more groups in the full configuration. |
| Requires supplemental_info to be provided. |
| The returned indices are sorted in ascending order, so that the joint ordering |
| of the full model is preserved. |
| |
| Args: |
| group_names: Either a single group name (str) or a set of group names (Set[str]) |
| |
| Returns: |
| List of joint indices in sorted order with no duplicates |
| """ |
| if self.supplemental_info is None: |
| raise ValueError("supplemental_info must be provided to use this method") |
|
|
| |
| if isinstance(group_names, str): |
| group_names = {group_names} |
|
|
| |
| all_indices = set() |
| for group_name in group_names: |
| if group_name not in self._joint_group_indices: |
| raise ValueError(f"Unknown joint group: {group_name}") |
| all_indices.update(self._joint_group_indices[group_name]) |
|
|
| return sorted(all_indices) |
|
|
| def cache_forward_kinematics(self, q: np.ndarray, auto_clip=True) -> None: |
| """ |
| Perform forward kinematics to update the pose of every joint and frame |
| in the Pinocchio data structures for the given configuration `q`. |
| |
| :param q: A numpy array of shape (num_dofs,) representing the robot configuration. |
| """ |
| if q.shape[0] != self.num_dofs: |
| raise ValueError(f"Expected q of length {self.num_dofs}, got {q.shape[0]} instead.") |
|
|
| |
| if auto_clip: |
| q = self.clip_configuration(q) |
|
|
| pin.framesForwardKinematics(self.pinocchio_wrapper.model, self.pinocchio_wrapper.data, q) |
|
|
| def compute_gravity_compensation_torques( |
| self, q: np.ndarray, joint_groups: Union[str, List[str], Set[str]] = None, auto_clip=True |
| ) -> np.ndarray: |
| """ |
| Compute gravity compensation torques for specified joint groups using pinocchio. |
| |
| :param q: Robot configuration (joint positions) |
| :param joint_groups: Joint groups to compensate (e.g., "arms", ["left_arm", "waist"], |
| {"left_arm", "waist"}). If None, compensates all joints |
| :param auto_clip: Whether to automatically clip joint values to limits |
| :return: Array of gravity compensation torques for all DOFs (zero for non-compensated joints) |
| """ |
| if q.shape[0] != self.num_dofs: |
| raise ValueError(f"Expected q of length {self.num_dofs}, got {q.shape[0]} instead.") |
|
|
| |
| if auto_clip: |
| q = self.clip_configuration(q) |
|
|
| try: |
| |
| self.cache_forward_kinematics(q, auto_clip=False) |
|
|
| |
| v = np.zeros(self.num_dofs) |
| a = np.zeros(self.num_dofs) |
|
|
| gravity_torques_full = pin.rnea( |
| self.pinocchio_wrapper.model, self.pinocchio_wrapper.data, q, v, a |
| ) |
|
|
| |
| if joint_groups is None: |
| return gravity_torques_full |
|
|
| |
| if isinstance(joint_groups, list): |
| joint_groups = set(joint_groups) |
|
|
| |
| try: |
| compensated_joint_indices = self.get_joint_group_indices(joint_groups) |
| except ValueError as e: |
| raise ValueError(f"Error resolving joint groups {joint_groups}: {e}") |
|
|
| |
| compensation_mask = np.zeros(self.num_dofs, dtype=bool) |
| for joint_idx in compensated_joint_indices: |
| if 0 <= joint_idx < len(compensation_mask): |
| compensation_mask[joint_idx] = True |
|
|
| |
| compensated_torques = np.zeros_like(gravity_torques_full) |
| compensated_torques[compensation_mask] = gravity_torques_full[compensation_mask] |
|
|
| return compensated_torques |
|
|
| except Exception as e: |
| raise RuntimeError(f"Error computing gravity compensation: {e}") |
|
|
| def clip_configuration(self, q: np.ndarray, margin: float = 1e-6) -> np.ndarray: |
| """ |
| Clip the configuration to stay within joint limits with a small tolerance. |
| |
| :param q: Configuration to clip |
| :param margin: Tolerance to keep away from joint limits |
| :return: Clipped configuration |
| """ |
| q_clipped = q.copy() |
|
|
| |
| root_nq = 7 if self.is_floating_base_model else 0 |
| q_clipped[root_nq:] = np.clip( |
| q[root_nq:], self.lower_joint_limits + margin, self.upper_joint_limits - margin |
| ) |
|
|
| return q_clipped |
|
|
| def frame_placement(self, frame_name: str) -> pin.SE3: |
| """ |
| Returns the SE3 transform of the specified frame in the world coordinate system. |
| Note: make sure cache_forward_kinematics() has been previously called. |
| |
| :param frame_name: Name of the frame, e.g. "link_elbow_frame", "hand_imu_frame", etc. |
| :return: A pin.SE3 object representing the pose of the frame. |
| """ |
| model = self.pinocchio_wrapper.model |
| data = self.pinocchio_wrapper.data |
|
|
| frame_id = model.getFrameId(frame_name) |
| if frame_id < 0 or frame_id >= len(model.frames): |
| valid_frames = [f.name for f in model.frames] |
| raise ValueError(f"Unknown frame '{frame_name}'. Valid frames: {valid_frames}") |
|
|
| |
| return data.oMf[frame_id].copy() |
|
|
| def get_body_actuated_joints(self, q: np.ndarray) -> np.ndarray: |
| """ |
| Get the configuration of body actuated joints from a full configuration. |
| |
| :param q: Configuration in full space |
| :return: Configuration of body actuated joints |
| """ |
| indices = self.get_body_actuated_joint_indices() |
|
|
| return q[indices] |
|
|
| def get_hand_actuated_joints(self, q: np.ndarray, side: str = "both") -> np.ndarray: |
| """ |
| Get the configuration of hand actuated joints from a full configuration. |
| |
| Args: |
| q: Configuration in full space |
| side: String specifying which hand to get joints for ('left', 'right', or 'both') |
| """ |
| indices = self.get_hand_actuated_joint_indices(side) |
| return q[indices] |
|
|
| def get_configuration_from_actuated_joints( |
| self, |
| body_actuated_joint_values: np.ndarray, |
| hand_actuated_joint_values: Optional[np.ndarray] = None, |
| left_hand_actuated_joint_values: Optional[np.ndarray] = None, |
| right_hand_actuated_joint_values: Optional[np.ndarray] = None, |
| ) -> np.ndarray: |
| """ |
| Get the full configuration from the body and hand actuated joint configurations. |
| Can specify either both hands together or left and right hands separately. |
| |
| Args: |
| body_actuated_joint_values: Configuration of body actuated joints |
| hand_actuated_joint_values: Configuration of both hands' actuated joints (optional) |
| left_hand_actuated_joint_values: Configuration of left hand actuated joints (optional) |
| right_hand_actuated_joint_values: Configuration of right hand actuated joints (optional) |
| |
| Returns: |
| Full configuration including body and hand joints |
| """ |
| q = self.pinocchio_wrapper.q0.copy() |
| q[self.get_body_actuated_joint_indices()] = body_actuated_joint_values |
|
|
| |
| if hand_actuated_joint_values is not None: |
| |
| q[self.get_hand_actuated_joint_indices("both")] = hand_actuated_joint_values |
| else: |
| |
| if left_hand_actuated_joint_values is not None: |
| q[self.get_hand_actuated_joint_indices("left")] = left_hand_actuated_joint_values |
| if right_hand_actuated_joint_values is not None: |
| q[self.get_hand_actuated_joint_indices("right")] = right_hand_actuated_joint_values |
|
|
| return q |
|
|
| def reset_forward_kinematics(self) -> None: |
| """ |
| Reset the forward kinematics to the initial configuration. |
| """ |
| self.cache_forward_kinematics(self.q_zero) |
|
|
| def get_initial_upper_body_pose(self) -> np.ndarray: |
| """ |
| Get the initial upper body pose of the robot. |
| """ |
| return self.initial_body_pose[self.get_joint_group_indices("upper_body")] |
|
|
| def get_default_body_pose(self) -> np.ndarray: |
| """ |
| Get the default body pose of the robot. |
| """ |
| return self.default_body_pose |
|
|
| def set_initial_body_pose(self, q: np.ndarray, q_idx=None) -> None: |
| """ |
| Set the initial body pose of the robot. |
| """ |
| if q_idx is None: |
| self.initial_body_pose = q |
| else: |
| self.initial_body_pose[q_idx] = q |
|
|
|
|
| class ReducedRobotModel(RobotModel): |
| """ |
| A class that creates a reduced order robot model by fixing certain joints. |
| This class maintains a mapping between the reduced state space and the full state space. |
| """ |
|
|
| def __init__( |
| self, |
| full_robot_model: RobotModel, |
| fixed_joints: List[str], |
| fixed_values: Optional[List[float]] = None, |
| ): |
| """ |
| Create a reduced order robot model by fixing specified joints. |
| |
| :param full_robot_model: The original robot model |
| :param fixed_joints: List of joint names to fix |
| :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial |
| joint positions (q0) from the full robot model. |
| """ |
| self.full_robot = full_robot_model |
| self.supplemental_info = full_robot_model.supplemental_info |
|
|
| |
| if fixed_values is None: |
| fixed_values = [] |
| for joint_name in fixed_joints: |
| full_idx = full_robot_model.dof_index(joint_name) |
| fixed_values.append(full_robot_model.pinocchio_wrapper.q0[full_idx]) |
| elif len(fixed_joints) != len(fixed_values): |
| raise ValueError("fixed_joints and fixed_values must have the same length") |
|
|
| |
| self.fixed_joints = fixed_joints |
| self.fixed_values = fixed_values |
|
|
| |
| self.reduced_to_full = [] |
| self.full_to_reduced = {} |
|
|
| |
| if full_robot_model.is_floating_base_model: |
| self.reduced_to_full.extend(range(7)) |
| for i in range(7): |
| self.full_to_reduced[i] = i |
|
|
| |
| for joint_name in full_robot_model.joint_names: |
| if joint_name not in fixed_joints: |
| full_idx = full_robot_model.dof_index(joint_name) |
| reduced_idx = len(self.reduced_to_full) |
| self.reduced_to_full.append(full_idx) |
| self.full_to_reduced[full_idx] = reduced_idx |
|
|
| |
| |
| locked_joint_ids = [] |
| for joint_name in fixed_joints: |
| joint_id = full_robot_model.pinocchio_wrapper.model.getJointId(joint_name) |
| if (full_robot_model.is_floating_base_model and joint_id > 1) or ( |
| not full_robot_model.is_floating_base_model and joint_id > 0 |
| ): |
| locked_joint_ids.append(joint_id) |
|
|
| |
| reduced_model = pin.buildReducedModel( |
| full_robot_model.pinocchio_wrapper.model, |
| locked_joint_ids, |
| full_robot_model.pinocchio_wrapper.q0, |
| ) |
|
|
| |
| self.pinocchio_wrapper = pin.RobotWrapper( |
| model=reduced_model, |
| ) |
|
|
| |
| self.joint_to_dof_index = {} |
| |
| |
| names = ( |
| self.pinocchio_wrapper.model.names[2:] |
| if self.full_robot.is_floating_base_model |
| else self.pinocchio_wrapper.model.names[1:] |
| ) |
| for name in names: |
| j_id = self.pinocchio_wrapper.model.getJointId(name) |
| jmodel = self.pinocchio_wrapper.model.joints[j_id] |
| self.joint_to_dof_index[name] = jmodel.idx_q |
|
|
| |
| root_nq = 7 if self.full_robot.is_floating_base_model else 0 |
| self.lower_joint_limits = self.pinocchio_wrapper.model.lowerPositionLimit[root_nq:].copy() |
| self.upper_joint_limits = self.pinocchio_wrapper.model.upperPositionLimit[root_nq:].copy() |
|
|
| |
| if self.supplemental_info is not None: |
| if ( |
| hasattr(self.supplemental_info, "joint_limits") |
| and self.supplemental_info.joint_limits |
| ): |
| for joint_name, limits in self.supplemental_info.joint_limits.items(): |
| if joint_name in self.joint_to_dof_index: |
| idx = self.joint_to_dof_index[joint_name] - root_nq |
| self.lower_joint_limits[idx] = limits[0] |
| self.upper_joint_limits[idx] = limits[1] |
|
|
| |
| full_body_indices = full_robot_model.get_body_actuated_joint_indices() |
| full_hand_indices = full_robot_model.get_hand_actuated_joint_indices("both") |
| full_left_hand_indices = full_robot_model.get_hand_actuated_joint_indices("left") |
| full_right_hand_indices = full_robot_model.get_hand_actuated_joint_indices("right") |
|
|
| |
| self._body_actuated_joint_indices = [] |
| for idx in full_body_indices: |
| if idx in self.full_to_reduced: |
| self._body_actuated_joint_indices.append(self.full_to_reduced[idx]) |
|
|
| self._hand_actuated_joint_indices = [] |
| for idx in full_hand_indices: |
| if idx in self.full_to_reduced: |
| self._hand_actuated_joint_indices.append(self.full_to_reduced[idx]) |
|
|
| self._left_hand_actuated_joint_indices = [] |
| for idx in full_left_hand_indices: |
| if idx in self.full_to_reduced: |
| self._left_hand_actuated_joint_indices.append(self.full_to_reduced[idx]) |
|
|
| self._right_hand_actuated_joint_indices = [] |
| for idx in full_right_hand_indices: |
| if idx in self.full_to_reduced: |
| self._right_hand_actuated_joint_indices.append(self.full_to_reduced[idx]) |
|
|
| |
| self._joint_group_indices = {} |
| for group_name in self.supplemental_info.joint_groups: |
| full_indices = full_robot_model.get_joint_group_indices(group_name) |
| reduced_indices = [] |
| for idx in full_indices: |
| if idx in self.full_to_reduced: |
| reduced_indices.append(self.full_to_reduced[idx]) |
| self._joint_group_indices[group_name] = sorted(set(reduced_indices)) |
|
|
| |
| self.default_body_pose = self.full_to_reduced_configuration( |
| full_robot_model.default_body_pose |
| ) |
|
|
| |
| self.initial_body_pose = self.full_to_reduced_configuration( |
| full_robot_model.initial_body_pose |
| ) |
|
|
| @property |
| def num_joints(self) -> int: |
| """Get the number of active joints in the reduced model.""" |
| return len(self.joint_names) |
|
|
| @property |
| def joint_names(self) -> List[str]: |
| """Get the names of the active joints in the reduced model.""" |
| return [name for name in self.full_robot.joint_names if name not in self.fixed_joints] |
|
|
| @classmethod |
| def from_fixed_groups( |
| cls, |
| full_robot_model: RobotModel, |
| fixed_group_names: List[str], |
| fixed_values: Optional[List[float]] = None, |
| ) -> "ReducedRobotModel": |
| """ |
| Create a reduced order robot model by fixing all joints in specified groups. |
| |
| :param full_robot_model: The original robot model |
| :param fixed_group_names: List of joint group names to fix |
| :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial |
| joint positions (q0) from the full robot model. |
| :return: A ReducedRobotModel instance |
| """ |
| if full_robot_model.supplemental_info is None: |
| raise ValueError("supplemental_info must be provided to use this method") |
|
|
| |
| fixed_joints = set() |
|
|
| for group_name in fixed_group_names: |
| if group_name not in full_robot_model.supplemental_info.joint_groups: |
| raise ValueError(f"Unknown joint group: {group_name}") |
|
|
| group_info = full_robot_model.supplemental_info.joint_groups[group_name] |
|
|
| |
| fixed_joints.update(group_info["joints"]) |
|
|
| |
| for subgroup_name in group_info["groups"]: |
| subgroup_joints = full_robot_model.get_joint_group_indices(subgroup_name) |
| fixed_joints.update([full_robot_model.joint_names[idx] for idx in subgroup_joints]) |
|
|
| |
| return cls(full_robot_model, list(fixed_joints), fixed_values) |
|
|
| @classmethod |
| def from_fixed_group( |
| cls, |
| full_robot_model: RobotModel, |
| fixed_group_name: str, |
| fixed_values: Optional[List[float]] = None, |
| ) -> "ReducedRobotModel": |
| """ |
| Create a reduced order robot model by fixing all joints in a specified group. |
| This is a convenience method that calls from_fixed_groups with a single group. |
| |
| :param full_robot_model: The original robot model |
| :param fixed_group_name: Name of the joint group to fix |
| :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial |
| joint positions (q0) from the full robot model. |
| :return: A ReducedRobotModel instance |
| """ |
| return cls.from_fixed_groups(full_robot_model, [fixed_group_name], fixed_values) |
|
|
| @classmethod |
| def from_active_group( |
| cls, |
| full_robot_model: RobotModel, |
| active_group_name: str, |
| fixed_values: Optional[List[float]] = None, |
| ) -> "ReducedRobotModel": |
| """ |
| Create a reduced order robot model by fixing all joints EXCEPT those in the specified group. |
| This is a convenience method that calls from_active_groups with a single group. |
| |
| :param full_robot_model: The original robot model |
| :param active_group_name: Name of the joint group to keep active (all other joints will be fixed) |
| :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial |
| joint positions (q0) from the full robot model. |
| :return: A ReducedRobotModel instance |
| """ |
| return cls.from_active_groups(full_robot_model, [active_group_name], fixed_values) |
|
|
| @classmethod |
| def from_active_groups( |
| cls, |
| full_robot_model: RobotModel, |
| active_group_names: List[str], |
| fixed_values: Optional[List[float]] = None, |
| ) -> "ReducedRobotModel": |
| """ |
| Create a reduced order robot model by fixing all joints EXCEPT those in the specified groups. |
| This is useful when you want to keep multiple groups active and fix everything else. |
| |
| :param full_robot_model: The original robot model |
| :param active_group_names: List of joint group names to keep active (all other joints will be fixed) |
| :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial |
| joint positions (q0) from the full robot model. |
| :return: A ReducedRobotModel instance |
| """ |
| if full_robot_model.supplemental_info is None: |
| raise ValueError("supplemental_info must be provided to use this method") |
|
|
| |
| active_joints = set() |
|
|
| def add_group_joints(group_name: str): |
| if group_name not in full_robot_model.supplemental_info.joint_groups: |
| raise ValueError(f"Unknown joint group: {group_name}") |
|
|
| group_info = full_robot_model.supplemental_info.joint_groups[group_name] |
|
|
| |
| if "joints" in group_info: |
| active_joints.update(group_info["joints"]) |
|
|
| |
| if "groups" in group_info: |
| for subgroup_name in group_info["groups"]: |
| add_group_joints(subgroup_name) |
|
|
| for group_name in active_group_names: |
| add_group_joints(group_name) |
|
|
| |
| all_joints = set(full_robot_model.joint_names) |
|
|
| |
| fixed_joints = list(all_joints - active_joints) |
|
|
| return cls(full_robot_model, fixed_joints, fixed_values) |
|
|
| def reduced_to_full_configuration(self, q_reduced: np.ndarray) -> np.ndarray: |
| """ |
| Convert a reduced configuration to the full configuration space. |
| |
| :param q_reduced: Configuration in reduced space |
| :return: Configuration in full space with fixed joints set to their fixed values |
| """ |
| if q_reduced.shape[0] != self.num_dofs: |
| raise ValueError( |
| f"Expected q_reduced of length {self.num_dofs}, got {q_reduced.shape[0]} instead" |
| ) |
|
|
| q_full = np.zeros(self.full_robot.num_dofs) |
|
|
| |
| for reduced_idx, full_idx in enumerate(self.reduced_to_full): |
| q_full[full_idx] = q_reduced[reduced_idx] |
|
|
| |
| for joint_name, value in zip(self.fixed_joints, self.fixed_values): |
| full_idx = self.full_robot.dof_index(joint_name) |
| q_full[full_idx] = value |
|
|
| return q_full |
|
|
| def full_to_reduced_configuration(self, q_full: np.ndarray) -> np.ndarray: |
| """ |
| Convert a full configuration to the reduced configuration space. |
| |
| :param q_full: Configuration in full space |
| :return: Configuration in reduced space |
| """ |
| if q_full.shape[0] != self.full_robot.num_dofs: |
| raise ValueError( |
| f"Expected q_full of length {self.full_robot.num_dofs}, got {q_full.shape[0]} instead" |
| ) |
|
|
| q_reduced = np.zeros(self.num_dofs) |
|
|
| |
| for reduced_idx, full_idx in enumerate(self.reduced_to_full): |
| q_reduced[reduced_idx] = q_full[full_idx] |
|
|
| return q_reduced |
|
|
| def cache_forward_kinematics(self, q_reduced: np.ndarray, auto_clip=True) -> None: |
| """ |
| Perform forward kinematics using the reduced configuration. |
| |
| :param q_reduced: Configuration in reduced space |
| """ |
| |
| q_full = self.reduced_to_full_configuration(q_reduced) |
| self.full_robot.cache_forward_kinematics(q_full, auto_clip) |
|
|
| |
| pin.framesForwardKinematics( |
| self.pinocchio_wrapper.model, self.pinocchio_wrapper.data, q_reduced |
| ) |
|
|
| def clip_configuration(self, q_reduced: np.ndarray, margin: float = 1e-6) -> np.ndarray: |
| """ |
| Clip the reduced configuration to stay within joint limits with a small tolerance. |
| |
| :param q_reduced: Configuration to clip |
| :param margin: Tolerance to keep away from joint limits |
| :return: Clipped configuration |
| """ |
| q_full = self.reduced_to_full_configuration(q_reduced) |
| q_full_clipped = self.full_robot.clip_configuration(q_full, margin) |
| return self.full_to_reduced_configuration(q_full_clipped) |
|
|
| def reset_forward_kinematics(self): |
| """ |
| Reset the forward kinematics to the initial configuration. |
| """ |
| |
| self.full_robot.reset_forward_kinematics() |
| |
| self.cache_forward_kinematics(self.q_zero) |
|
|