Isaac Sim 4.0 support, Kinematics API doc, Windows support
This commit is contained in:
@@ -10,34 +10,75 @@
|
||||
#
|
||||
|
||||
"""
|
||||
This module contains code for cuda accelerated kinematics.
|
||||
This module contains GPU accelerated kinematics leveraging CUDA. Kinematics computations enable
|
||||
mapping from a robot's joint configuration to the pose of the robot's links in Cartesian space
|
||||
(with reference to the robot's base link). In cuRobo, robot's geometry is approximated with spheres
|
||||
and their positions are also computed as part of kinematics. This mapping is differentiable,
|
||||
enabling their use in optimization problems and as part of neural networks.
|
||||
|
||||
|
||||
.. figure:: ../images/robot_representation.png
|
||||
:width: 400px
|
||||
:align: center
|
||||
|
||||
Robot representation in cuRobo is shown for the Franka Panda robot.
|
||||
|
||||
|
||||
|
||||
Kinematics in CuRobo currently supports single axis actuated joints, where the joint can be actuated
|
||||
as prismatic or revolute joints. Continuous joints are approximated to revolute joints with limits
|
||||
at [-6, +6] radians. Mimic joints are not supported, so convert mimic joints to independent joints.
|
||||
|
||||
CuRobo loads a robot's kinematic tree from :class:`types.KinematicsTensorConfig`. This config is
|
||||
generated using :class:`cuda_robot_generator.CudaRobotGenerator`. A parser base class
|
||||
:class:`kinematics_parser.KinematicsParer` is provided to help with parsing kinematics from
|
||||
standard formats. Kinematics parsing from URDF is implemented in
|
||||
:class:`urdf_kinematics_parser.UrdfKinematicsParser`. An experimental USD kinematics parser is
|
||||
provided in :class:`usd_kinematics_parser.UsdKinematicsParser`, which is missing an additional
|
||||
transform between the joint origin and link origin, so this might not work for all robots.
|
||||
CuRobo loads a robot's kinematic tree from :class:`~types.KinematicsTensorConfig`. This config is
|
||||
generated using :class:`~cuda_robot_generator.CudaRobotGenerator`. A parser base class
|
||||
:class:`~kinematics_parser.KinematicsParser` is provided to help with parsing kinematics from
|
||||
standard formats. Kinematics parsing from URDF is implemented in
|
||||
:class:`~urdf_kinematics_parser.UrdfKinematicsParser`. An experimental USD kinematics parser is
|
||||
provided in :class:`~usd_kinematics_parser.UsdKinematicsParser`, which is missing an additional
|
||||
transform between the joint origin and link origin, so this might not work for all robots. An
|
||||
example workflow for setting up a robot from URDF is shown below:
|
||||
|
||||
In addition to parsing data from a kinematics file (urdf, usd), CuRobo also needs a sphere
|
||||
representation of the robot that approximates the volume of the robot's links with spheres.
|
||||
Several other parameters are also needed to represent kinematics in CuRobo. A tutorial on setting up a
|
||||
robot is provided in :ref:`tut_robot_configuration`.
|
||||
.. graphviz::
|
||||
|
||||
Once a robot configuration file is setup, you can pass this to
|
||||
:class:`cuda_robot_model.CudaRobotModelConfig` to generate an instance of kinematics configuraiton.
|
||||
:class:`cuda_robot_model.CudaRobotModel` takes this configuration and provides access to kinematics
|
||||
digraph {
|
||||
rankdir=LR;
|
||||
bgcolor="#808080";
|
||||
edge [color = "#FFFFFF"; fontsize=10];
|
||||
node [shape="box", style="rounded, filled", fontsize=12, color="#76b900", fontcolor="#FFFFFF"];
|
||||
"CudaRobotGenerator" [color="#FFFFFF", fontcolor="#000000"]
|
||||
"UrdfKinematicsParser" [fillcolor="#FFFFFF", fontcolor="#000000", style="box, filled", color="#000000"]
|
||||
"CudaRobotGenerator" [color="#FFFFFF", fontcolor="#000000"]
|
||||
"URDF" [fillcolor="#FFFFFF", fontcolor="#000000", style="box, filled, dashed", color="#000000"]
|
||||
"XRDF" [fillcolor="#FFFFFF", fontcolor="#000000", style="box, filled, dashed", color="#000000"]
|
||||
"cuRobo YML" [fillcolor="#FFFFFF", fontcolor="#000000", style="box, filled", color="#000000"]
|
||||
|
||||
"CudaRobotGeneratorConfig" -> "CudaRobotGenerator";
|
||||
"CudaRobotGenerator" -> "UrdfKinematicsParser" [dir="both"];
|
||||
"CudaRobotGenerator" -> "CudaRobotModelConfig";
|
||||
"URDF" -> "cuRobo YML";
|
||||
"XRDF" -> "cuRobo YML" [style="dashed",label="Optional", fontcolor="#FFFFFF"];
|
||||
"cuRobo YML" -> "CudaRobotGeneratorConfig";
|
||||
|
||||
}
|
||||
|
||||
|
||||
In addition to parsing data from a kinematics file (urdf, usd), CuRobo also needs a sphere
|
||||
representation of the robot that approximates the volume of the robot's links with spheres.
|
||||
Several other parameters are also needed to represent kinematics in CuRobo. A tutorial on setting up a
|
||||
robot is provided in :ref:`tut_robot_configuration`. cuRobo also supports using
|
||||
`XRDF <https://nvidia-isaac-ros.github.io/concepts/manipulation/xrdf.html>`_ for representing
|
||||
the additional parameters of the robot that are not available in URDF.
|
||||
|
||||
Once a robot configuration file is setup, you can pass this to
|
||||
:class:`~cuda_robot_model.CudaRobotModelConfig` to generate an instance of kinematics configuraiton.
|
||||
:class:`~cuda_robot_model.CudaRobotModel` takes this configuration and provides access to kinematics
|
||||
computations.
|
||||
|
||||
.. note::
|
||||
:class:`cuda_robot_model.CudaRobotModel` creates memory tensors that are used by CUDA kernels
|
||||
while :class:`cuda_robot_model.CudaRobotModelConfig` contains only the robot kinematics
|
||||
configuration. To reduce memory overhead, you can pass one instance of
|
||||
:class:`cuda_robot_model.CudaRobotModelConfig` to many instances of
|
||||
:class:`cuda_robot_model.CudaRobotModel`.
|
||||
:class:`~cuda_robot_model.CudaRobotModel` creates memory tensors that are used by CUDA kernels
|
||||
while :class:`~cuda_robot_model.CudaRobotModelConfig` contains only the robot kinematics
|
||||
configuration. To reduce memory overhead, you can pass one instance of
|
||||
:class:`~cuda_robot_model.CudaRobotModelConfig` to many instances of
|
||||
:class:`~cuda_robot_model.CudaRobotModel`.
|
||||
|
||||
"""
|
||||
|
||||
@@ -8,16 +8,23 @@
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
"""
|
||||
Generates a Tensor representation of kinematics for use in
|
||||
:class:`~curobo.cuda_robot_model.CudaRobotModel`. This module reads the robot from a
|
||||
:class:`~curobo.cuda_robot_model.kinematics_parser.KinematicsParser` and
|
||||
generates the necessary tensors for kinematics computation.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Standard Library
|
||||
import copy
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
# Third Party
|
||||
import importlib_resources
|
||||
import torch
|
||||
import torch.autograd.profiler as profiler
|
||||
|
||||
@@ -45,13 +52,13 @@ try:
|
||||
except ImportError:
|
||||
log_info(
|
||||
"USDParser failed to import, install curobo with pip install .[usd] "
|
||||
+ "or pip install usd-core, NOTE: Do not install this if using with ISAAC SIM."
|
||||
+ "or pip install usd-core, NOTE: Do not install this if using with Isaac Sim."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CudaRobotGeneratorConfig:
|
||||
"""Create Cuda Robot Model Configuration."""
|
||||
"""Robot representation generator configuration, loads from a dictionary."""
|
||||
|
||||
#: Name of base link for kinematic tree.
|
||||
base_link: str
|
||||
@@ -69,7 +76,7 @@ class CudaRobotGeneratorConfig:
|
||||
collision_link_names: Optional[List[str]] = None
|
||||
|
||||
#: Collision spheres that fill the volume occupied by the links of the robot.
|
||||
#: Collision spheres can be generated for robot using https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/advanced_tutorials/tutorial_motion_generation_robot_description_editor.html#collision-spheres
|
||||
#: Collision spheres can be generated for robot using `Isaac Sim Robot Description Editor <https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/advanced_tutorials/tutorial_motion_generation_robot_description_editor.html#collision-spheres>`_.
|
||||
collision_spheres: Union[None, str, Dict[str, Any]] = None
|
||||
|
||||
#: Radius buffer to add to collision spheres as padding.
|
||||
@@ -79,13 +86,17 @@ class CudaRobotGeneratorConfig:
|
||||
compute_jacobian: bool = False
|
||||
|
||||
#: Padding to add for self collision between links. Some robots use a large padding
|
||||
#: for self collision avoidance (https://github.com/ros-planning/panda_moveit_config/pull/35#issuecomment-671333863)
|
||||
#: for self collision avoidance (e.g., `MoveIt Panda Issue <https://github.com/ros-planning/panda_moveit_config/pull/35#issuecomment-671333863>`_).
|
||||
self_collision_buffer: Optional[Dict[str, float]] = None
|
||||
|
||||
#: Self-collision ignore
|
||||
#: Dictionary with each key as a link name and value as a list of link names to ignore self
|
||||
#: collision. E.g., {"link1": ["link2", "link3"], "link2": ["link3", "link4"]} will
|
||||
#: ignore self collision between link1 and link2, link1 and link3, link2 and link3, link2 and
|
||||
#: link4. The mapping is bidirectional so it's sufficient to mention the mapping in one
|
||||
#: direction (i.e., not necessary to mention "link1" in ignore list for "link2").
|
||||
self_collision_ignore: Optional[Dict[str, List[str]]] = None
|
||||
|
||||
#: debug config
|
||||
#: Debugging information to pass to kinematics module.
|
||||
debug: Optional[Dict[str, Any]] = None
|
||||
|
||||
#: Enabling this flag writes out the cumulative transformation matrix to global memory. This
|
||||
@@ -129,33 +140,46 @@ class CudaRobotGeneratorConfig:
|
||||
#: joint angle given from this dictionary.
|
||||
lock_joints: Optional[Dict[str, float]] = None
|
||||
|
||||
#: Additional links to add to parsed kinematics tree. This is useful for adding fixed links
|
||||
#: that are not present in the URDF or USD.
|
||||
extra_links: Optional[Dict[str, LinkParams]] = None
|
||||
|
||||
#: this is deprecated
|
||||
#: Deprecated way to add a fixed link.
|
||||
add_object_link: bool = False
|
||||
|
||||
#: Deprecated flag to load assets from external module. Now, pass absolute path to
|
||||
#: asset_root_path or use :class:`~curobo.util.file_path.ContentPath`.
|
||||
use_external_assets: bool = False
|
||||
|
||||
#: Deprecated path to load assets from external module. Use
|
||||
#: :class:`~curobo.util.file_path.ContentPath` instead.
|
||||
external_asset_path: Optional[str] = None
|
||||
|
||||
#: Deprecated path to load robot configs from external module. Use
|
||||
#: :class:`~curobo.util.file_path.ContentPath` instead.
|
||||
external_robot_configs_path: Optional[str] = None
|
||||
|
||||
#: Create n collision spheres for links with name
|
||||
extra_collision_spheres: Optional[Dict[str, int]] = None
|
||||
|
||||
#: cspace config
|
||||
#: Configuration space parameters for robot (e.g, acceleration, jerk limits).
|
||||
cspace: Union[None, CSpaceConfig, Dict[str, List[Any]]] = None
|
||||
|
||||
#: Enable loading meshes from kinematics parser.
|
||||
load_meshes: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post initialization adds absolute paths, converts dictionaries to objects."""
|
||||
|
||||
# add root path:
|
||||
# Check if an external asset path is provided:
|
||||
asset_path = get_assets_path()
|
||||
robot_path = get_robot_configs_path()
|
||||
if self.external_asset_path is not None:
|
||||
log_warn("Deprecated: external_asset_path is deprecated, use ContentPath")
|
||||
asset_path = self.external_asset_path
|
||||
if self.external_robot_configs_path is not None:
|
||||
log_warn("Deprecated: external_robot_configs_path is deprecated, use ContentPath")
|
||||
robot_path = self.external_robot_configs_path
|
||||
|
||||
if self.urdf_path is not None:
|
||||
@@ -214,30 +238,45 @@ class CudaRobotGeneratorConfig:
|
||||
|
||||
|
||||
class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
"""Robot Kinematics Representation Generator.
|
||||
|
||||
The word "Chain" is used interchangeably with "Tree" in this class.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config: CudaRobotGeneratorConfig) -> None:
|
||||
"""Initialize the robot generator.
|
||||
|
||||
Args:
|
||||
config: Parameters to initialize the robot generator.
|
||||
"""
|
||||
super().__init__(**vars(config))
|
||||
self.cpu_tensor_args = self.tensor_args.cpu()
|
||||
|
||||
self._self_collision_data = None
|
||||
self.non_fixed_joint_names = []
|
||||
self._n_dofs = 1
|
||||
|
||||
self._kinematics_config = None
|
||||
self.initialize_tensors()
|
||||
|
||||
@property
|
||||
def kinematics_config(self):
|
||||
def kinematics_config(self) -> KinematicsTensorConfig:
|
||||
"""Kinematics representation as Tensors."""
|
||||
return self._kinematics_config
|
||||
|
||||
@property
|
||||
def self_collision_config(self):
|
||||
def self_collision_config(self) -> SelfCollisionKinematicsConfig:
|
||||
"""Self collision configuration for robot."""
|
||||
return self._self_collision_data
|
||||
|
||||
@property
|
||||
def kinematics_parser(self):
|
||||
"""Kinematics parser used to generate robot parameters."""
|
||||
return self._kinematics_parser
|
||||
|
||||
@profiler.record_function("robot_generator/initialize_tensors")
|
||||
def initialize_tensors(self):
|
||||
"""Initialize tensors for kinematics representatiobn."""
|
||||
self._joint_limits = None
|
||||
self._self_collision_data = None
|
||||
self.lock_jointstate = None
|
||||
@@ -335,10 +374,15 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
lock_jointstate=self.lock_jointstate,
|
||||
mimic_joints=self._mimic_joint_data,
|
||||
)
|
||||
if self.asset_root_path != "":
|
||||
if self.asset_root_path is not None and self.asset_root_path != "":
|
||||
self._kinematics_parser.add_absolute_path_to_link_meshes(self.asset_root_path)
|
||||
|
||||
def add_link(self, link_params: LinkParams):
|
||||
"""Add an extra link to the robot kinematics tree.
|
||||
|
||||
Args:
|
||||
link_params: Parameters of the link to add.
|
||||
"""
|
||||
self.extra_links[link_params.link_name] = link_params
|
||||
|
||||
def add_fixed_link(
|
||||
@@ -348,6 +392,14 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
joint_name: Optional[str] = None,
|
||||
transform: Optional[Pose] = None,
|
||||
):
|
||||
"""Add a fixed link to the robot kinematics tree.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link to add.
|
||||
parent_link_name: Parent link to add the fixed link to.
|
||||
joint_name: Name of fixed to joint to create.
|
||||
transform: Offset transform of the fixed link from the joint.
|
||||
"""
|
||||
if transform is None:
|
||||
transform = (
|
||||
Pose.from_list([0, 0, 0, 1, 0, 0, 0], self.tensor_args)
|
||||
@@ -368,7 +420,22 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
self.add_link(link_params)
|
||||
|
||||
@profiler.record_function("robot_generator/build_chain")
|
||||
def _build_chain(self, base_link, ee_link, other_links, link_names):
|
||||
def _build_chain(
|
||||
self,
|
||||
base_link: str,
|
||||
ee_link: str,
|
||||
other_links: List[str],
|
||||
) -> List[str]:
|
||||
"""Build kinematic tree of the robot.
|
||||
|
||||
Args:
|
||||
base_link: Name of base link for the chain.
|
||||
ee_link: Name of end-effector link for the chain.
|
||||
other_links: List of other links to add to the chain.
|
||||
|
||||
Returns:
|
||||
List[str]: List of link names in the chain.
|
||||
"""
|
||||
self._n_dofs = 0
|
||||
self._controlled_links = []
|
||||
self._bodies = []
|
||||
@@ -404,7 +471,13 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
self.non_fixed_joint_names = self.joint_names.copy()
|
||||
return chain_link_names
|
||||
|
||||
def _get_mimic_joint_data(self):
|
||||
def _get_mimic_joint_data(self) -> Dict[str, List[int]]:
|
||||
"""Get joints that are mimicked from actuated joints joints.
|
||||
|
||||
Returns:
|
||||
Dict[str, List[int]]: Dictionary containing name of actuated joint and list of mimic
|
||||
joint indices.
|
||||
"""
|
||||
# get joint types:
|
||||
mimic_joint_data = {}
|
||||
for i in range(1, len(self._bodies)):
|
||||
@@ -417,7 +490,16 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
return mimic_joint_data
|
||||
|
||||
@profiler.record_function("robot_generator/build_kinematics_tensors")
|
||||
def _build_kinematics_tensors(self, base_link, ee_link, link_names, chain_link_names):
|
||||
def _build_kinematics_tensors(self, base_link, link_names, chain_link_names):
|
||||
"""Create kinematic tensors for robot given kinematic tree.
|
||||
|
||||
Args:
|
||||
base_link: Name of base link for the tree.
|
||||
link_names: Namer of links to compute kinematics for. This is used to determine link
|
||||
indices to store pose during forward kinematics.
|
||||
chain_link_names: List of link names in the kinematic tree. Used to traverse the
|
||||
kinematic tree.
|
||||
"""
|
||||
self._active_joints = []
|
||||
self._mimic_joint_data = {}
|
||||
link_map = [0 for i in range(len(self._bodies))]
|
||||
@@ -493,9 +575,19 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
self._all_joint_names = all_joint_names
|
||||
|
||||
@profiler.record_function("robot_generator/build_kinematics")
|
||||
def _build_kinematics(self, base_link, ee_link, other_links, link_names):
|
||||
chain_link_names = self._build_chain(base_link, ee_link, other_links, link_names)
|
||||
self._build_kinematics_tensors(base_link, ee_link, link_names, chain_link_names)
|
||||
def _build_kinematics(
|
||||
self, base_link: str, ee_link: str, other_links: List[str], link_names: List[str]
|
||||
):
|
||||
"""Build kinematics tensors given base link, end-effector link and other links.
|
||||
|
||||
Args:
|
||||
base_link: Name of base link for the kinematic tree.
|
||||
ee_link: Name of end-effector link for the kinematic tree.
|
||||
other_links: List of other links to add to the kinematic tree.
|
||||
link_names: List of link names to store poses after kinematics computation.
|
||||
"""
|
||||
chain_link_names = self._build_chain(base_link, ee_link, other_links)
|
||||
self._build_kinematics_tensors(base_link, link_names, chain_link_names)
|
||||
if self.collision_spheres is not None and len(self.collision_link_names) > 0:
|
||||
self._build_collision_model(
|
||||
self.collision_spheres, self.collision_link_names, self.collision_sphere_buffer
|
||||
@@ -504,13 +596,26 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
@profiler.record_function("robot_generator/build_kinematics_with_lock_joints")
|
||||
def _build_kinematics_with_lock_joints(
|
||||
self,
|
||||
base_link,
|
||||
ee_link,
|
||||
other_links,
|
||||
link_names,
|
||||
base_link: str,
|
||||
ee_link: str,
|
||||
other_links: List[str],
|
||||
link_names: List[str],
|
||||
lock_joints: Dict[str, float],
|
||||
):
|
||||
chain_link_names = self._build_chain(base_link, ee_link, other_links, link_names)
|
||||
"""Build kinematics with locked joints.
|
||||
|
||||
This function will first build the chain with no locked joints, find the transforms
|
||||
when the locked joints are set to the given values, and then use these transforms as
|
||||
fixed transforms for the locked joints.
|
||||
|
||||
Args:
|
||||
base_link: Base link of the kinematic tree.
|
||||
ee_link: End-effector link of the kinematic tree.
|
||||
other_links: Other links to add to the kinematic tree.
|
||||
link_names: List of link names to store poses after kinematics computation.
|
||||
lock_joints: Joints to lock in the kinematic tree with value to lock at.
|
||||
"""
|
||||
chain_link_names = self._build_chain(base_link, ee_link, other_links)
|
||||
# find links attached to lock joints:
|
||||
lock_joint_names = list(lock_joints.keys())
|
||||
|
||||
@@ -531,7 +636,7 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
new_link_names = list(set(link_names + lock_links))
|
||||
|
||||
# rebuild kinematic tree with link names added to link pose computation:
|
||||
self._build_kinematics_tensors(base_link, ee_link, new_link_names, chain_link_names)
|
||||
self._build_kinematics_tensors(base_link, new_link_names, chain_link_names)
|
||||
if self.collision_spheres is not None and len(self.collision_link_names) > 0:
|
||||
self._build_collision_model(
|
||||
self.collision_spheres, self.collision_link_names, self.collision_sphere_buffer
|
||||
@@ -628,12 +733,12 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
collision_link_names: List[str],
|
||||
collision_sphere_buffer: Union[float, Dict[str, float]] = 0.0,
|
||||
):
|
||||
"""
|
||||
"""Build collision model for robot.
|
||||
|
||||
Args:
|
||||
collision_spheres (_type_): _description_
|
||||
collision_link_names (_type_): _description_
|
||||
collision_sphere_buffer (float, optional): _description_. Defaults to 0.0.
|
||||
collision_spheres: Spheres for each link of the robot.
|
||||
collision_link_names: Name of links to load spheres for.
|
||||
collision_sphere_buffer: Additional padding to add to collision spheres.
|
||||
"""
|
||||
|
||||
# We create all tensors on cpu and then finally move them to gpu
|
||||
@@ -680,7 +785,7 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
)
|
||||
|
||||
# build self collision distance tensor:
|
||||
self.self_collision_distance = (
|
||||
self_collision_distance = (
|
||||
torch.zeros(
|
||||
(self.total_spheres, self.total_spheres),
|
||||
dtype=cpu_tensor_args.dtype,
|
||||
@@ -691,7 +796,6 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
self.self_collision_offset = torch.zeros(
|
||||
(self.total_spheres), dtype=cpu_tensor_args.dtype, device=cpu_tensor_args.device
|
||||
)
|
||||
|
||||
with profiler.record_function("robot_generator/self_collision_distance"):
|
||||
# iterate through each link:
|
||||
for j_idx, j in enumerate(collision_link_names):
|
||||
@@ -724,44 +828,66 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
sp1 = link1_spheres_idx[k1]
|
||||
for k2 in range(len(rad2)):
|
||||
sp2 = link2_spheres_idx[k2]
|
||||
self.self_collision_distance[sp1, sp2] = rad1[k1] + rad2[k2] + c1 + c2
|
||||
self_collision_distance[sp1, sp2] = rad1[k1] + rad2[k2] + c1 + c2
|
||||
|
||||
self_collision_distance = self_collision_distance.to(device=self.tensor_args.device)
|
||||
with profiler.record_function("robot_generator/self_collision_min"):
|
||||
d_mat = self.self_collision_distance
|
||||
self.self_collision_distance = torch.minimum(d_mat, d_mat.transpose(0, 1))
|
||||
d_mat = self_collision_distance
|
||||
self_collision_distance = torch.minimum(d_mat, d_mat.transpose(0, 1))
|
||||
|
||||
if self.debug is not None and "self_collision_experimental" in self.debug:
|
||||
use_experimental_kernel = self.debug["self_collision_experimental"]
|
||||
self.self_collision_distance = self.self_collision_distance.to(
|
||||
device=self.tensor_args.device
|
||||
)
|
||||
(
|
||||
self._self_coll_thread_locations,
|
||||
self._self_coll_idx,
|
||||
valid_data,
|
||||
checks_per_thread,
|
||||
) = self._create_self_collision_thread_data(self.self_collision_distance)
|
||||
self._self_coll_matrix = (self.self_collision_distance != -(torch.inf)).to(
|
||||
dtype=torch.uint8
|
||||
)
|
||||
|
||||
) = self._create_self_collision_thread_data(self_collision_distance)
|
||||
use_experimental_kernel = True
|
||||
|
||||
if (
|
||||
self.debug is not None
|
||||
and "self_collision_experimental" in self.debug
|
||||
and self.debug["self_collision_experimental"] is not None
|
||||
):
|
||||
use_experimental_kernel = self.debug["self_collision_experimental"]
|
||||
|
||||
if not valid_data:
|
||||
use_experimental_kernel = False
|
||||
log_warn("Self Collision checks are greater than 32 * 512, using slower kernel")
|
||||
if use_experimental_kernel:
|
||||
self_coll_matrix = torch.zeros((2), device=self.tensor_args.device, dtype=torch.uint8)
|
||||
else:
|
||||
self_coll_matrix = (self_collision_distance != -(torch.inf)).to(dtype=torch.uint8)
|
||||
# self_coll_matrix = (self_collision_distance != -(torch.inf)).to(dtype=torch.uint8)
|
||||
|
||||
# convert all tensors to gpu:
|
||||
self._link_sphere_idx_map = self._link_sphere_idx_map.to(device=self.tensor_args.device)
|
||||
self._link_spheres_tensor = self._link_spheres_tensor.to(device=self.tensor_args.device)
|
||||
self.self_collision_offset = self.self_collision_offset.to(device=self.tensor_args.device)
|
||||
self._self_collision_data = SelfCollisionKinematicsConfig(
|
||||
offset=self.self_collision_offset,
|
||||
distance_threshold=self.self_collision_distance,
|
||||
thread_location=self._self_coll_thread_locations,
|
||||
thread_max=self._self_coll_idx,
|
||||
collision_matrix=self._self_coll_matrix,
|
||||
experimental_kernel=valid_data and use_experimental_kernel,
|
||||
collision_matrix=self_coll_matrix,
|
||||
experimental_kernel=use_experimental_kernel,
|
||||
checks_per_thread=checks_per_thread,
|
||||
)
|
||||
|
||||
@profiler.record_function("robot_generator/create_self_collision_thread_data")
|
||||
def _create_self_collision_thread_data(self, collision_threshold):
|
||||
def _create_self_collision_thread_data(
|
||||
self, collision_threshold: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, int, bool, int]:
|
||||
"""Create thread data for self collision checks.
|
||||
|
||||
Args:
|
||||
collision_threshold: Collision distance between spheres of the robot. Used to
|
||||
skip self collision checks when distance is -inf.
|
||||
|
||||
Returns:
|
||||
Tuple[torch.Tensor, int, bool, int]: Thread location for self collision checks,
|
||||
number of self collision checks, if thread calculation was successful,
|
||||
and number of checks per thread.
|
||||
|
||||
"""
|
||||
coll_cpu = collision_threshold.cpu()
|
||||
max_checks_per_thread = 512
|
||||
thread_loc = torch.zeros((2 * 32 * max_checks_per_thread), dtype=torch.int16) - 1
|
||||
@@ -815,7 +941,10 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
elif val < 512:
|
||||
max_checks_per_thread = 512
|
||||
else:
|
||||
raise ValueError("Self Collision not supported")
|
||||
log_error(
|
||||
"Self Collision not supported as checks are greater than 32 * 512, \
|
||||
reduce number of spheres used to approximate the robot."
|
||||
)
|
||||
|
||||
if max_checks_per_thread < 2:
|
||||
max_checks_per_thread = 2
|
||||
@@ -829,7 +958,13 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
)
|
||||
|
||||
@profiler.record_function("robot_generator/add_body_to_tree")
|
||||
def _add_body_to_tree(self, link_name, base=False):
|
||||
def _add_body_to_tree(self, link_name: str, base=False):
|
||||
"""Add link to kinematic tree.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link to add.
|
||||
base: Is this the base link of the kinematic tree?
|
||||
"""
|
||||
body_idx = len(self._bodies)
|
||||
|
||||
rigid_body_params = self._kinematics_parser.get_link_parameters(link_name, base=base)
|
||||
@@ -848,7 +983,17 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
)
|
||||
self._name_to_idx_map[rigid_body_params.link_name] = body_idx
|
||||
|
||||
def _get_joint_links(self, joint_names: List[str]):
|
||||
def _get_joint_links(self, joint_names: List[str]) -> Dict[str, Dict[str, Union[str, int]]]:
|
||||
"""Get data (parent link, child link, mimic, link_index) for joints given in the list.
|
||||
|
||||
Args:
|
||||
joint_names: Names of joints to get data for.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, Union[str, int]]]: Dictionary containing joint name as key and
|
||||
dictionary containing parent link, child link, and link index as
|
||||
values. Also includes mimic joint data if present.
|
||||
"""
|
||||
j_data = {}
|
||||
|
||||
for j in joint_names:
|
||||
@@ -878,6 +1023,22 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
def _get_link_poses(
|
||||
self, q: torch.Tensor, link_names: List[str], kinematics_config: KinematicsTensorConfig
|
||||
) -> Pose:
|
||||
"""Get Pose of links at given joint angles using forward kinematics.
|
||||
|
||||
This is implemented here to avoid circular dependencies with
|
||||
:class:`~curobo.cuda_robot_model.cuda_robot_model.CudaRobotModel` module. This is used
|
||||
to calculate fixed transforms for locked joints in this class. This implementation
|
||||
does not compute position of robot spheres.
|
||||
|
||||
Args:
|
||||
q: Joint angles to compute forward kinematics for.
|
||||
link_names: Name of links to return pose.
|
||||
kinematics_config: Tensor Configuration for kinematics computation.
|
||||
|
||||
Returns:
|
||||
Pose: Pose of links at given joint angles.
|
||||
"""
|
||||
q = q.view(1, -1)
|
||||
link_pos_seq = torch.zeros(
|
||||
(1, len(self.link_names), 3),
|
||||
device=self.tensor_args.device,
|
||||
@@ -889,7 +1050,7 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
dtype=self.tensor_args.dtype,
|
||||
)
|
||||
batch_robot_spheres = torch.zeros(
|
||||
(1, self.total_spheres, 4),
|
||||
(1, 0, 4),
|
||||
device=self.tensor_args.device,
|
||||
dtype=self.tensor_args.dtype,
|
||||
)
|
||||
@@ -903,23 +1064,24 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
device=self.tensor_args.device,
|
||||
dtype=self.tensor_args.dtype,
|
||||
)
|
||||
|
||||
link_pos_seq, link_quat_seq, _ = get_cuda_kinematics(
|
||||
link_pos_seq,
|
||||
link_quat_seq,
|
||||
batch_robot_spheres,
|
||||
batch_robot_spheres.contiguous(),
|
||||
global_cumul_mat,
|
||||
q,
|
||||
kinematics_config.fixed_transforms,
|
||||
kinematics_config.link_spheres,
|
||||
kinematics_config.fixed_transforms.contiguous(),
|
||||
kinematics_config.link_spheres.contiguous(),
|
||||
kinematics_config.link_map, # tells which link is attached to which link i
|
||||
kinematics_config.joint_map, # tells which joint is attached to a link i
|
||||
kinematics_config.joint_map_type, # joint type
|
||||
kinematics_config.store_link_map,
|
||||
kinematics_config.link_sphere_idx_map, # sphere idx map
|
||||
kinematics_config.link_sphere_idx_map.contiguous(), # sphere idx map
|
||||
kinematics_config.link_chain_map,
|
||||
kinematics_config.joint_offset_map,
|
||||
grad_out_q,
|
||||
self.use_global_cumul,
|
||||
False,
|
||||
)
|
||||
position = torch.zeros(
|
||||
(q.shape[0], len(link_names), 3),
|
||||
@@ -936,14 +1098,21 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
i = self.link_names.index(l)
|
||||
position[:, li, :] = link_pos_seq[:, i, :]
|
||||
quaternion[:, li, :] = link_quat_seq[:, i, :]
|
||||
return Pose(position=position, quaternion=quaternion)
|
||||
return Pose(position=position.clone(), quaternion=quaternion.clone())
|
||||
|
||||
@property
|
||||
def get_joint_limits(self):
|
||||
def get_joint_limits(self) -> JointLimits:
|
||||
"""Get joint limits for the robot."""
|
||||
return self._joint_limits
|
||||
|
||||
@profiler.record_function("robot_generator/get_joint_limits")
|
||||
def _get_joint_position_velocity_limits(self):
|
||||
def _get_joint_position_velocity_limits(self) -> Dict[str, torch.Tensor]:
|
||||
"""Compute joint position and velocity limits for the robot.
|
||||
|
||||
Returns:
|
||||
Dict[str, torch.Tensor]: Dictionary containing position and velocity limits for the
|
||||
robot. Each value is a tensor of shape (2, n_joints) with first row containing
|
||||
minimum limits and second row containing maximum limits.
|
||||
"""
|
||||
joint_limits = {"position": [[], []], "velocity": [[], []]}
|
||||
|
||||
for idx in self._active_joints:
|
||||
@@ -959,6 +1128,7 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
|
||||
@profiler.record_function("robot_generator/update_joint_limits")
|
||||
def _update_joint_limits(self):
|
||||
"""Update limits from CSpaceConfig (acceleration, jerk limits and position clips)."""
|
||||
joint_limits = self._get_joint_position_velocity_limits()
|
||||
joint_limits["jerk"] = torch.cat(
|
||||
[-1.0 * self.cspace.max_jerk.unsqueeze(0), self.cspace.max_jerk.unsqueeze(0)]
|
||||
@@ -970,7 +1140,6 @@ class CudaRobotGenerator(CudaRobotGeneratorConfig):
|
||||
]
|
||||
)
|
||||
# clip joint position:
|
||||
# NOTE: change this to be per joint
|
||||
joint_limits["position"][0] += self.cspace.position_limit_clip
|
||||
joint_limits["position"][1] -= self.cspace.position_limit_clip
|
||||
joint_limits["velocity"][0] *= self.cspace.velocity_scale
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
|
||||
"""
|
||||
This module builds a kinematic representation of a robot on the GPU and provides
|
||||
differentiable mapping from it's joint configuration to Cartesian pose of it's links
|
||||
(forward kinematics). This module also computes the position of the spheres of the robot as part
|
||||
of the forward kinematics function.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Standard Library
|
||||
@@ -24,28 +32,75 @@ from curobo.cuda_robot_model.cuda_robot_generator import (
|
||||
CudaRobotGeneratorConfig,
|
||||
)
|
||||
from curobo.cuda_robot_model.kinematics_parser import KinematicsParser
|
||||
from curobo.cuda_robot_model.types import KinematicsTensorConfig, SelfCollisionKinematicsConfig
|
||||
from curobo.cuda_robot_model.types import (
|
||||
CSpaceConfig,
|
||||
JointLimits,
|
||||
KinematicsTensorConfig,
|
||||
SelfCollisionKinematicsConfig,
|
||||
)
|
||||
from curobo.cuda_robot_model.util import load_robot_yaml
|
||||
from curobo.curobolib.kinematics import get_cuda_kinematics
|
||||
from curobo.geom.types import Mesh, Sphere
|
||||
from curobo.geom.sphere_fit import SphereFitType
|
||||
from curobo.geom.types import Mesh, Obstacle, Sphere
|
||||
from curobo.types.base import TensorDeviceType
|
||||
from curobo.types.file_path import ContentPath
|
||||
from curobo.types.math import Pose
|
||||
from curobo.types.state import JointState
|
||||
from curobo.util.logger import log_error
|
||||
from curobo.util_file import get_robot_path, join_path, load_yaml
|
||||
from curobo.util.logger import log_error, log_info, log_warn
|
||||
from curobo.util_file import is_file_xrdf
|
||||
|
||||
|
||||
@dataclass
|
||||
class CudaRobotModelConfig:
|
||||
"""
|
||||
Configuration for robot kinematics on GPU.
|
||||
|
||||
Helper functions are provided to load this configuration from an URDF file or from
|
||||
a cuRobo robot configuration file (:ref:`tut_robot_configuration`). To create from a XRDF, use
|
||||
:ref:`curobo.util.xrdf_utils.convert_xrdf_to_curobo`.
|
||||
"""
|
||||
|
||||
#: Device and floating point precision to use for kinematics.
|
||||
tensor_args: TensorDeviceType
|
||||
|
||||
#: Names of links to compute poses with forward kinematics.
|
||||
link_names: List[str]
|
||||
|
||||
#: Tensors representing kinematics of the robot. This can be created using
|
||||
#: :class:`~curobo.cuda_robot_model.cuda_robot_generator.CudaRobotGenerator`.
|
||||
kinematics_config: KinematicsTensorConfig
|
||||
|
||||
#: Collision pairs to ignore when computing self collision between spheres across all
|
||||
#: robot links. This also contains distance threshold between spheres pairs and which thread
|
||||
#: indices for calculating the distances. More details on computing these parameters is in
|
||||
#: :func:`~curobo.cuda_robot_model.cuda_robot_generator.CudaRobotGenerator._build_collision_model`.
|
||||
self_collision_config: Optional[SelfCollisionKinematicsConfig] = None
|
||||
|
||||
#: Parser to load kinematics from URDF or USD files. This is used to load kinematics
|
||||
#: representation of the robot. This is created using
|
||||
#: :class:`~curobo.cuda_robot_model.kinematics_parser.KinematicsParser`.
|
||||
#: USD is an experimental feature and might not work for all robots.
|
||||
kinematics_parser: Optional[KinematicsParser] = None
|
||||
|
||||
#: Output jacobian during forward kinematics. This is not implemented. The forward kinematics
|
||||
#: function does use Jacobian during backward pass. What's not supported is
|
||||
compute_jacobian: bool = False
|
||||
use_global_cumul: bool = False
|
||||
|
||||
#: Store transformation matrix of every link during forward kinematics call in global memory.
|
||||
#: This helps speed up backward pass as we don't need to recompute the transformation matrices.
|
||||
#: However, this increases memory usage and also slightly slows down forward kinematics.
|
||||
#: Enabling this is recommended for getting the best performance.
|
||||
use_global_cumul: bool = True
|
||||
|
||||
#: Generator config used to create this robot kinematics model.
|
||||
generator_config: Optional[CudaRobotGeneratorConfig] = None
|
||||
|
||||
def get_joint_limits(self):
|
||||
def get_joint_limits(self) -> JointLimits:
|
||||
"""Get limits of actuated joints of the robot.
|
||||
|
||||
Returns:
|
||||
JointLimits: Joint limits of the robot's actuated joints.
|
||||
"""
|
||||
return self.kinematics_config.joint_limits
|
||||
|
||||
@staticmethod
|
||||
@@ -99,29 +154,96 @@ class CudaRobotModelConfig:
|
||||
return CudaRobotModelConfig.from_config(config)
|
||||
|
||||
@staticmethod
|
||||
def from_robot_yaml_file(
|
||||
file_path: str,
|
||||
def from_content_path(
|
||||
content_path: ContentPath,
|
||||
ee_link: Optional[str] = None,
|
||||
tensor_args: TensorDeviceType = TensorDeviceType(),
|
||||
):
|
||||
config_file = load_yaml(join_path(get_robot_path(), file_path))["robot_cfg"]["kinematics"]
|
||||
) -> CudaRobotModelConfig:
|
||||
"""Load robot from Contentpath containing paths to robot description files.
|
||||
|
||||
Args:
|
||||
content_path: Path to robot configuration files.
|
||||
ee_link: End-effector link name. If None, it is read from the file.
|
||||
tensor_args: Device to load robot model, defaults to cuda:0.
|
||||
|
||||
Returns:
|
||||
CudaRobotModelConfig: cuda robot model configuration.
|
||||
"""
|
||||
|
||||
config_file = load_robot_yaml(content_path)
|
||||
if "robot_cfg" in config_file:
|
||||
config_file = config_file["robot_cfg"]
|
||||
if "kinematics" in config_file:
|
||||
config_file = config_file["kinematics"]
|
||||
if ee_link is not None:
|
||||
config_file["ee_link"] = ee_link
|
||||
|
||||
return CudaRobotModelConfig.from_config(
|
||||
CudaRobotGeneratorConfig(**config_file, tensor_args=tensor_args)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_robot_yaml_file(
|
||||
file_path: Union[str, Dict],
|
||||
ee_link: Optional[str] = None,
|
||||
tensor_args: TensorDeviceType = TensorDeviceType(),
|
||||
urdf_path: Optional[str] = None,
|
||||
) -> CudaRobotModelConfig:
|
||||
"""Load robot from a yaml file that is in cuRobo's format (:ref:`tut_robot_configuration`).
|
||||
|
||||
Args:
|
||||
file_path: Path to robot configuration file (yml or xrdf).
|
||||
ee_link: End-effector link name. If None, it is read from the file.
|
||||
tensor_args: Device to load robot model, defaults to cuda:0.
|
||||
urdf_path: Path to urdf file. This is required when loading a xrdf file.
|
||||
|
||||
Returns:
|
||||
CudaRobotModelConfig: cuda robot model configuration.
|
||||
"""
|
||||
if isinstance(file_path, dict):
|
||||
content_path = ContentPath(robot_urdf_file=urdf_path, robot_config_file=file_path)
|
||||
else:
|
||||
if is_file_xrdf(file_path):
|
||||
content_path = ContentPath(robot_urdf_file=urdf_path, robot_xrdf_file=file_path)
|
||||
else:
|
||||
content_path = ContentPath(robot_urdf_file=urdf_path, robot_config_file=file_path)
|
||||
|
||||
return CudaRobotModelConfig.from_content_path(content_path, ee_link, tensor_args)
|
||||
|
||||
@staticmethod
|
||||
def from_data_dict(
|
||||
data_dict: Dict[str, Any],
|
||||
tensor_args: TensorDeviceType = TensorDeviceType(),
|
||||
):
|
||||
) -> CudaRobotModelConfig:
|
||||
"""Load robot from a dictionary containing data for :class:`~curobo.cuda_robot_model.cuda_robot_generator.CudaRobotGeneratorConfig`.
|
||||
|
||||
:tut_robot_configuration discusses the data required to load a robot.
|
||||
|
||||
Args:
|
||||
data_dict: Input dictionary containing robot configuration.
|
||||
tensor_args: Device to load robot model, defaults to cuda:0.
|
||||
|
||||
Returns:
|
||||
CudaRobotModelConfig: cuda robot model configuration.
|
||||
"""
|
||||
if "robot_cfg" in data_dict:
|
||||
data_dict = data_dict["robot_cfg"]
|
||||
if "kinematics" in data_dict:
|
||||
data_dict = data_dict["kinematics"]
|
||||
return CudaRobotModelConfig.from_config(
|
||||
CudaRobotGeneratorConfig(**data_dict, tensor_args=tensor_args)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_config(config: CudaRobotGeneratorConfig):
|
||||
def from_config(config: CudaRobotGeneratorConfig) -> CudaRobotModelConfig:
|
||||
"""Create a robot model configuration from a generator configuration.
|
||||
|
||||
Args:
|
||||
config: Input robot generator configuration.
|
||||
|
||||
Returns:
|
||||
CudaRobotModelConfig: robot model configuration.
|
||||
"""
|
||||
# create a config generator and load all values
|
||||
generator = CudaRobotGenerator(config)
|
||||
return CudaRobotModelConfig(
|
||||
@@ -136,17 +258,19 @@ class CudaRobotModelConfig:
|
||||
)
|
||||
|
||||
@property
|
||||
def cspace(self):
|
||||
def cspace(self) -> CSpaceConfig:
|
||||
"""Get cspace parameters of the robot."""
|
||||
return self.kinematics_config.cspace
|
||||
|
||||
@property
|
||||
def dof(self) -> int:
|
||||
"""Get the number of actuated joints (degrees of freedom) of the robot"""
|
||||
return self.kinematics_config.n_dof
|
||||
|
||||
|
||||
@dataclass
|
||||
class CudaRobotModelState:
|
||||
"""Dataclass that stores kinematics information."""
|
||||
"""Kinematic state of robot."""
|
||||
|
||||
#: End-effector position stored as x,y,z in meters [b, 3]. End-effector is defined by
|
||||
#: :attr:`CudaRobotModel.ee_link`.
|
||||
@@ -207,20 +331,38 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
"""
|
||||
CUDA Accelerated Robot Model
|
||||
|
||||
Load basic kinematics from an URDF with :func:`~CudaRobotModelConfig.from_basic_urdf`.
|
||||
Check :ref:`tut_robot_configuration` for details on how to also create a geometric
|
||||
representation of the robot.
|
||||
Currently dof is created only for links that we need to compute kinematics. E.g., for robots
|
||||
with many serial chains, add all links of the robot to get the correct dof. This is not an
|
||||
issue if you are loading collision spheres as that will cover the full geometry of the robot.
|
||||
"""
|
||||
|
||||
def __init__(self, config: CudaRobotModelConfig):
|
||||
"""Initialize kinematics instance with a robot model configuration.
|
||||
|
||||
Args:
|
||||
config: Input robot model configuration.
|
||||
"""
|
||||
super().__init__(**vars(config))
|
||||
self._batch_size = 0
|
||||
self.update_batch_size(1, reset_buffers=True)
|
||||
|
||||
def update_batch_size(self, batch_size, force_update=False, reset_buffers=False):
|
||||
def update_batch_size(
|
||||
self, batch_size: int, force_update: bool = False, reset_buffers: bool = False
|
||||
):
|
||||
"""Update batch size of the robot model.
|
||||
|
||||
Args:
|
||||
batch_size: Batch size to update the robot model.
|
||||
force_update: Detach gradients of tensors. This is not supported.
|
||||
reset_buffers: Recreate the tensors even if the batch size is same.
|
||||
"""
|
||||
if batch_size == 0:
|
||||
log_error("batch size is zero")
|
||||
if force_update and self._batch_size == batch_size and self.compute_jacobian:
|
||||
log_error("Outputting jacobian is not supported")
|
||||
self.lin_jac = self.lin_jac.detach() # .requires_grad_(True)
|
||||
self.ang_jac = self.ang_jac.detach() # .requires_grad_(True)
|
||||
elif self._batch_size != batch_size or reset_buffers:
|
||||
@@ -252,6 +394,7 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
dtype=self.tensor_args.dtype,
|
||||
)
|
||||
if self.compute_jacobian:
|
||||
log_error("Outputting jacobian is not supported")
|
||||
self.lin_jac = torch.zeros(
|
||||
[batch_size, 3, self.kinematics_config.n_dofs],
|
||||
device=self.tensor_args.device,
|
||||
@@ -264,10 +407,27 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
)
|
||||
|
||||
@profiler.record_function("cuda_robot_model/forward_kinematics")
|
||||
def forward(self, q, link_name=None, calculate_jacobian=False):
|
||||
# pos, rot = self.compute_forward_kinematics(q, qd, link_name)
|
||||
def forward(
|
||||
self, q, link_name=None, calculate_jacobian=False
|
||||
) -> Tuple[Tensor, Tensor, None, None, Tensor, Tensor, Tensor]:
|
||||
"""Compute forward kinematics of the robot.
|
||||
|
||||
Use :func:`~get_state` to get a structured output.
|
||||
|
||||
Args:
|
||||
q: Joint configuration of the robot. Shape should be [batch_size, dof].
|
||||
link_name: Name of link to return pose of. If None, returns end-effector pose.
|
||||
calculate_jacobian: Calculate jacobian of the robot. Not supported.
|
||||
|
||||
Returns:
|
||||
Tuple[Tensor, Tensor, None, None, Tensor, Tensor, Tensor]: End-effector position,
|
||||
end-effector quaternion (wxyz), linear jacobian(None), angular jacobian(None),
|
||||
link positions, link quaternion (wxyz), link spheres.
|
||||
"""
|
||||
if len(q.shape) > 2:
|
||||
raise ValueError("q shape should be [batch_size, dof]")
|
||||
log_error("q shape should be [batch_size, dof]")
|
||||
if len(q.shape) == 1:
|
||||
q = q.unsqueeze(0)
|
||||
batch_size = q.shape[0]
|
||||
self.update_batch_size(batch_size, force_update=q.requires_grad)
|
||||
|
||||
@@ -287,7 +447,7 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
|
||||
# compute jacobians?
|
||||
if calculate_jacobian:
|
||||
raise NotImplementedError
|
||||
log_error("Outputting jacobian is not supported")
|
||||
return (
|
||||
ee_pos,
|
||||
ee_quat,
|
||||
@@ -298,7 +458,19 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
link_spheres_tensor,
|
||||
)
|
||||
|
||||
def get_state(self, q, link_name=None, calculate_jacobian=False) -> CudaRobotModelState:
|
||||
def get_state(
|
||||
self, q: torch.Tensor, link_name: str = None, calculate_jacobian: bool = False
|
||||
) -> CudaRobotModelState:
|
||||
"""Get kinematic state of the robot by computing forward kinematics.
|
||||
|
||||
Args:
|
||||
q: Joint configuration of the robot. Shape should be [batch_size, dof].
|
||||
link_name: Name of link to return pose of. If None, returns end-effector pose.
|
||||
calculate_jacobian: Calculate jacobian of the robot. Not supported.
|
||||
|
||||
Returns:
|
||||
CudaRobotModelState: Kinematic state of the robot.
|
||||
"""
|
||||
out = self.forward(q, link_name, calculate_jacobian)
|
||||
state = CudaRobotModelState(
|
||||
out[0],
|
||||
@@ -312,12 +484,46 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
)
|
||||
return state
|
||||
|
||||
def get_robot_link_meshes(self):
|
||||
def compute_kinematics(
|
||||
self, js: JointState, link_name: Optional[str] = None, calculate_jacobian: bool = False
|
||||
) -> CudaRobotModelState:
|
||||
"""Compute forward kinematics of the robot.
|
||||
|
||||
Args:
|
||||
js: Joint state of robot.
|
||||
link_name: Name of link to return pose of. If None, returns end-effector pose.
|
||||
calculate_jacobian: Calculate jacobian of the robot. Not supported.
|
||||
|
||||
|
||||
Returns:
|
||||
CudaRobotModelState: Kinematic state of the robot.
|
||||
|
||||
"""
|
||||
if js.joint_names is not None:
|
||||
if js.joint_names != self.kinematics_config.joint_names:
|
||||
log_error("Joint names do not match, reoder joints before forward kinematics")
|
||||
|
||||
return self.get_state(js.position, link_name, calculate_jacobian)
|
||||
|
||||
def get_robot_link_meshes(self) -> List[Mesh]:
|
||||
"""Get meshes of all links of the robot.
|
||||
|
||||
Returns:
|
||||
List[Mesh]: List of all link meshes.
|
||||
"""
|
||||
m_list = [self.get_link_mesh(l) for l in self.kinematics_config.mesh_link_names]
|
||||
|
||||
return m_list
|
||||
|
||||
def get_robot_as_mesh(self, q: torch.Tensor):
|
||||
def get_robot_as_mesh(self, q: torch.Tensor) -> List[Mesh]:
|
||||
"""Transform robot links to Cartesian poses using forward kinematics and return as meshes.
|
||||
|
||||
Args:
|
||||
q: Joint configuration of the robot, shape should be [1, dof].
|
||||
|
||||
Returns:
|
||||
List[Mesh]: List of all link meshes.
|
||||
"""
|
||||
# get all link meshes:
|
||||
m_list = self.get_robot_link_meshes()
|
||||
pose = self.get_link_poses(q, self.kinematics_config.mesh_link_names)
|
||||
@@ -328,7 +534,16 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
|
||||
return m_list
|
||||
|
||||
def get_robot_as_spheres(self, q: torch.Tensor, filter_valid: bool = True):
|
||||
def get_robot_as_spheres(self, q: torch.Tensor, filter_valid: bool = True) -> List[Sphere]:
|
||||
"""Get robot spheres using forward kinematics on given joint configuration q.
|
||||
|
||||
Args:
|
||||
q: Joint configuration of the robot, shape should be [1, dof].
|
||||
filter_valid: Filter out spheres with radius <= 0.
|
||||
|
||||
Returns:
|
||||
List[Sphere]: List of all robot spheres.
|
||||
"""
|
||||
state = self.get_state(q)
|
||||
|
||||
# state has sphere position and radius
|
||||
@@ -361,6 +576,18 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
return sph_traj
|
||||
|
||||
def get_link_poses(self, q: torch.Tensor, link_names: List[str]) -> Pose:
|
||||
"""Get Pose of links at given joint configuration q using forward kinematics.
|
||||
|
||||
Note that only the links specified in :var:`~link_names` are returned.
|
||||
|
||||
Args:
|
||||
q: Joint configuration of the robot, shape should be [batch_size, dof].
|
||||
link_names: Names of links to get pose of. This should be a subset of
|
||||
:var:`~link_names`.
|
||||
|
||||
Returns:
|
||||
Pose: Poses of links at given joint configuration.
|
||||
"""
|
||||
state = self.get_state(q)
|
||||
position = torch.zeros(
|
||||
(q.shape[0], len(link_names), 3),
|
||||
@@ -379,7 +606,15 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
quaternion[:, li, :] = state.links_quaternion[:, i, :]
|
||||
return Pose(position=position, quaternion=quaternion)
|
||||
|
||||
def _cuda_forward(self, q):
|
||||
def _cuda_forward(self, q: torch.Tensor) -> Tuple[Tensor, Tensor, Tensor]:
|
||||
"""Compute forward kinematics on GPU. Use :func:`~get_state` or :func:`~forward` instead.
|
||||
|
||||
Args:
|
||||
q: Joint configuration of the robot, shape should be [batch_size, dof].
|
||||
|
||||
Returns:
|
||||
Tuple[Tensor, Tensor, Tensor]: Link positions, link quaternions, link
|
||||
"""
|
||||
link_pos, link_quat, robot_spheres = get_cuda_kinematics(
|
||||
self._link_pos_seq,
|
||||
self._link_quat_seq,
|
||||
@@ -401,22 +636,36 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
return link_pos, link_quat, robot_spheres
|
||||
|
||||
@property
|
||||
def all_articulated_joint_names(self):
|
||||
def all_articulated_joint_names(self) -> List[str]:
|
||||
"""Names of all articulated joints of the robot."""
|
||||
return self.kinematics_config.non_fixed_joint_names
|
||||
|
||||
def get_self_collision_config(self) -> SelfCollisionKinematicsConfig:
|
||||
"""Get self collision configuration parameters of the robot."""
|
||||
return self.self_collision_config
|
||||
|
||||
def get_link_mesh(self, link_name: str) -> Mesh:
|
||||
"""Get mesh of a link of the robot."""
|
||||
mesh = self.kinematics_parser.get_link_mesh(link_name)
|
||||
return mesh
|
||||
|
||||
def get_link_transform(self, link_name: str) -> Pose:
|
||||
mat = self.kinematics_config.fixed_transforms[self.kinematics_config.link_name_to_idx_map[link_name]]
|
||||
"""Get pose offset of a link from it's parent joint.
|
||||
|
||||
Args:
|
||||
link_name: Name of link to get pose of.
|
||||
|
||||
Returns:
|
||||
Pose: Pose of the link.
|
||||
"""
|
||||
mat = self.kinematics_config.fixed_transforms[
|
||||
self.kinematics_config.link_name_to_idx_map[link_name]
|
||||
]
|
||||
pose = Pose(position=mat[:3, 3], rotation=mat[:3, :3])
|
||||
return pose
|
||||
|
||||
def get_all_link_transforms(self) -> Pose:
|
||||
"""Get offset pose of all links with respect to their parent joint."""
|
||||
pose = Pose(
|
||||
self.kinematics_config.fixed_transforms[:, :3, 3],
|
||||
rotation=self.kinematics_config.fixed_transforms[:, :3, :3],
|
||||
@@ -424,32 +673,59 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
return pose
|
||||
|
||||
def get_dof(self) -> int:
|
||||
"""Get degrees of freedom of the robot."""
|
||||
return self.kinematics_config.n_dof
|
||||
|
||||
@property
|
||||
def dof(self) -> int:
|
||||
"""Degrees of freedom of the robot."""
|
||||
return self.kinematics_config.n_dof
|
||||
|
||||
@property
|
||||
def joint_names(self) -> List[str]:
|
||||
"""Names of actuated joints."""
|
||||
return self.kinematics_config.joint_names
|
||||
|
||||
@property
|
||||
def total_spheres(self) -> int:
|
||||
"""Number of spheres used to approximate robot geometry."""
|
||||
return self.kinematics_config.total_spheres
|
||||
|
||||
@property
|
||||
def lock_jointstate(self):
|
||||
def lock_jointstate(self) -> JointState:
|
||||
"""State of joints that are locked in the kinematic representation."""
|
||||
return self.kinematics_config.lock_jointstate
|
||||
|
||||
def get_full_js(self, js: JointState):
|
||||
def get_full_js(self, js: JointState) -> JointState:
|
||||
"""Get state of all joints, including locked joints.
|
||||
|
||||
This function will not provide state of mimic joints. If you need mimic joints, use
|
||||
:func:`~get_mimic_js`.
|
||||
|
||||
Args:
|
||||
js: State containing articulated joints.
|
||||
|
||||
Returns:
|
||||
JointState: State of all joints.
|
||||
"""
|
||||
all_joint_names = self.all_articulated_joint_names
|
||||
lock_joint_state = self.lock_jointstate
|
||||
|
||||
new_js = js.get_augmented_joint_state(all_joint_names, lock_joint_state)
|
||||
return new_js
|
||||
|
||||
def get_mimic_js(self, js: JointState):
|
||||
def get_mimic_js(self, js: JointState) -> JointState:
|
||||
"""Get state of mimic joints from active joints.
|
||||
|
||||
Current implementation uses a for loop over joints to calculate the state. This can be
|
||||
optimized by using a custom CUDA kernel or a matrix multiplication.
|
||||
|
||||
Args:
|
||||
js: State containing articulated joints.
|
||||
|
||||
Returns:
|
||||
JointState: State of active, locked, and mimic joints.
|
||||
"""
|
||||
if self.kinematics_config.mimic_joints is None:
|
||||
return None
|
||||
extra_joints = {"position": [], "joint_names": []}
|
||||
@@ -467,21 +743,118 @@ class CudaRobotModel(CudaRobotModelConfig):
|
||||
new_js = js.get_augmented_joint_state(js.joint_names + extra_js.joint_names, extra_js)
|
||||
return new_js
|
||||
|
||||
def update_kinematics_config(self, new_kin_config: KinematicsTensorConfig):
|
||||
"""Update kinematics representation of the robot.
|
||||
|
||||
A kinematics representation can be updated with new parameters. Some parameters that could
|
||||
require updating are state of locked joints, when a robot grasps an object. Another instance
|
||||
is when using different planners for different parts of the robot, example updating the
|
||||
state of robot base or another arm. Updations should result in the same tensor dimensions,
|
||||
if not then the instance of this class requires reinitialization.
|
||||
|
||||
Args:
|
||||
new_kin_config: New kinematics representation of the robot.
|
||||
"""
|
||||
|
||||
self.kinematics_config.copy_(new_kin_config)
|
||||
|
||||
def attach_external_objects_to_robot(
|
||||
self,
|
||||
joint_state: JointState,
|
||||
external_objects: List[Obstacle],
|
||||
surface_sphere_radius: float = 0.001,
|
||||
link_name: str = "attached_object",
|
||||
sphere_fit_type: SphereFitType = SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE,
|
||||
voxelize_method: str = "ray",
|
||||
world_objects_pose_offset: Optional[Pose] = None,
|
||||
) -> bool:
|
||||
"""Attach external objects to a robot's link. See :ref:`attach_object_note` for details.
|
||||
|
||||
Args:
|
||||
joint_state: Joint state of the robot.
|
||||
external_objects: List of external objects to attach to the robot.
|
||||
surface_sphere_radius: Radius (in meters) to use for points sampled on surface of the
|
||||
object. A smaller radius will allow for generating motions very close to obstacles.
|
||||
link_name: Name of the link (frame) to attach the objects to. The assumption is that
|
||||
this link does not have any geometry and all spheres of this link represent
|
||||
attached objects.
|
||||
sphere_fit_type: Sphere fit algorithm to use. See :ref:`attach_object_note` for more
|
||||
details. The default method :attr:`SphereFitType.VOXEL_VOLUME_SAMPLE_SURFACE`
|
||||
voxelizes the volume of the objects and adds spheres representing the voxels, then
|
||||
samples points on the surface of the object, adds :attr:`surface_sphere_radius` to
|
||||
these points. This should be used for most cases.
|
||||
voxelize_method: Method to use for voxelization, passed to
|
||||
:py:func:`trimesh.voxel.creation.voxelize`.
|
||||
world_objects_pose_offset: Offset to apply to the object poses before attaching to the
|
||||
robot. This is useful when attaching an object that's in contact with the world.
|
||||
The offset is applied in the world frame before attaching to the robot.
|
||||
"""
|
||||
log_info("Attach objects to robot")
|
||||
if len(external_objects) == 0:
|
||||
log_error("no object in external_objects")
|
||||
kin_state = self.compute_kinematics(joint_state)
|
||||
ee_pose = kin_state.ee_pose # w_T_ee
|
||||
if world_objects_pose_offset is not None:
|
||||
# add offset from ee:
|
||||
ee_pose = world_objects_pose_offset.inverse().multiply(ee_pose)
|
||||
# new ee_pose:
|
||||
# w_T_ee = offset_T_w * w_T_ee
|
||||
# ee_T_w
|
||||
ee_pose = ee_pose.inverse() # ee_T_w to multiply all objects later
|
||||
max_spheres = self.kinematics_config.get_number_of_spheres(link_name)
|
||||
object_names = [x.name for x in external_objects]
|
||||
n_spheres = int(max_spheres / len(object_names))
|
||||
sphere_tensor = torch.zeros((max_spheres, 4))
|
||||
sphere_tensor[:, 3] = -10.0
|
||||
sph_list = []
|
||||
if n_spheres == 0:
|
||||
log_warn(
|
||||
"No spheres found, max_spheres: "
|
||||
+ str(max_spheres)
|
||||
+ " n_objects: "
|
||||
+ str(len(object_names))
|
||||
)
|
||||
return False
|
||||
for i, x in enumerate(object_names):
|
||||
obs = external_objects[i]
|
||||
sph = obs.get_bounding_spheres(
|
||||
n_spheres,
|
||||
surface_sphere_radius,
|
||||
pre_transform_pose=ee_pose,
|
||||
tensor_args=self.tensor_args,
|
||||
fit_type=sphere_fit_type,
|
||||
voxelize_method=voxelize_method,
|
||||
)
|
||||
sph_list += [s.position + [s.radius] for s in sph]
|
||||
|
||||
log_info("MG: Computed spheres for attach objects to robot")
|
||||
|
||||
spheres = self.tensor_args.to_device(torch.as_tensor(sph_list))
|
||||
|
||||
if spheres.shape[0] > max_spheres:
|
||||
spheres = spheres[: spheres.shape[0]]
|
||||
sphere_tensor[: spheres.shape[0], :] = spheres.contiguous()
|
||||
|
||||
self.kinematics_config.attach_object(sphere_tensor=sphere_tensor, link_name=link_name)
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def ee_link(self):
|
||||
def ee_link(self) -> str:
|
||||
"""End-effector link of the robot. Changing requires reinitializing this class."""
|
||||
return self.kinematics_config.ee_link
|
||||
|
||||
@property
|
||||
def base_link(self):
|
||||
def base_link(self) -> str:
|
||||
"""Base link of the robot. Changing requires reinitializing this class."""
|
||||
return self.kinematics_config.base_link
|
||||
|
||||
@property
|
||||
def robot_spheres(self):
|
||||
"""Spheres representing robot geometry."""
|
||||
return self.kinematics_config.link_spheres
|
||||
|
||||
def update_kinematics_config(self, new_kin_config: KinematicsTensorConfig):
|
||||
self.kinematics_config.copy_(new_kin_config)
|
||||
|
||||
@property
|
||||
def retract_config(self):
|
||||
def retract_config(self) -> torch.Tensor:
|
||||
"""Retract configuration of the robot. Use :func:`~joint_names` to get joint names."""
|
||||
return self.kinematics_config.cspace.retract_config
|
||||
|
||||
@@ -8,10 +8,21 @@
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
"""
|
||||
Base module for parsing kinematics from different representations.
|
||||
|
||||
cuRobo provides kinematics parsing from an URDF and a partial implementation for parsing from
|
||||
a USD. To parse from other representations, an user can extend the :class:`~KinematicsParser`
|
||||
class and implement only the abstract methods. Optionally, user can also provide functions for
|
||||
reading meshes, useful for debugging and visualization.
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# Standard Library
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# Third Party
|
||||
import numpy as np
|
||||
@@ -25,6 +36,8 @@ from curobo.types.math import Pose
|
||||
|
||||
@dataclass
|
||||
class LinkParams:
|
||||
"""Parameters of a link in the kinematic tree."""
|
||||
|
||||
link_name: str
|
||||
joint_name: str
|
||||
joint_type: JointType
|
||||
@@ -38,7 +51,15 @@ class LinkParams:
|
||||
mimic_joint_name: Optional[str] = None
|
||||
|
||||
@staticmethod
|
||||
def from_dict(dict_data):
|
||||
def from_dict(dict_data: Dict[str, Any]) -> LinkParams:
|
||||
"""Create a LinkParams object from a dictionary.
|
||||
|
||||
Args:
|
||||
dict_data: Dictionary containing link parameters.
|
||||
|
||||
Returns:
|
||||
LinkParams: Link parameters object.
|
||||
"""
|
||||
dict_data["joint_type"] = JointType[dict_data["joint_type"]]
|
||||
dict_data["fixed_transform"] = (
|
||||
Pose.from_list(dict_data["fixed_transform"], tensor_args=TensorDeviceType())
|
||||
@@ -50,7 +71,21 @@ class LinkParams:
|
||||
|
||||
|
||||
class KinematicsParser:
|
||||
"""
|
||||
Base class for parsing kinematics.
|
||||
|
||||
Implement abstractmethods to parse kinematics from any representation. Optionally, implement
|
||||
methods for reading meshes for visualization and debugging.
|
||||
"""
|
||||
|
||||
def __init__(self, extra_links: Optional[Dict[str, LinkParams]] = None) -> None:
|
||||
"""Initialize the KinematicsParser.
|
||||
|
||||
Args:
|
||||
extra_links: Additional links to be added to the kinematic tree.
|
||||
"""
|
||||
|
||||
#: Parent link for all link in the kinematic tree.
|
||||
self._parent_map = {}
|
||||
self.extra_links = extra_links
|
||||
self.build_link_parent()
|
||||
@@ -59,10 +94,45 @@ class KinematicsParser:
|
||||
for i in self.extra_links:
|
||||
self._parent_map[i] = {"parent": self.extra_links[i].parent_link_name}
|
||||
|
||||
@abstractmethod
|
||||
def build_link_parent(self):
|
||||
"""Build a map of parent links to each link in the kinematic tree.
|
||||
|
||||
NOTE: Use this function to fill self._parent_map.
|
||||
Use this function to fill ``_parent_map``. Check
|
||||
:meth:`curobo.cuda_robot_model.urdf_kinematics_parser.UrdfKinematicsParser.build_link_parent`
|
||||
for an example implementation.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_link_parameters(self, link_name: str, base: bool = False) -> LinkParams:
|
||||
"""Get parameters of a link in the kinematic tree.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link.
|
||||
base: Is this the base link of the robot?
|
||||
|
||||
Returns:
|
||||
LinkParams: Parameters of the link.
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_absolute_path_to_link_meshes(self, mesh_dir: str = ""):
|
||||
"""Add absolute path to link meshes.
|
||||
|
||||
Args:
|
||||
mesh_dir: Absolute path to the directory containing link meshes.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_link_mesh(self, link_name: str) -> Mesh:
|
||||
"""Get mesh of a link.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link.
|
||||
|
||||
Returns:
|
||||
Mesh: Mesh of the link.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -85,18 +155,31 @@ class KinematicsParser:
|
||||
chain_links.reverse()
|
||||
return chain_links
|
||||
|
||||
def get_controlled_joint_names(self) -> List[str]:
|
||||
"""Get names of all controlled joints in the robot.
|
||||
|
||||
Returns:
|
||||
Names of all controlled joints in the robot.
|
||||
"""
|
||||
j_list = []
|
||||
for k in self._parent_map.keys():
|
||||
joint_name = self._parent_map[k]["joint_name"]
|
||||
joint = self._robot.joint_map[joint_name]
|
||||
if joint.type != "fixed" and joint.mimic is None:
|
||||
j_list.append(joint_name)
|
||||
return j_list
|
||||
|
||||
def _get_from_extra_links(self, link_name: str) -> LinkParams:
|
||||
"""Get link parameters for extra links.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link.
|
||||
|
||||
Returns:
|
||||
LinkParams: Link parameters if found, else None.
|
||||
"""
|
||||
if self.extra_links is None:
|
||||
return None
|
||||
if link_name in self.extra_links.keys():
|
||||
return self.extra_links[link_name]
|
||||
return None
|
||||
|
||||
def get_link_parameters(self, link_name: str, base: bool = False) -> LinkParams:
|
||||
pass
|
||||
|
||||
def add_absolute_path_to_link_meshes(self, mesh_dir: str = ""):
|
||||
pass
|
||||
|
||||
def get_link_mesh(self, link_name: str) -> Mesh:
|
||||
pass
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
"""Common structures used for Kinematics are defined in this module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Standard Library
|
||||
@@ -28,33 +30,87 @@ from curobo.util.tensor_util import clone_if_not_none, copy_if_not_none
|
||||
|
||||
|
||||
class JointType(Enum):
|
||||
"""Type of Joint. Arbitrary axis of change is not supported."""
|
||||
|
||||
#: Fixed joint.
|
||||
FIXED = -1
|
||||
|
||||
#: Prismatic joint along x-axis.
|
||||
X_PRISM = 0
|
||||
|
||||
#: Prismatic joint along y-axis.
|
||||
Y_PRISM = 1
|
||||
|
||||
#: Prismatic joint along z-axis.
|
||||
Z_PRISM = 2
|
||||
|
||||
#: Revolute joint along x-axis.
|
||||
X_ROT = 3
|
||||
|
||||
#: Revolute joint along y-axis.
|
||||
Y_ROT = 4
|
||||
|
||||
#: Revolute joint along z-axis.
|
||||
Z_ROT = 5
|
||||
|
||||
#: Prismatic joint along negative x-axis.
|
||||
X_PRISM_NEG = 6
|
||||
|
||||
#: Prismatic joint along negative y-axis.
|
||||
Y_PRISM_NEG = 7
|
||||
|
||||
#: Prismatic joint along negative z-axis.
|
||||
Z_PRISM_NEG = 8
|
||||
|
||||
#: Revolute joint along negative x-axis.
|
||||
X_ROT_NEG = 9
|
||||
|
||||
#: Revolute joint along negative y-axis.
|
||||
Y_ROT_NEG = 10
|
||||
|
||||
#: Revolute joint along negative z-axis.
|
||||
Z_ROT_NEG = 11
|
||||
|
||||
|
||||
@dataclass
|
||||
class JointLimits:
|
||||
"""Joint limits for a robot."""
|
||||
|
||||
#: Names of the joints. All tensors are indexed by joint names.
|
||||
joint_names: List[str]
|
||||
|
||||
#: Position limits for each joint. Shape [n_joints, 2] with columns having [min, max] values.
|
||||
position: torch.Tensor
|
||||
|
||||
#: Velocity limits for each joint. Shape [n_joints, 2] with columns having [min, max] values.
|
||||
velocity: torch.Tensor
|
||||
|
||||
#: Acceleration limits for each joint. Shape [n_joints, 2] with columns having [min, max]
|
||||
#: values.
|
||||
acceleration: torch.Tensor
|
||||
|
||||
#: Jerk limits for each joint. Shape [n_joints, 2] with columns having [min, max] values.
|
||||
jerk: torch.Tensor
|
||||
|
||||
#: Effort limits for each joint. This is not used.
|
||||
effort: Optional[torch.Tensor] = None
|
||||
|
||||
#: Device and floating point precision for tensors.
|
||||
tensor_args: TensorDeviceType = TensorDeviceType()
|
||||
|
||||
@staticmethod
|
||||
def from_data_dict(data: Dict, tensor_args: TensorDeviceType = TensorDeviceType()):
|
||||
def from_data_dict(
|
||||
data: Dict, tensor_args: TensorDeviceType = TensorDeviceType()
|
||||
) -> JointLimits:
|
||||
"""Create JointLimits from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing joint limits. E.g., {"position": [0, 1], ...}.
|
||||
tensor_args: Device and floating point precision for tensors.
|
||||
|
||||
Returns:
|
||||
JointLimits: Joint limits instance.
|
||||
"""
|
||||
p = tensor_args.to_device(data["position"])
|
||||
v = tensor_args.to_device(data["velocity"])
|
||||
a = tensor_args.to_device(data["acceleration"])
|
||||
@@ -66,6 +122,7 @@ class JointLimits:
|
||||
return JointLimits(data["joint_names"], p, v, a, j, e)
|
||||
|
||||
def clone(self) -> JointLimits:
|
||||
"""Clone joint limits."""
|
||||
return JointLimits(
|
||||
self.joint_names.copy(),
|
||||
self.position.clone(),
|
||||
@@ -76,7 +133,15 @@ class JointLimits:
|
||||
self.tensor_args,
|
||||
)
|
||||
|
||||
def copy_(self, new_jl: JointLimits):
|
||||
def copy_(self, new_jl: JointLimits) -> JointLimits:
|
||||
"""Copy joint limits from another instance. This maintains reference and copies the data.
|
||||
|
||||
Args:
|
||||
new_jl: JointLimits instance to copy from.
|
||||
|
||||
Returns:
|
||||
JointLimits: Data copied joint limits.
|
||||
"""
|
||||
self.joint_names = new_jl.joint_names.copy()
|
||||
self.position.copy_(new_jl.position)
|
||||
self.velocity.copy_(new_jl.velocity)
|
||||
@@ -87,19 +152,53 @@ class JointLimits:
|
||||
|
||||
@dataclass
|
||||
class CSpaceConfig:
|
||||
"""Configuration space parameters of the robot."""
|
||||
|
||||
#: Names of the joints.
|
||||
joint_names: List[str]
|
||||
|
||||
#: Retract configuration for the robot. This is the configuration used to bias graph search
|
||||
#: and also regularize inverse kinematics. This configuration is also used to initialize
|
||||
#: the robot during warmup phase of an optimizer. Set this to a collision-free configuration
|
||||
#: for good performance. When this configuration is in collision, it's not used to bias
|
||||
#: graph search.
|
||||
retract_config: Optional[T_DOF] = None
|
||||
|
||||
#: Weight for each joint in configuration space. Used to measure distance between nodes in
|
||||
#: graph search-based planning.
|
||||
cspace_distance_weight: Optional[T_DOF] = None
|
||||
|
||||
#: Weight for each joint, used in regularization cost term for inverse kinematics.
|
||||
null_space_weight: Optional[T_DOF] = None
|
||||
|
||||
#: Device and floating point precision for tensors.
|
||||
tensor_args: TensorDeviceType = TensorDeviceType()
|
||||
|
||||
#: Maximum acceleration for each joint. Accepts a scalar or a list of values for each joint.
|
||||
max_acceleration: Union[float, List[float]] = 10.0
|
||||
|
||||
#: Maximum jerk for each joint. Accepts a scalar or a list of values for each joint.
|
||||
max_jerk: Union[float, List[float]] = 500.0
|
||||
|
||||
#: Velocity scale for each joint. Accepts a scalar or a list of values for each joint.
|
||||
#: This is used to scale the velocity limits for each joint.
|
||||
velocity_scale: Union[float, List[float]] = 1.0
|
||||
|
||||
#: Acceleration scale for each joint. Accepts a scalar or a list of values for each joint.
|
||||
#: This is used to scale the acceleration limits for each joint.
|
||||
acceleration_scale: Union[float, List[float]] = 1.0
|
||||
|
||||
#: Jerk scale for each joint. Accepts a scalar or a list of values for each joint.
|
||||
#: This is used to scale the jerk limits for each joint.
|
||||
jerk_scale: Union[float, List[float]] = 1.0
|
||||
|
||||
#: Position limit clip value. This is used to clip the position limits for each joint.
|
||||
#: Accepts a scalar or a list of values for each joint. This is useful to truncate limits
|
||||
#: to account for any safety margins imposed by real robot controllers.
|
||||
position_limit_clip: Union[float, List[float]] = 0.0
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post initialization checks and data transfer to device tensors."""
|
||||
if self.retract_config is not None:
|
||||
self.retract_config = self.tensor_args.to_device(self.retract_config)
|
||||
if self.cspace_distance_weight is not None:
|
||||
@@ -139,6 +238,8 @@ class CSpaceConfig:
|
||||
self.acceleration_scale = self.tensor_args.to_device(self.acceleration_scale)
|
||||
if isinstance(self.jerk_scale, List):
|
||||
self.jerk_scale = self.tensor_args.to_device(self.jerk_scale)
|
||||
if isinstance(self.position_limit_clip, List):
|
||||
self.position_limit_clip = self.tensor_args.to_device(self.position_limit_clip)
|
||||
# check shapes:
|
||||
if self.retract_config is not None:
|
||||
dof = self.retract_config.shape
|
||||
@@ -148,6 +249,12 @@ class CSpaceConfig:
|
||||
log_error("null_space_weight shape does not match retract_config")
|
||||
|
||||
def inplace_reindex(self, joint_names: List[str]):
|
||||
"""Change order of joints in configuration space tensors to match given order of names.
|
||||
|
||||
Args:
|
||||
joint_names: New order of joint names.
|
||||
|
||||
"""
|
||||
new_index = [self.joint_names.index(j) for j in joint_names]
|
||||
if self.retract_config is not None:
|
||||
self.retract_config = self.retract_config[new_index].clone()
|
||||
@@ -163,7 +270,18 @@ class CSpaceConfig:
|
||||
joint_names = [self.joint_names[n] for n in new_index]
|
||||
self.joint_names = joint_names
|
||||
|
||||
def copy_(self, new_config: CSpaceConfig):
|
||||
def copy_(self, new_config: CSpaceConfig) -> CSpaceConfig:
|
||||
"""Copy parameters from another instance.
|
||||
|
||||
This maintains reference and copies the data. Assumes that the new instance has the same
|
||||
number of joints as the current instance and also same shape of tensors.
|
||||
|
||||
Args:
|
||||
new_config: New parameters to copy into current instance.
|
||||
|
||||
Returns:
|
||||
CSpaceConfig: Same instance of cspace configuration which has updated parameters.
|
||||
"""
|
||||
self.joint_names = new_config.joint_names.copy()
|
||||
self.retract_config = copy_if_not_none(new_config.retract_config, self.retract_config)
|
||||
self.null_space_weight = copy_if_not_none(
|
||||
@@ -183,6 +301,8 @@ class CSpaceConfig:
|
||||
return self
|
||||
|
||||
def clone(self) -> CSpaceConfig:
|
||||
"""Clone configuration space parameters."""
|
||||
|
||||
return CSpaceConfig(
|
||||
joint_names=self.joint_names.copy(),
|
||||
retract_config=clone_if_not_none(self.retract_config),
|
||||
@@ -194,9 +314,22 @@ class CSpaceConfig:
|
||||
velocity_scale=self.velocity_scale.clone(),
|
||||
acceleration_scale=self.acceleration_scale.clone(),
|
||||
jerk_scale=self.jerk_scale.clone(),
|
||||
position_limit_clip=(
|
||||
self.position_limit_clip.clone()
|
||||
if isinstance(self.position_limit_clip, torch.Tensor)
|
||||
else self.position_limit_clip
|
||||
),
|
||||
)
|
||||
|
||||
def scale_joint_limits(self, joint_limits: JointLimits):
|
||||
def scale_joint_limits(self, joint_limits: JointLimits) -> JointLimits:
|
||||
"""Scale joint limits by the given scale factors.
|
||||
|
||||
Args:
|
||||
joint_limits: Joint limits to scale.
|
||||
|
||||
Returns:
|
||||
JointLimits: Scaled joint limits.
|
||||
"""
|
||||
if self.velocity_scale is not None:
|
||||
joint_limits.velocity = joint_limits.velocity * self.velocity_scale
|
||||
if self.acceleration_scale is not None:
|
||||
@@ -212,7 +345,20 @@ class CSpaceConfig:
|
||||
joint_position_lower: torch.Tensor,
|
||||
joint_names: List[str],
|
||||
tensor_args: TensorDeviceType = TensorDeviceType(),
|
||||
):
|
||||
) -> CSpaceConfig:
|
||||
"""Load CSpace configuration from joint limits.
|
||||
|
||||
Args:
|
||||
joint_position_upper: Upper position limits for each joint.
|
||||
joint_position_lower: Lower position limits for each joint.
|
||||
joint_names: Names of the joints. This should match the order of joints in the upper
|
||||
and lower limits.
|
||||
tensor_args: Device and floating point precision for tensors.
|
||||
|
||||
Returns:
|
||||
CSpaceConfig: CSpace configuration with retract configuration set to the middle of the
|
||||
joint limits and all weights set to 1.
|
||||
"""
|
||||
retract_config = ((joint_position_upper + joint_position_lower) / 2).flatten()
|
||||
n_dof = retract_config.shape[-1]
|
||||
null_space_weight = torch.ones(n_dof, **(tensor_args.as_torch_dict()))
|
||||
@@ -238,25 +384,26 @@ class KinematicsTensorConfig:
|
||||
#: Static Homogenous Transform from parent link to child link for all links [n_links,4,4].
|
||||
fixed_transforms: torch.Tensor
|
||||
|
||||
#: index of fixed_transform given link index [n_links].
|
||||
#: Index of fixed_transform given link index [n_links].
|
||||
link_map: torch.Tensor
|
||||
|
||||
#: joint index given link index [n_links].
|
||||
#: Joint index given link index [n_links].
|
||||
joint_map: torch.Tensor
|
||||
|
||||
#: type of joint given link index [n_links].
|
||||
#: Type of joint given link index [n_links].
|
||||
joint_map_type: torch.Tensor
|
||||
|
||||
#: Joint offset to store scalars for mimic joints and negative axis joints.
|
||||
joint_offset_map: torch.Tensor
|
||||
|
||||
#: index of link to write out pose [n_store_links].
|
||||
#: Index of link to write out pose [n_store_links].
|
||||
store_link_map: torch.Tensor
|
||||
|
||||
#: Mapping between each link to every other link, this is used to check
|
||||
#: if a link is part of a serial chain formed by another link [n_links, n_links].
|
||||
link_chain_map: torch.Tensor
|
||||
|
||||
#: Name of links whose pose will be stored [n_store_links].
|
||||
#: Name of links to compute pose during kinematics computation [n_store_links].
|
||||
link_names: List[str]
|
||||
|
||||
#: Joint limits
|
||||
@@ -274,31 +421,32 @@ class KinematicsTensorConfig:
|
||||
#: Name of all actuated joints.
|
||||
joint_names: Optional[List[str]] = None
|
||||
|
||||
#:
|
||||
#: Name of joints to lock to a fixed value along with the locked value
|
||||
lock_jointstate: Optional[JointState] = None
|
||||
|
||||
#:
|
||||
#: Joints that mimic other joints. This will be populated by :class:~`CudaRobotGenerator`
|
||||
# when parsing the kinematics of the robot.
|
||||
mimic_joints: Optional[dict] = None
|
||||
|
||||
#:
|
||||
#: Sphere representation of the robot's geometry. This is used for collision detection.
|
||||
link_spheres: Optional[torch.Tensor] = None
|
||||
|
||||
#:
|
||||
#: Mapping of link index to sphere index. This is used to get spheres for a link.
|
||||
link_sphere_idx_map: Optional[torch.Tensor] = None
|
||||
|
||||
#:
|
||||
#: Mapping of link name to link index. This is used to get link index from link name.
|
||||
link_name_to_idx_map: Optional[Dict[str, int]] = None
|
||||
|
||||
#: total number of spheres that represent the robot's geometry.
|
||||
#: Total number of spheres that represent the robot's geometry.
|
||||
total_spheres: int = 0
|
||||
|
||||
#: Additional debug parameters.
|
||||
debug: Optional[Any] = None
|
||||
|
||||
#: index of end-effector in stored link poses.
|
||||
#: Index of end-effector in stored link poses.
|
||||
ee_idx: int = 0
|
||||
|
||||
#: Cspace configuration
|
||||
#: Cspace parameters for the robot.
|
||||
cspace: Optional[CSpaceConfig] = None
|
||||
|
||||
#: Name of base link. This is the root link from which all kinematic parameters were computed.
|
||||
@@ -312,6 +460,7 @@ class KinematicsTensorConfig:
|
||||
reference_link_spheres: Optional[torch.Tensor] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post initialization checks and data transfer to device tensors."""
|
||||
if self.cspace is None and self.joint_limits is not None:
|
||||
self.load_cspace_cfg_from_kinematics()
|
||||
if self.joint_limits is not None and self.cspace is not None:
|
||||
@@ -320,6 +469,18 @@ class KinematicsTensorConfig:
|
||||
self.reference_link_spheres = self.link_spheres.clone()
|
||||
|
||||
def copy_(self, new_config: KinematicsTensorConfig) -> KinematicsTensorConfig:
|
||||
"""Copy parameters from another instance into current instance.
|
||||
|
||||
This maintains reference and copies the data. Assumes that the new instance has the same
|
||||
number of joints as the current instance and also same shape of tensors.
|
||||
|
||||
Args:
|
||||
new_config: New parameters to copy into current instance.
|
||||
|
||||
Returns:
|
||||
KinematicsTensorConfig: Same instance of kinematics configuration which has updated
|
||||
parameters.
|
||||
"""
|
||||
self.fixed_transforms.copy_(new_config.fixed_transforms)
|
||||
self.link_map.copy_(new_config.link_map)
|
||||
self.joint_map.copy_(new_config.joint_map)
|
||||
@@ -350,10 +511,17 @@ class KinematicsTensorConfig:
|
||||
self.link_names = new_config.link_names
|
||||
self.mesh_link_names = new_config.mesh_link_names
|
||||
self.total_spheres = new_config.total_spheres
|
||||
if self.lock_jointstate is not None and new_config.lock_jointstate is not None:
|
||||
self.lock_jointstate.copy_(new_config.lock_jointstate)
|
||||
self.mimic_joints = new_config.mimic_joints
|
||||
|
||||
return self
|
||||
|
||||
def load_cspace_cfg_from_kinematics(self):
|
||||
"""Load CSpace configuration from joint limits.
|
||||
|
||||
This sets the retract configuration to the middle of the joint limits and all weights to 1.
|
||||
"""
|
||||
retract_config = (
|
||||
(self.joint_limits.position[1] + self.joint_limits.position[0]) / 2
|
||||
).flatten()
|
||||
@@ -371,22 +539,27 @@ class KinematicsTensorConfig:
|
||||
)
|
||||
|
||||
def get_sphere_index_from_link_name(self, link_name: str) -> torch.Tensor:
|
||||
"""Get indices of spheres for a link.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Indices of spheres for the link.
|
||||
"""
|
||||
link_idx = self.link_name_to_idx_map[link_name]
|
||||
# get sphere index for this link:
|
||||
link_spheres_idx = torch.nonzero(self.link_sphere_idx_map == link_idx).view(-1)
|
||||
return link_spheres_idx
|
||||
|
||||
def update_link_spheres(
|
||||
self, link_name: str, sphere_position_radius: torch.Tensor, start_sph_idx: int = 0
|
||||
):
|
||||
"""Update sphere parameters
|
||||
|
||||
NOTE: This currently does not update self collision distances.
|
||||
"""Update sphere parameters of a specific link given by name.
|
||||
|
||||
Args:
|
||||
link_name: _description_
|
||||
sphere_position_radius: _description_
|
||||
start_sph_idx: _description_. Defaults to 0.
|
||||
link_name: Name of the link.
|
||||
sphere_position_radius: Tensor of shape [n_spheres, 4] with columns [x, y, z, r].
|
||||
start_sph_idx: If providing a subset of spheres, this is the starting index.
|
||||
"""
|
||||
# get sphere indices from link name:
|
||||
link_sphere_index = self.get_sphere_index_from_link_name(link_name)[
|
||||
@@ -398,14 +571,31 @@ class KinematicsTensorConfig:
|
||||
def get_link_spheres(
|
||||
self,
|
||||
link_name: str,
|
||||
):
|
||||
) -> torch.Tensor:
|
||||
"""Get spheres of a link.
|
||||
|
||||
Args:
|
||||
link_name: Name of link.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Spheres of the link with shape [n_spheres, 4] with columns [x, y, z, r].
|
||||
"""
|
||||
link_sphere_index = self.get_sphere_index_from_link_name(link_name)
|
||||
return self.link_spheres[link_sphere_index, :]
|
||||
|
||||
def get_reference_link_spheres(
|
||||
self,
|
||||
link_name: str,
|
||||
):
|
||||
) -> torch.Tensor:
|
||||
"""Get link spheres from the original robot configuration data before any modifications.
|
||||
|
||||
Args:
|
||||
link_name: Name of link.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Spheres of the link with shape [n_spheres, 4] with columns [x, y, z, r].
|
||||
"""
|
||||
|
||||
link_sphere_index = self.get_sphere_index_from_link_name(link_name)
|
||||
return self.reference_link_spheres[link_sphere_index, :]
|
||||
|
||||
@@ -415,47 +605,45 @@ class KinematicsTensorConfig:
|
||||
sphere_tensor: Optional[torch.Tensor] = None,
|
||||
link_name: str = "attached_object",
|
||||
) -> bool:
|
||||
"""_summary_
|
||||
"""Attach object approximated by spheres to a link of the robot.
|
||||
|
||||
This function updates the sphere parameters of the link to represent the attached object.
|
||||
|
||||
Args:
|
||||
sphere_radius: _description_. Defaults to None.
|
||||
sphere_tensor: _description_. Defaults to None.
|
||||
link_name: _description_. Defaults to "attached_object".
|
||||
|
||||
Raises:
|
||||
ValueError: _description_
|
||||
ValueError: _description_
|
||||
sphere_radius: Radius to change for existing spheres. If changing position of spheres
|
||||
as well, then set this to None.
|
||||
sphere_tensor: New sphere tensor to replace existing spheres. Shape [n_spheres, 4] with
|
||||
columns [x, y, z, r]. If changing only radius, set this to None and use
|
||||
sphere_radius.
|
||||
link_name: Name of the link to attach object to. Defaults to "attached_object".
|
||||
|
||||
Returns:
|
||||
_description_
|
||||
bool: True if successful.
|
||||
"""
|
||||
if link_name not in self.link_name_to_idx_map.keys():
|
||||
raise ValueError(link_name + " not found in spheres")
|
||||
log_error(link_name + " not found in spheres")
|
||||
curr_spheres = self.get_link_spheres(link_name)
|
||||
|
||||
if sphere_radius is not None:
|
||||
curr_spheres[:, 3] = sphere_radius
|
||||
if sphere_tensor is not None:
|
||||
if sphere_tensor.shape != curr_spheres.shape and sphere_tensor.shape[0] != 1:
|
||||
raise ValueError("sphere_tensor shape does not match current spheres")
|
||||
log_error("sphere_tensor shape does not match current spheres")
|
||||
curr_spheres[:, :] = sphere_tensor
|
||||
self.update_link_spheres(link_name, curr_spheres)
|
||||
return True
|
||||
|
||||
def detach_object(self, link_name: str = "attached_object") -> bool:
|
||||
"""Detach object from robot end-effector
|
||||
"""Detach object spheres from a link by setting all spheres to zero with negative radius.
|
||||
|
||||
Args:
|
||||
link_name: _description_. Defaults to "attached_object".
|
||||
|
||||
Raises:
|
||||
ValueError: _description_
|
||||
link_name: Name of the link to detach object from.
|
||||
|
||||
Returns:
|
||||
_description_
|
||||
bool: True if successful.
|
||||
"""
|
||||
if link_name not in self.link_name_to_idx_map.keys():
|
||||
raise ValueError(link_name + " not found in spheres")
|
||||
log_error(link_name + " not found in spheres")
|
||||
curr_spheres = self.get_link_spheres(link_name)
|
||||
curr_spheres[:, 3] = -100.0
|
||||
curr_spheres[:, :3] = 0.0
|
||||
@@ -472,15 +660,25 @@ class KinematicsTensorConfig:
|
||||
return self.get_link_spheres(link_name).shape[0]
|
||||
|
||||
def disable_link_spheres(self, link_name: str):
|
||||
"""Disable spheres of a link by setting all spheres to zero with negative radius.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link to disable spheres.
|
||||
"""
|
||||
if link_name not in self.link_name_to_idx_map.keys():
|
||||
raise ValueError(link_name + " not found in spheres")
|
||||
log_error(link_name + " not found in spheres")
|
||||
curr_spheres = self.get_link_spheres(link_name)
|
||||
curr_spheres[:, 3] = -100.0
|
||||
self.update_link_spheres(link_name, curr_spheres)
|
||||
|
||||
def enable_link_spheres(self, link_name: str):
|
||||
"""Enable spheres of a link by resetting to values from initial robot configuration data.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link to enable spheres.
|
||||
"""
|
||||
if link_name not in self.link_name_to_idx_map.keys():
|
||||
raise ValueError(link_name + " not found in spheres")
|
||||
log_error(link_name + " not found in spheres")
|
||||
curr_spheres = self.get_reference_link_spheres(link_name)
|
||||
self.update_link_spheres(link_name, curr_spheres)
|
||||
|
||||
@@ -489,10 +687,29 @@ class KinematicsTensorConfig:
|
||||
class SelfCollisionKinematicsConfig:
|
||||
"""Dataclass that stores self collision attributes to pass to cuda kernel."""
|
||||
|
||||
#: Offset radii for each sphere. This is used to inflate the spheres for self collision
|
||||
#: detection.
|
||||
offset: Optional[torch.Tensor] = None
|
||||
distance_threshold: Optional[torch.Tensor] = None
|
||||
|
||||
#: Sphere index to use for a given thread.
|
||||
thread_location: Optional[torch.Tensor] = None
|
||||
|
||||
#: Maximum number of threads to launch for computing self collision between spheres.
|
||||
thread_max: Optional[int] = None
|
||||
collision_matrix: Optional[torch.Tensor] = None
|
||||
|
||||
#: Distance threshold for self collision detection. This is currently not used.
|
||||
distance_threshold: Optional[torch.Tensor] = None
|
||||
|
||||
#: Two kernel implementations are available. Set this to True to use the experimental kernel
|
||||
#: which is faster. Set this to False to use the collision matrix based kernel which is slower.
|
||||
experimental_kernel: bool = True
|
||||
|
||||
#: Collision matrix containing information about which pair of spheres need to be checked for
|
||||
#: collision. This is only used when experimental_kernel is set to False.
|
||||
collision_matrix: Optional[torch.Tensor] = None
|
||||
|
||||
#: Number of collision checks to perform per thread. Each thread loads a sphere and is allowed
|
||||
#: to check upto checks_per_thread other spheres for collision. Note that all checks have to
|
||||
#: be performed within 1024 threads as shared memory is used. So,
|
||||
# checks_per_thread * n_spheres <= 1024.
|
||||
checks_per_thread: int = 32
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
"""
|
||||
Parses Kinematics from an `URDF <https://wiki.ros.org/urdf>`__ file that describes the
|
||||
kinematic tree of a robot.
|
||||
"""
|
||||
|
||||
# Standard Library
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# Third Party
|
||||
import numpy as np
|
||||
@@ -20,13 +25,14 @@ from lxml import etree
|
||||
from curobo.cuda_robot_model.kinematics_parser import KinematicsParser, LinkParams
|
||||
from curobo.cuda_robot_model.types import JointType
|
||||
from curobo.geom.types import Mesh as CuroboMesh
|
||||
from curobo.types.base import TensorDeviceType
|
||||
from curobo.types.math import Pose
|
||||
from curobo.util.logger import log_error, log_warn
|
||||
from curobo.util_file import join_path
|
||||
|
||||
|
||||
class UrdfKinematicsParser(KinematicsParser):
|
||||
"""Parses Kinematics from an URDF file and provides access to the kinematic tree."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
urdf_path,
|
||||
@@ -35,6 +41,16 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
extra_links: Optional[Dict[str, LinkParams]] = None,
|
||||
build_scene_graph: bool = False,
|
||||
) -> None:
|
||||
"""Initialize instance with URDF file path.
|
||||
|
||||
Args:
|
||||
urdf_path: Path to the URDF file.
|
||||
load_meshes: Load meshes for links from the URDF file.
|
||||
mesh_root: Absolute path to the directory where link meshes are stored.
|
||||
extra_links: Extra links to add to the kinematic tree.
|
||||
build_scene_graph: Build scene graph for the robot. Set this to True if you want to
|
||||
determine the root link of the robot.
|
||||
"""
|
||||
# load robot from urdf:
|
||||
self._robot = yourdfpy.URDF.load(
|
||||
urdf_path,
|
||||
@@ -46,6 +62,7 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
super().__init__(extra_links)
|
||||
|
||||
def build_link_parent(self):
|
||||
"""Build parent map for the robot."""
|
||||
self._parent_map = {}
|
||||
for jid, j in enumerate(self._robot.joint_map):
|
||||
self._parent_map[self._robot.joint_map[j].child] = {
|
||||
@@ -54,11 +71,31 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
"joint_name": j,
|
||||
}
|
||||
|
||||
def _get_joint_name(self, idx):
|
||||
def _get_joint_name(self, idx) -> str:
|
||||
"""Get the name of the joint at the given index.
|
||||
|
||||
Args:
|
||||
idx: Index of the joint.
|
||||
|
||||
Returns:
|
||||
str: Name of the joint.
|
||||
"""
|
||||
joint = self._robot.joint_names[idx]
|
||||
return joint
|
||||
|
||||
def _get_joint_limits(self, joint):
|
||||
def _get_joint_limits(self, joint: yourdfpy.Joint) -> Tuple[Dict[str, float], str]:
|
||||
"""Get the limits of a joint.
|
||||
|
||||
This function converts continuous joints to revolute joints with limits [-6.28, 6.28].
|
||||
|
||||
Args:
|
||||
joint: Instance of the joint.
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, float], str]: Limits of the joint and the type of the joint
|
||||
(revolute or prismatic).
|
||||
"""
|
||||
|
||||
joint_type = joint.type
|
||||
if joint_type != "continuous":
|
||||
joint_limits = {
|
||||
@@ -79,6 +116,16 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
return joint_limits, joint_type
|
||||
|
||||
def get_link_parameters(self, link_name: str, base=False) -> LinkParams:
|
||||
"""Get parameters of a link in the kinematic tree.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link.
|
||||
base: Is this the base link of the robot?
|
||||
|
||||
Returns:
|
||||
LinkParams: Parameters of the link.
|
||||
"""
|
||||
|
||||
link_params = self._get_from_extra_links(link_name)
|
||||
if link_params is not None:
|
||||
return link_params
|
||||
@@ -122,7 +169,7 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
mimic_joint_name = joint_name
|
||||
active_joint_name = joint.mimic.joint
|
||||
active_joint = self._robot.joint_map[active_joint_name]
|
||||
active_joint_limits, active_joint_type = self._get_joint_limits(active_joint)
|
||||
active_joint_limits, _ = self._get_joint_limits(active_joint)
|
||||
# check to make sure mimic joint limits are not larger than active joint:
|
||||
if (
|
||||
active_joint_limits["lower"] * joint_offset[0] + joint_offset[1]
|
||||
@@ -196,6 +243,11 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
return link_params
|
||||
|
||||
def add_absolute_path_to_link_meshes(self, mesh_dir: str = ""):
|
||||
"""Add absolute path to link meshes.
|
||||
|
||||
Args:
|
||||
mesh_dir: Absolute path to the directory containing link meshes.
|
||||
"""
|
||||
# read all link meshes and update their mesh paths by prepending mesh_dir
|
||||
links = self._robot.link_map
|
||||
for k in links.keys():
|
||||
@@ -211,11 +263,21 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
if m is not None:
|
||||
m.filename = join_path(mesh_dir, m.filename)
|
||||
|
||||
def get_urdf_string(self):
|
||||
def get_urdf_string(self) -> str:
|
||||
"""Get the contents of URDF as a string."""
|
||||
txt = etree.tostring(self._robot.write_xml(), method="xml", encoding="unicode")
|
||||
return txt
|
||||
|
||||
def get_link_mesh(self, link_name):
|
||||
def get_link_mesh(self, link_name: str) -> CuroboMesh:
|
||||
"""Get mesh of a link.
|
||||
|
||||
Args:
|
||||
link_name: Name of the link.
|
||||
|
||||
Returns:
|
||||
Mesh: Mesh of the link.
|
||||
"""
|
||||
|
||||
link_data = self._robot.link_map[link_name]
|
||||
|
||||
if len(link_data.visuals) == 0:
|
||||
@@ -237,14 +299,12 @@ class UrdfKinematicsParser(KinematicsParser):
|
||||
)
|
||||
|
||||
@property
|
||||
def root_link(self):
|
||||
return self._robot.base_link
|
||||
def root_link(self) -> str:
|
||||
"""Returns the name of the base link of the robot.
|
||||
|
||||
def get_controlled_joint_names(self) -> List[str]:
|
||||
j_list = []
|
||||
for k in self._parent_map.keys():
|
||||
joint_name = self._parent_map[k]["joint_name"]
|
||||
joint = self._robot.joint_map[joint_name]
|
||||
if joint.type != "fixed" and joint.mimic is None:
|
||||
j_list.append(joint_name)
|
||||
return j_list
|
||||
Only available when the URDF is loaded with build_scene_graph=True.
|
||||
|
||||
Returns:
|
||||
str: Name of the base link.
|
||||
"""
|
||||
return self._robot.base_link
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
"""
|
||||
An experimental kinematics parser that reads the robot from a USD file or stage.
|
||||
|
||||
Basic loading of simple robots should work but more complex robots may not be supported. E.g.,
|
||||
mimic joints cannot be parsed correctly. Use the URDF parser (:class:`~UrdfKinematicsParser`)
|
||||
for more complex robots.
|
||||
"""
|
||||
|
||||
|
||||
# Standard Library
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
@@ -33,8 +42,10 @@ except ImportError:
|
||||
|
||||
class UsdKinematicsParser(KinematicsParser):
|
||||
"""An experimental kinematics parser from USD.
|
||||
NOTE: A more complete solution will be available in a future release. Current implementation
|
||||
does not account for link geometry transformations after a joints.
|
||||
|
||||
Current implementation does not account for link geometry transformations after a joints.
|
||||
Also, cannot read mimic joints.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -46,7 +57,17 @@ class UsdKinematicsParser(KinematicsParser):
|
||||
tensor_args: TensorDeviceType = TensorDeviceType(),
|
||||
extra_links: Optional[Dict[str, LinkParams]] = None,
|
||||
) -> None:
|
||||
# load usd file:
|
||||
"""Initialize instance with USD file path.
|
||||
|
||||
Args:
|
||||
usd_path: path to usd reference. This will opened as a Usd Stage.
|
||||
flip_joints: list of joint names to flip axis. This is required as current
|
||||
implementation does not read transformations from joint to link correctly.
|
||||
flip_joint_limits: list of joint names to flip joint limits.
|
||||
usd_robot_root: Root prim of the robot in the Usd Stage.
|
||||
tensor_args: Device and floating point precision for tensors.
|
||||
extra_links: Additional links to add to the robot kinematics structure.
|
||||
"""
|
||||
|
||||
# create a usd stage
|
||||
self._flip_joints = flip_joints
|
||||
@@ -59,9 +80,11 @@ class UsdKinematicsParser(KinematicsParser):
|
||||
|
||||
@property
|
||||
def robot_prim_root(self):
|
||||
"""Root prim of the robot in the Usd Stage."""
|
||||
return self._usd_robot_root
|
||||
|
||||
def build_link_parent(self):
|
||||
"""Build a dictionary containing parent link for each link in the robot."""
|
||||
self._parent_map = {}
|
||||
all_joints = [
|
||||
x
|
||||
@@ -77,16 +100,16 @@ class UsdKinematicsParser(KinematicsParser):
|
||||
def get_link_parameters(self, link_name: str, base: bool = False) -> LinkParams:
|
||||
"""Get Link parameters from usd stage.
|
||||
|
||||
NOTE: USD kinematics "X" axis joints map to "Z" in URDF. Specifically,
|
||||
USD kinematics "X" axis joints map to "Z" in URDF. Specifically,
|
||||
uniform token physics:axis = "X" value only matches "Z" in URDF. This is because of usd
|
||||
files assuming Y axis as up while urdf files assume Z axis as up.
|
||||
|
||||
Args:
|
||||
link_name (str): Name of link.
|
||||
base (bool, optional): flag to specify base link. Defaults to False.
|
||||
base (bool, optional): Is this the base link of the robot?
|
||||
|
||||
Returns:
|
||||
LinkParams: obtained link parameters.
|
||||
LinkParams: Obtained link parameters.
|
||||
"""
|
||||
link_params = self._get_from_extra_links(link_name)
|
||||
if link_params is not None:
|
||||
@@ -157,7 +180,15 @@ class UsdKinematicsParser(KinematicsParser):
|
||||
)
|
||||
return link_params
|
||||
|
||||
def _get_joint_transform(self, prim: Usd.Prim):
|
||||
def _get_joint_transform(self, prim: Usd.Prim) -> Pose:
|
||||
"""Get pose of link from joint prim.
|
||||
|
||||
Args:
|
||||
prim: joint prim in the usd stage.
|
||||
|
||||
Returns:
|
||||
Pose: pose of the link from joint origin.
|
||||
"""
|
||||
j_prim = UsdPhysics.Joint(prim)
|
||||
position = np.ravel(j_prim.GetLocalPos0Attr().Get())
|
||||
quatf = j_prim.GetLocalRot0Attr().Get()
|
||||
@@ -188,10 +219,16 @@ class UsdKinematicsParser(KinematicsParser):
|
||||
def get_links_for_joint(prim: Usd.Prim) -> Tuple[Optional[Usd.Prim], Optional[Usd.Prim]]:
|
||||
"""Get all link prims from the given joint prim.
|
||||
|
||||
Note:
|
||||
This assumes that the `body0_rel_targets` and `body1_rel_targets` are configured such
|
||||
that the parent link is specified in `body0_rel_targets` and the child links is specified
|
||||
in `body1_rel_targets`.
|
||||
|
||||
This assumes that the `body0_rel_targets` and `body1_rel_targets` are configured such
|
||||
that the parent link is specified in `body0_rel_targets` and the child links is specified
|
||||
in `body1_rel_targets`.
|
||||
|
||||
Args:
|
||||
prim: joint prim in the usd stage.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[Usd.Prim], Optional[Usd.Prim]]: parent link prim and child link prim.
|
||||
"""
|
||||
stage = prim.GetStage()
|
||||
joint_api = UsdPhysics.Joint(prim)
|
||||
|
||||
63
src/curobo/cuda_robot_model/util.py
Normal file
63
src/curobo/cuda_robot_model/util.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
|
||||
# property and proprietary rights in and to this material, related
|
||||
# documentation and any modifications thereto. Any use, reproduction,
|
||||
# disclosure or distribution of this material and related documentation
|
||||
# without an express license agreement from NVIDIA CORPORATION or
|
||||
# its affiliates is strictly prohibited.
|
||||
#
|
||||
|
||||
""" This module contains a function to load robot representation from a yaml or xrdf file. """
|
||||
|
||||
# Standard Library
|
||||
from typing import Optional, Union
|
||||
|
||||
# CuRobo
|
||||
from curobo.types.file_path import ContentPath
|
||||
from curobo.util.logger import log_error
|
||||
from curobo.util.xrdf_utils import convert_xrdf_to_curobo
|
||||
from curobo.util_file import join_path, load_yaml
|
||||
|
||||
|
||||
def load_robot_yaml(content_path: ContentPath = ContentPath()) -> dict:
|
||||
"""Load robot representation from a yaml or xrdf file.
|
||||
|
||||
Args:
|
||||
content_path: Path to the robot configuration files.
|
||||
|
||||
Returns:
|
||||
dict: Robot representation as a dictionary.
|
||||
"""
|
||||
if isinstance(content_path, str):
|
||||
log_error("content_path should be of type ContentPath")
|
||||
|
||||
robot_data = load_yaml(content_path.get_robot_configuration_path())
|
||||
|
||||
if "format" in robot_data and robot_data["format"] == "xrdf":
|
||||
robot_data = convert_xrdf_to_curobo(
|
||||
content_path=content_path,
|
||||
)
|
||||
robot_data["robot_cfg"]["kinematics"][
|
||||
"asset_root_path"
|
||||
] = content_path.robot_asset_absolute_path
|
||||
|
||||
if "robot_cfg" not in robot_data:
|
||||
robot_data["robot_cfg"] = robot_data
|
||||
if "kinematics" not in robot_data["robot_cfg"]:
|
||||
robot_data["robot_cfg"]["kinematics"] = robot_data
|
||||
if content_path.robot_urdf_absolute_path is not None:
|
||||
robot_data["robot_cfg"]["kinematics"]["urdf_path"] = content_path.robot_urdf_absolute_path
|
||||
if content_path.robot_usd_absolute_path is not None:
|
||||
robot_data["robot_cfg"]["kinematics"]["usd_path"] = content_path.robot_usd_absolute_path
|
||||
if content_path.robot_asset_absolute_path is not None:
|
||||
robot_data["robot_cfg"]["kinematics"][
|
||||
"asset_root_path"
|
||||
] = content_path.robot_asset_absolute_path
|
||||
if isinstance(robot_data["robot_cfg"]["kinematics"]["collision_spheres"], str):
|
||||
robot_data["robot_cfg"]["kinematics"]["collision_spheres"] = join_path(
|
||||
content_path.robot_config_root_path,
|
||||
robot_data["robot_cfg"]["kinematics"]["collision_spheres"],
|
||||
)
|
||||
return robot_data
|
||||
Reference in New Issue
Block a user